Completed
Push — develop ( 92de50...a0aee2 )
by Schlaefer
06:57
created

EntriesTable::createPosting()   C

Complexity

Conditions 9
Paths 16

Size

Total Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 16
nop 1
dl 0
loc 82
rs 6.8371
c 0
b 0
f 0

1 Method

Rating   Name   Duplication   Size   Complexity  
A EntriesTable::getParentId() 0 11 2

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Saito - The Threaded Web Forum
7
 *
8
 * @copyright Copyright (c) the Saito Project Developers
9
 * @link https://github.com/Schlaefer/Saito
10
 * @license http://opensource.org/licenses/MIT
11
 */
12
13
namespace App\Model\Table;
14
15
use App\Lib\Model\Table\AppTable;
16
use App\Model\Entity\Entry;
17
use App\Model\Table\CategoriesTable;
18
use Bookmarks\Model\Table\BookmarksTable;
19
use Cake\Cache\Cache;
20
use Cake\Datasource\EntityInterface;
21
use Cake\Event\Event;
22
use Cake\Http\Exception\NotFoundException;
23
use Cake\ORM\Entity;
24
use Cake\ORM\Query;
25
use Cake\ORM\RulesChecker;
26
use Cake\Validation\Validator;
27
use Saito\App\Registry;
28
use Saito\Posting\Posting;
29
use Saito\RememberTrait;
30
use Saito\User\CurrentUser\CurrentUserInterface;
31
use Search\Manager;
32
use Stopwatch\Lib\Stopwatch;
33
34
/**
35
 * Stores postings
36
 *
37
 * Field notes:
38
 * - `edited_by` - Came from mylittleforum. @td Should by migrated to User.id.
39
 * - `name` - Came from mylittleforum. Is still used in fulltext index.
40
 *
41
 * @property BookmarksTable $Bookmarks
42
 * @property CategoriesTable $Categories
43
 * @method array treeBuild(array $postings)
44
 * @method createPosting(array $data, CurrentUserInterface $CurrentUser)
45
 * @method updatePosting(Entry $posting, array $data, CurrentUserInterface $CurrentUser)
46
 * @method array prepareChildPosting(BasicPostingInterface $parent, array $data)
47
 */
48
class EntriesTable extends AppTable
49
{
50
    use RememberTrait;
51
52
    /**
53
     * Fields for search plugin
54
     *
55
     * @var array
56
     */
57
    public $filterArgs = [
58
        'subject' => ['type' => 'like'],
59
        'text' => ['type' => 'like'],
60
        'name' => ['type' => 'like'],
61
        'category' => ['type' => 'value'],
62
    ];
63
64
    /**
65
     * field list necessary for displaying a thread_line
66
     *
67
     * Entry.text determine if Entry is n/t
68
     *
69
     * @var array
70
     */
71
    public $threadLineFieldList = [
72
        'Entries.id',
73
        'Entries.pid',
74
        'Entries.tid',
75
        'Entries.subject',
76
        'Entries.text',
77
        'Entries.time',
78
        'Entries.fixed',
79
        'Entries.last_answer',
80
        'Entries.views',
81
        'Entries.user_id',
82
        'Entries.locked',
83
        'Entries.name',
84
        'Entries.solves',
85
        'Users.username',
86
        'Categories.id',
87
        'Categories.accession',
88
        'Categories.category',
89
        'Categories.description'
90
    ];
91
92
    /**
93
     * fields additional to $threadLineFieldList to show complete entry
94
     *
95
     * @var array
96
     */
97
    public $showEntryFieldListAdditional = [
98
        'Entries.edited',
99
        'Entries.edited_by',
100
        'Entries.ip',
101
        'Entries.category_id',
102
        'Users.id',
103
        'Users.avatar',
104
        'Users.signature',
105
        'Users.user_place'
106
    ];
107
108
    protected $_defaultConfig = [
109
        'subject_maxlength' => 100
110
    ];
111
112
    /**
113
     * {@inheritDoc}
114
     */
115
    public function initialize(array $config)
116
    {
117
        $this->setPrimaryKey('id');
118
119
        $this->addBehavior('Posting');
120
        $this->addBehavior('IpLogging');
121
        $this->addBehavior('Timestamp');
122
        $this->addBehavior('Tree');
123
124
        $this->addBehavior(
125
            'CounterCache',
126
            [
127
                // cache how many postings a user has
128
                'Users' => ['entry_count'],
129
                // cache how many threads a category has
130
                'Categories' => [
131
                    'thread_count' => function ($event, Entry $entity, $table, $original) {
132
                        if (!$entity->isRoot()) {
133
                            return false;
134
                        }
135
                        // posting is moved to new category…
136
                        if ($original) {
137
                            // update old category (should decrement counter)
138
                            $categoryId = $entity->getOriginal('category_id');
139
                        } else {
140
                            // update new category (increment counter)
141
                            $categoryId = $entity->get('category_id');
142
                        }
143
144
                        $query = $table->find('all', ['conditions' => [
145
                            'pid' => 0, 'category_id' => $categoryId
146
                        ]]);
147
                        $count = $query->count();
148
149
                        return $count;
150
                    }
151
                ]
152
            ]
153
        );
154
155
        $this->belongsTo('Categories', ['foreignKey' => 'category_id']);
156
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
157
158
        $this->hasMany(
159
            'Bookmarks',
160
            ['foreignKey' => 'entry_id', 'dependent' => true]
161
        );
162
    }
163
164
    /**
165
     * {@inheritDoc}
166
     */
167
    public function validationDefault(Validator $validator)
168
    {
169
        $validator->setProvider(
170
            'saito',
171
            'Saito\Validation\SaitoValidationProvider'
172
        );
173
174
        /// category_id
175
        $validator
0 ignored issues
show
Deprecated Code introduced by
The method Cake\Validation\Validator::notEmpty() has been deprecated with message: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead.

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
176
            ->notEmpty('category_id')
177
            ->requirePresence('category_id', 'create')
178
            ->add(
179
                'category_id',
180
                [
181
                    'numeric' => ['rule' => 'numeric'],
182
                    'assoc' => [
183
                        'rule' => ['validateAssoc', 'Categories'],
184
                        'last' => true,
185
                        'provider' => 'saito'
186
                    ]
187
                ]
188
            );
189
190
        /// last_answer
191
        $validator
192
            ->requirePresence('last_answer', 'create')
193
            ->notEmptyDateTime('last_answer', null, 'create');
194
195
        /// name
196
        $validator
197
            ->requirePresence('name', 'create')
198
            ->notEmptyString('name', null, 'create');
199
200
        /// pid
201
        $validator->requirePresence('pid', 'create');
202
203
        /// subject
204
        $validator
205
            ->notEmptyString('subject', __d('validation', 'entries.subject.notEmpty'))
206
            ->requirePresence('subject', 'create')
207
            ->add(
208
                'subject',
209
                [
210
                    'maxLength' => [
211
                        'rule' => [$this, 'validateSubjectMaxLength'],
212
                        'message' => __d(
213
                            'validation',
214
                            'entries.subject.maxlength'
215
                        )
216
                    ]
217
                ]
218
            );
219
220
        /// time
221
        $validator
222
            ->requirePresence('time', 'create')
223
            ->notEmptyDateTime('time', null, 'create');
224
225
        /// user_id
226
        $validator
227
            ->requirePresence('user_id', 'create')
228
            ->add('user_id', ['numeric' => ['rule' => 'numeric']]);
229
230
        /// views
231
        $validator->add(
232
            'views',
233
            ['comparison' => ['rule' => ['comparison', '>=', 0]]]
234
        );
235
236
        return $validator;
237
    }
238
239
    /**
240
     * {@inheritDoc}
241
     */
242
    public function buildRules(RulesChecker $rules)
243
    {
244
        $rules = parent::buildRules($rules);
245
246
        $rules->addUpdate(
247
            function ($entity) {
248
                if ($entity->isDirty('category_id')) {
249
                    return $entity->isRoot();
250
                }
251
252
                return true;
253
            },
254
            'checkCategoryChangeOnlyOnRootPostings',
255
            [
256
                'errorField' => 'category_id',
257
                'message' => 'Cannot change category on non-root-postings.',
258
            ]
259
        );
260
261
        return $rules;
262
    }
263
264
    /**
265
     * Advanced search configuration from SaitoSearch plugin
266
     *
267
     * @see https://github.com/FriendsOfCake/search
268
     *
269
     * @return Manager
270
     */
271
    public function searchManager(): Manager
272
    {
273
        /** @var Manager $searchManager */
274
        $searchManager = $this->getBehavior('Search')->searchManager();
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Cake\ORM\Behavior as the method searchManager() does only exist in the following sub-classes of Cake\ORM\Behavior: Search\Model\Behavior\SearchBehavior. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
275
        $searchManager
276
        ->like('subject', [
277
            'before' => true,
278
            'after' => true,
279
            'fieldMode' => 'OR',
280
            'comparison' => 'LIKE',
281
            'wildcardAny' => '*',
282
            'wildcardOne' => '?',
283
            'field' => ['subject'],
284
            'filterEmpty' => true,
285
        ])
286
        ->like('text', [
287
            'before' => true,
288
            'after' => true,
289
            'fieldMode' => 'OR',
290
            'comparison' => 'LIKE',
291
            'wildcardAny' => '*',
292
            'wildcardOne' => '?',
293
            'field' => ['text'],
294
            'filterEmpty' => true,
295
        ])
296
        ->value('name', ['filterEmpty' => true]);
297
298
        return $searchManager;
299
    }
300
301
    /**
302
     * Get recent postings
303
     *
304
     * ### Options:
305
     *
306
     * - `user_id` int|<null> If provided finds only postings of that user.
307
     * - `limit` int <10> Number of postings to find.
308
     *
309
     * @param CurrentUserInterface $User User who has access to postings
310
     * @param array $options find options
311
     *
312
     * @return array Array of Postings
313
     */
314
    public function getRecentEntries(
315
        CurrentUserInterface $User,
316
        array $options = []
317
    ) {
318
        Stopwatch::start('Model->User->getRecentEntries()');
319
320
        $options += [
321
            'user_id' => null,
322
            'limit' => 10,
323
        ];
324
325
        $options['category_id'] = $User->getCategories()->getAll('read');
326
327
        $read = function () use ($options) {
328
            $conditions = [];
329
            if ($options['user_id'] !== null) {
330
                $conditions[]['Entries.user_id'] = $options['user_id'];
331
            }
332
            if ($options['category_id'] !== null) {
333
                $conditions[]['Entries.category_id IN'] = $options['category_id'];
334
            };
335
336
            $result = $this
337
                ->find(
338
                    'all',
339
                    [
340
                        'contain' => ['Users', 'Categories'],
341
                        'fields' => $this->threadLineFieldList,
342
                        'conditions' => $conditions,
343
                        'limit' => $options['limit'],
344
                        'order' => ['time' => 'DESC']
345
                    ]
346
                )
347
                // hydrating kills performance
348
                ->enableHydration(false)
349
                ->all();
350
351
            return $result;
352
        };
353
354
        $key = 'Entry.recentEntries-' . md5(serialize($options));
355
        $results = Cache::remember($key, $read, 'entries');
356
357
        $threads = [];
358
        foreach ($results as $result) {
359
            $threads[$result['id']] = Registry::newInstance(
360
                '\Saito\Posting\Posting',
361
                ['rawData' => $result]
362
            );
363
        }
364
365
        Stopwatch::stop('Model->User->getRecentEntries()');
366
367
        return $threads;
368
    }
369
370
    /**
371
     * Finds the thread-id for a posting
372
     *
373
     * @param int $id Posting-Id
374
     * @return int Thread-Id
375
     * @throws \UnexpectedValueException
376
     */
377
    public function getThreadId($id)
378
    {
379
        $entry = $this->find(
380
            'all',
381
            ['conditions' => ['id' => $id], 'fields' => 'tid']
382
        )->first();
383
        if (empty($entry)) {
384
            throw new \UnexpectedValueException(
385
                'Posting not found. Posting-Id: ' . $id
386
            );
387
        }
388
389
        return $entry->get('tid');
390
    }
391
392
    /**
393
     * Shorthand for reading an entry with full data
394
     *
395
     * @param int $primaryKey key
396
     * @param array $options options
397
     * @return mixed Posting if found false otherwise
398
     */
399
    public function get($primaryKey, $options = [])
400
    {
401
        $options += ['return' => 'Posting'];
402
        $return = $options['return'];
403
        unset($options['return']);
404
405
        /** @var Entry */
406
        $result = $this->find('entry')
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->find('entry')->wh...$primaryKey))->first(); of type Cake\Datasource\EntityInterface|array|null adds the type array to the return on line 419 which is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.
Loading history...
407
            ->where([$this->getAlias() . '.id' => $primaryKey])
408
            ->first();
409
410
        if (!$result) {
411
            return false;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return false; (false) is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
412
        }
413
414
        switch ($return) {
415
            case 'Posting':
416
                return $result->toPosting();
417
            case 'Entity':
418
            default:
419
                return $result;
420
        }
421
    }
422
423
    /**
424
     * get parent id
425
     *
426
     * @param int $id id
427
     * @return mixed
428
     * @throws \UnexpectedValueException
429
     */
430
    public function getParentId($id)
431
    {
432
        $entry = $this->find()->select('pid')->where(['id' => $id])->first();
433
        if (!$entry) {
434
            throw new \UnexpectedValueException(
435
                'Posting not found. Posting-Id: ' . $id
436
            );
437
        }
438
439
        return $entry->get('pid');
440
    }
441
442
    /**
443
     * creates a new root or child entry for a node
444
     *
445
     * fields in $data are filtered
446
     *
447
     * @param array $data data
448
     * @return Entry|null on success, null otherwise
449
     */
450
    public function createEntry(array $data): ?Entry
451
    {
452
        $data['time'] = bDate();
453
        $data['last_answer'] = bDate();
454
455
        /** @var Entry */
456
        $posting = $this->newEntity($data);
457
        $errors = $posting->getErrors();
458
        if (!empty($errors)) {
459
            return $posting;
460
        }
461
462
        $posting = $this->save($posting);
463
        if (!$posting) {
464
            return null;
465
        }
466
467
        $id = $posting->get('id');
468
        /** @var Entry */
469
        $posting = $this->get($id, ['return' => 'Entity']);
470
471
        if ($posting->isRoot()) {
472
            // posting started a new thread, so set thread-ID to posting's own ID
473
            /** @var Entry */
474
            $posting = $this->patchEntity($posting, ['tid' => $id]);
475
            if (!$this->save($posting)) {
476
                return $posting;
477
            }
478
479
            $this->_dispatchEvent('Model.Thread.create', ['subject' => $id, 'data' => $posting]);
480
        } else {
481
            // update last answer time of root entry
482
            $this->updateAll(
483
                ['last_answer' => $posting->get('last_answer')],
484
                ['id' => $posting->get('tid')]
485
            );
486
487
            $eventData = ['subject' => $posting->get('pid'), 'data' => $posting];
488
            $this->_dispatchEvent('Model.Entry.replyToEntry', $eventData);
489
            $this->_dispatchEvent('Model.Entry.replyToThread', $eventData);
490
        }
491
492
        return $posting;
493
    }
494
495
    /**
496
     * Updates a posting
497
     *
498
     * fields in $data are filtered except for $id!
499
     *
500
     * @param Entry $posting Entity
501
     * @param array $data data
502
     * @return Entry|null
503
     */
504
    public function updateEntry(Entry $posting, array $data): ?Entry
505
    {
506
        $data['id'] = $posting->get('id');
507
        $data['edited'] = bDate();
508
509
        /** @var Entry */
510
        $patched = $this->patchEntity($posting, $data);
511
        $errors = $patched->getErrors();
512
        if (!empty($errors)) {
513
            return $patched;
514
        }
515
516
        /** @var Entry */
517
        $new = $this->save($posting);
518
        if (!$new) {
519
            return null;
520
        }
521
522
        $this->_dispatchEvent(
523
            'Model.Entry.update',
524
            ['subject' => $posting->get('id'), 'data' => $posting]
525
        );
526
527
        return $new;
528
    }
529
530
    /**
531
     * tree of a single node and its subentries
532
     *
533
     * $options = array(
534
     *    'root' => true // performance improvements if it's a known thread-root
535
     * );
536
     *
537
     * @param int $id id
538
     * @param array $options options
539
     * @return Posting|null tree or null if nothing found
540
     */
541
    public function treeForNode(int $id, ?array $options = []): ?Posting
542
    {
543
        $options += [
544
            'root' => false,
545
            'complete' => false
546
        ];
547
548
        if ($options['root']) {
549
            $tid = $id;
550
        } else {
551
            $tid = $this->getThreadId($id);
552
        }
553
554
        $fields = null;
555
        if ($options['complete']) {
556
            $fields = array_merge(
557
                $this->threadLineFieldList,
558
                $this->showEntryFieldListAdditional
559
            );
560
        }
561
562
        $tree = $this->treesForThreads([$tid], null, $fields);
563
564
        if (!$tree) {
565
            return null;
566
        }
567
568
        $tree = reset($tree);
569
570
        //= extract subtree
571
        if ((int)$tid !== (int)$id) {
572
            $tree = $tree->getThread()->get($id);
573
        }
574
575
        return $tree;
576
    }
577
578
    /**
579
     * trees for multiple tids
580
     *
581
     * @param array $ids ids
582
     * @param array $order order
583
     * @param array $fieldlist fieldlist
584
     * @return array|null array of Postings, null if nothing found
585
     */
586
    public function treesForThreads(array $ids, ?array $order = null, array $fieldlist = null): ?array
587
    {
588
        if (empty($ids)) {
589
            return [];
590
        }
591
592
        if (empty($order)) {
593
            $order = ['last_answer' => 'ASC'];
594
        }
595
596
        if ($fieldlist === null) {
597
            $fieldlist = $this->threadLineFieldList;
598
        }
599
600
        Stopwatch::start('EntriesTable::treesForThreads() DB');
601
        $postings = $this->_getThreadEntries(
602
            $ids,
603
            ['order' => $order, 'fields' => $fieldlist]
604
        );
605
        Stopwatch::stop('EntriesTable::treesForThreads() DB');
606
607
        if (!$postings->count()) {
608
            return null;
609
        }
610
611
        Stopwatch::start('EntriesTable::treesForThreads() CPU');
612
        $threads = [];
613
        $postings = $this->treeBuild($postings);
0 ignored issues
show
Documentation introduced by
$postings is of type object<Cake\ORM\Query>, but the function expects a array.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
614
        foreach ($postings as $thread) {
615
            $id = $thread['tid'];
616
            $threads[$id] = $thread;
617
            $threads[$id] = Registry::newInstance(
618
                '\Saito\Posting\Posting',
619
                ['rawData' => $thread]
620
            );
621
        }
622
        Stopwatch::stop('EntriesTable::treesForThreads() CPU');
623
624
        return $threads;
625
    }
626
627
    /**
628
     * Returns all entries of threads $tid
629
     *
630
     * @param array $tid ids
631
     * @param array $params params
632
     * - 'fields' array of thread-ids: [1, 2, 5]
633
     * - 'order' sort order for threads ['time' => 'ASC'],
634
     * @return mixed unhydrated result set
635
     */
636
    protected function _getThreadEntries(array $tid, array $params = [])
637
    {
638
        $params += [
639
            'fields' => $this->threadLineFieldList,
640
            'order' => ['last_answer' => 'ASC']
641
        ];
642
643
        $threads = $this
644
            ->find(
645
                'all',
646
                [
647
                    'conditions' => ['tid IN' => $tid],
648
                    'contain' => ['Users', 'Categories'],
649
                    'fields' => $params['fields'],
650
                    'order' => $params['order']
651
                ]
652
            )
653
            // hydrating kills performance
654
            ->enableHydration(false);
655
656
        return $threads;
657
    }
658
659
    /**
660
     * Marks a sub-entry as solution to a root entry
661
     *
662
     * @param Entry $posting posting to toggle
663
     * @return bool success
664
     */
665
    public function toggleSolve(Entry $posting)
666
    {
667
        if ($posting->get('solves')) {
668
            $value = 0;
669
        } else {
670
            $value = $posting->get('tid');
671
        }
672
673
        $this->patchEntity($posting, ['solves' => $value]);
674
        if (!$this->save($posting)) {
675
            return false;
676
        }
677
678
        $this->_dispatchEvent(
679
            'Model.Entry.update',
680
            ['subject' => $posting->get('id'), 'data' => $posting]
681
        );
682
683
        return true;
684
    }
685
686
    /**
687
     * {@inheritDoc}
688
     */
689
    public function toggle($id, $key)
690
    {
691
        $result = parent::toggle($id, $key);
692
        if ($key === 'locked') {
693
            $this->_threadLock($id, $result);
0 ignored issues
show
Documentation introduced by
$result is of type integer, but the function expects a boolean.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
694
        }
695
696
        $entry = $this->get($id);
697
        $this->_dispatchEvent(
698
            'Model.Entry.update',
699
            [
700
                'subject' => $entry->get('id'),
701
                'data' => $entry
702
            ]
703
        );
704
705
        return $result;
706
    }
707
708
    /**
709
     * {@inheritDoc}
710
     */
711
    public function beforeValidate(
712
        Event $event,
1 ignored issue
show
Unused Code introduced by
The parameter $event is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
713
        Entity $entity,
714
        \ArrayObject $options,
1 ignored issue
show
Unused Code introduced by
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
715
        Validator $validator
0 ignored issues
show
Unused Code introduced by
The parameter $validator is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
716
    ) {
717
        //= in n/t posting delete unnecessary body text
718
        // @bogus move to entity?
719
        if ($entity->isDirty('text')) {
720
            $entity->set('text', rtrim($entity->get('text')));
721
        }
722
    }
723
724
    /**
725
     * Deletes posting incl. all its subposting and associated data
726
     *
727
     * @param int $id id
728
     * @throws \InvalidArgumentException
729
     * @throws \Exception
730
     * @return bool
731
     */
732
    public function treeDeleteNode($id)
733
    {
734
        $root = $this->treeForNode((int)$id);
735
736
        if (empty($root)) {
737
            throw new \Exception;
738
        }
739
740
        $nodesToDelete[] = $root;
0 ignored issues
show
Coding Style Comprehensibility introduced by
$nodesToDelete was never initialized. Although not strictly required by PHP, it is generally a good practice to add $nodesToDelete = array(); before regardless.

Adding an explicit array definition is generally preferable to implicit array definition as it guarantees a stable state of the code.

Let’s take a look at an example:

foreach ($collection as $item) {
    $myArray['foo'] = $item->getFoo();

    if ($item->hasBar()) {
        $myArray['bar'] = $item->getBar();
    }

    // do something with $myArray
}

As you can see in this example, the array $myArray is initialized the first time when the foreach loop is entered. You can also see that the value of the bar key is only written conditionally; thus, its value might result from a previous iteration.

This might or might not be intended. To make your intention clear, your code more readible and to avoid accidental bugs, we recommend to add an explicit initialization $myArray = array() either outside or inside the foreach loop.

Loading history...
741
        $nodesToDelete = array_merge($nodesToDelete, $root->getAllChildren());
742
743
        $idsToDelete = [];
744
        foreach ($nodesToDelete as $node) {
745
            $idsToDelete[] = $node->get('id');
746
        };
747
748
        $success = $this->deleteAll(['id IN' => $idsToDelete]);
749
750
        if (!$success) {
751
            return false;
752
        }
753
754
        $this->Bookmarks->deleteAll(['entry_id IN' => $idsToDelete]);
755
756
        $this->dispatchSaitoEvent(
757
            'Model.Saito.Posting.delete',
758
            ['subject' => $root, 'table' => $this]
759
        );
760
761
        return true;
762
    }
763
764
    /**
765
     * Anonymizes the entries for a user
766
     *
767
     * @param int $userId user-ID
768
     * @return void
769
     */
770
    public function anonymizeEntriesFromUser(int $userId): void
771
    {
772
        // remove username from all entries and reassign to anonyme user
773
        $success = (bool)$this->updateAll(
774
            [
775
                'edited_by' => null,
776
                'ip' => null,
777
                'name' => null,
778
                'user_id' => 0,
779
            ],
780
            ['user_id' => $userId]
781
        );
782
783
        if ($success) {
784
            $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
785
        }
786
    }
787
788
    /**
789
     * Merge thread on to entry $targetId
790
     *
791
     * @param int $sourceId root-id of the posting that is merged onto another
792
     *     thread
793
     * @param int $targetId id of the posting the source-thread should be
794
     *     appended to
795
     * @return bool true if merge was successfull false otherwise
796
     */
797
    public function threadMerge($sourceId, $targetId)
798
    {
799
        $sourcePosting = $this->get($sourceId, ['return' => 'Entity']);
800
801
        // check that source is thread-root and not an subposting
802
        if (!$sourcePosting->isRoot()) {
803
            return false;
804
        }
805
806
        $targetPosting = $this->get($targetId);
807
808
        // check that target exists
809
        if (!$targetPosting) {
810
            return false;
811
        }
812
813
        // check that a thread is not merged onto itself
814
        if ($targetPosting->get('tid') === $sourcePosting->get('tid')) {
815
            return false;
816
        }
817
818
        // set target entry as new parent entry
819
        $this->patchEntity(
820
            $sourcePosting,
821
            ['pid' => $targetPosting->get('id')]
822
        );
823
        if ($this->save($sourcePosting)) {
824
            // associate all entries in source thread to target thread
825
            $this->updateAll(
826
                ['tid' => $targetPosting->get('tid')],
827
                ['tid' => $sourcePosting->get('tid')]
828
            );
829
830
            // appended source entries get category of target thread
831
            $this->_threadChangeCategory(
832
                $targetPosting->get('tid'),
833
                $targetPosting->get('category_id')
834
            );
835
836
            // update target thread last answer if source is newer
837
            $sourceLastAnswer = $sourcePosting->get('last_answer');
838
            $targetLastAnswer = $targetPosting->get('last_answer');
839
            if ($sourceLastAnswer->gt($targetLastAnswer)) {
840
                $targetRoot = $this->get(
841
                    $targetPosting->get('tid'),
842
                    ['return' => 'Entity']
843
                );
844
                $targetRoot = $this->patchEntity(
845
                    $targetRoot,
846
                    ['last_answer' => $sourceLastAnswer]
847
                );
848
                $this->save($targetRoot);
849
            }
850
851
            // propagate pinned property from target to source
852
            $isTargetPinned = $targetPosting->isLocked();
853
            $isSourcePinned = $sourcePosting->isLocked();
854
            if ($isSourcePinned !== $isTargetPinned) {
855
                $this->_threadLock($targetPosting->get('tid'), $isTargetPinned);
856
            }
857
858
            $this->_dispatchEvent(
859
                'Model.Thread.change',
860
                ['subject' => $targetPosting->get('tid')]
861
            );
862
863
            return true;
864
        }
865
866
        return false;
867
    }
868
869
    /**
870
     * Implements the custom find type 'entry'
871
     *
872
     * @param Query $query query
873
     * @return Query
874
     */
875
    public function findEntry(Query $query)
876
    {
877
        $fields = array_merge(
878
            $this->threadLineFieldList,
879
            $this->showEntryFieldListAdditional
880
        );
881
        $query->select($fields)->contain(['Users', 'Categories']);
882
883
        return $query;
884
    }
885
886
    /**
887
     * Implements the custom find type 'index paginator'
888
     *
889
     * @param Query $query query
890
     * @param array $options finder options
891
     * @return Query
892
     */
893
    public function findIndexPaginator(Query $query, array $options)
894
    {
895
        $query
896
            ->select(['id', 'pid', 'tid', 'time', 'last_answer', 'fixed'])
897
            ->where(['Entries.pid' => 0]);
898
899
        if (!empty($options['counter'])) {
900
            $query->counter($options['counter']);
901
        }
902
903
        return $query;
904
    }
905
906
    /**
907
     * Un-/Locks thread: sets posting in thread $tid to $locked
908
     *
909
     * @param int $tid thread-ID
910
     * @param bool $locked flag
911
     * @return void
912
     */
913
    protected function _threadLock($tid, $locked)
914
    {
915
        $this->updateAll(['locked' => $locked], ['tid' => $tid]);
916
    }
917
918
    /**
919
     * {@inheritDoc}
920
     */
921
    public function beforeSave(Event $event, Entity $entity)
922
    {
923
        $success = true;
924
925
        /// change category of thread if category of root entry changed
926
        if (!$entity->isNew() && $entity->isDirty('category_id')) {
927
            $success &= $this->_threadChangeCategory(
928
                // rules checks that only roots are allowed to change category, so tid = id
929
                $entity->get('id'),
930
                $entity->get('category_id')
931
            );
932
        }
933
934
        if (!$success) {
935
            $event->stopPropagation();
936
        }
937
    }
938
939
    /**
940
     * check subject max length
941
     *
942
     * @param mixed $subject subject
943
     * @return bool
944
     */
945
    public function validateSubjectMaxLength($subject)
946
    {
947
        return mb_strlen($subject) <= $this->getConfig('subject_maxlength');
948
    }
949
950
    /**
951
     * Changes the category of a thread.
952
     *
953
     * Assigns the new category-id to all postings in that thread.
954
     *
955
     * @param int $tid thread-ID
956
     * @param int $newCategoryId id for new category
957
     * @return bool success
958
     * @throws NotFoundException
959
     */
960
    protected function _threadChangeCategory(int $tid, int $newCategoryId): bool
961
    {
962
        $exists = $this->Categories->exists($newCategoryId);
963
        if (!$exists) {
964
            throw new NotFoundException();
965
        }
966
        $affected = $this->updateAll(
967
            ['category_id' => $newCategoryId],
968
            ['tid' => $tid]
969
        );
970
971
        return $affected > 0;
972
    }
973
}
974