Completed
Branch feature/currentUserRefactoring (c13c1d)
by Schlaefer
04:13
created

EntriesTable   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 691
Duplicated Lines 0 %

Coupling/Cohesion

Components 3
Dependencies 14

Importance

Changes 0
Metric Value
dl 0
loc 691
rs 7.349
c 0
b 0
f 0
wmc 52
lcom 3
cbo 14

21 Methods

Rating   Name   Duplication   Size   Complexity  
A initialize() 0 50 3
B validationDefault() 0 67 1
A buildRules() 0 21 2
A searchManager() 0 29 1
A get() 0 10 2
A findEntry() 0 9 1
A getFieldset() 0 44 2
A getThreadId() 0 14 2
B createEntry() 0 44 5
A updateEntry() 0 25 3
A toggleSolve() 0 20 3
A toggle() 0 18 2
A beforeMarshal() 0 10 3
A deleteWithIds() 0 18 2
A anonymizeEntriesFromUser() 0 17 2
B threadMerge() 0 71 7
A findIndexPaginator() 0 12 2
A _threadLock() 0 4 1
A beforeSave() 0 17 4
A afterSave() 0 6 2
A _threadChangeCategory() 0 13 2

How to fix   Complexity   

Complex Class

Complex classes like EntriesTable often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use EntriesTable, and based on these observations, apply Extract Interface, too.

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 App\Model\Table\DraftsTable;
19
use Bookmarks\Model\Table\BookmarksTable;
20
use Cake\Datasource\Exception\RecordNotFoundException;
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\Posting\PostingInterface;
28
use Saito\User\CurrentUser\CurrentUserInterface;
29
use Saito\Validation\SaitoValidationProvider;
30
use Search\Manager;
31
32
/**
33
 * Stores postings
34
 *
35
 * Field notes:
36
 * - `edited_by` - Came from mylittleforum. @td Should by migrated to User.id.
37
 * - `name` - Came from mylittleforum. Is still used in fulltext index.
38
 *
39
 * @property BookmarksTable $Bookmarks
40
 * @property CategoriesTable $Categories
41
 * @property DraftsTable $Drafts
42
 * @method array treeBuild(array $postings)
43
 * @method createPosting(array $data, CurrentUserInterface $CurrentUser)
44
 * @method updatePosting(Entry $posting, array $data, CurrentUserInterface $CurrentUser)
45
 * @method array prepareChildPosting(BasicPostingInterface $parent, array $data)
46
 * @method array getRecentPostings(CurrentUserInterface $CU, ?array $options = [])
47
 * @method bool deletePosting(int $id)
48
 * @method array postingsForThreads(array $tids, ?array $order = null, ?CurrentUserInterface $CU)
49
 * @method PostingInterface postingsForThread(int $tid, ?bool $complete = false, ?CurrentUserInterface $CU)
50
 */
51
class EntriesTable extends AppTable
52
{
53
    /**
54
     * Max subject length.
55
     *
56
     * Constrained to 191 due to InnoDB index max-length on MySQL 5.6.
57
     */
58
    public const SUBJECT_MAXLENGTH = 191;
59
60
    /**
61
     * Fields for search plugin
62
     *
63
     * @var array
64
     */
65
    public $filterArgs = [
66
        'subject' => ['type' => 'like'],
67
        'text' => ['type' => 'like'],
68
        'name' => ['type' => 'like'],
69
        'category' => ['type' => 'value'],
70
    ];
71
72
    protected $_defaultConfig = [
73
        'subject_maxlength' => 100
74
    ];
75
76
    /**
77
     * {@inheritDoc}
78
     */
79
    public function initialize(array $config)
80
    {
81
        $this->setPrimaryKey('id');
82
83
        $this->addBehavior('Posting');
84
        $this->addBehavior('IpLogging');
85
        $this->addBehavior('Timestamp');
86
87
        $this->addBehavior(
88
            'CounterCache',
89
            [
90
                // cache how many postings a user has
91
                'Users' => ['entry_count'],
92
                // cache how many threads a category has
93
                'Categories' => [
94
                    'thread_count' => function ($event, Entry $entity, $table, $original) {
95
                        if (!$entity->isRoot()) {
96
                            return false;
97
                        }
98
                        // posting is moved to new category…
99
                        if ($original) {
100
                            // update old category (should decrement counter)
101
                            $categoryId = $entity->getOriginal('category_id');
102
                        } else {
103
                            // update new category (increment counter)
104
                            $categoryId = $entity->get('category_id');
105
                        }
106
107
                        $query = $table->find('all', ['conditions' => [
108
                            'pid' => 0, 'category_id' => $categoryId
109
                        ]]);
110
                        $count = $query->count();
111
112
                        return $count;
113
                    }
114
                ]
115
            ]
116
        );
117
118
        $this->belongsTo('Categories', ['foreignKey' => 'category_id']);
119
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
120
121
        $this->hasMany(
122
            'Bookmarks',
123
            ['foreignKey' => 'entry_id', 'dependent' => true]
124
        );
125
126
        // Releation never queried. Just for quick access to the table.
127
        $this->hasOne('Drafts');
128
    }
129
130
    /**
131
     * {@inheritDoc}
132
     */
133
    public function validationDefault(Validator $validator)
134
    {
135
        $validator->setProvider('saito', SaitoValidationProvider::class);
136
137
        /// category_id
138
        $categoryRequiredL10N = __('vld.entries.categories.notEmpty');
139
        $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...
140
            ->notEmpty('category_id', $categoryRequiredL10N)
141
            ->requirePresence('category_id', 'create', $categoryRequiredL10N)
142
            ->add(
143
                'category_id',
144
                [
145
                    'numeric' => ['rule' => 'numeric'],
146
                    'assoc' => [
147
                        'rule' => ['validateAssoc', 'Categories'],
148
                        'last' => true,
149
                        'provider' => 'saito'
150
                    ]
151
                ]
152
            );
153
154
        /// last_answer
155
        $validator
156
            ->requirePresence('last_answer', 'create')
157
            ->notEmptyDateTime('last_answer', null, 'create');
158
159
        /// name
160
        $validator
161
            ->requirePresence('name', 'create')
162
            ->notEmptyString('name', null, 'create');
163
164
        /// pid
165
        $validator->requirePresence('pid', 'create');
166
167
        /// subject
168
        $subjectRequiredL10N = __('vld.entries.subject.notEmpty');
169
        $validator
170
            ->notEmptyString('subject', $subjectRequiredL10N)
171
            ->requirePresence('subject', 'create', $subjectRequiredL10N)
172
            ->add(
173
                'subject',
174
                [
175
                    'maxLength' => [
176
                        'rule' => ['maxLength', $this->getConfig('subject_maxlength')],
177
                        'message' => __('vld.entries.subject.maxlength')
178
                    ]
179
                ]
180
            );
181
182
        /// time
183
        $validator
184
            ->requirePresence('time', 'create')
185
            ->notEmptyDateTime('time', null, 'create');
186
187
        /// user_id
188
        $validator
189
            ->requirePresence('user_id', 'create')
190
            ->add('user_id', ['numeric' => ['rule' => 'numeric']]);
191
192
        /// views
193
        $validator->add(
194
            'views',
195
            ['comparison' => ['rule' => ['comparison', '>=', 0]]]
196
        );
197
198
        return $validator;
199
    }
200
201
    /**
202
     * {@inheritDoc}
203
     */
204
    public function buildRules(RulesChecker $rules)
205
    {
206
        $rules = parent::buildRules($rules);
207
208
        $rules->addUpdate(
209
            function ($entity) {
210
                if ($entity->isDirty('category_id')) {
211
                    return $entity->isRoot();
212
                }
213
214
                return true;
215
            },
216
            'checkCategoryChangeOnlyOnRootPostings',
217
            [
218
                'errorField' => 'category_id',
219
                'message' => 'Cannot change category on non-root-postings.',
220
            ]
221
        );
222
223
        return $rules;
224
    }
225
226
    /**
227
     * Advanced search configuration from SaitoSearch plugin
228
     *
229
     * @see https://github.com/FriendsOfCake/search
230
     *
231
     * @return Manager
232
     */
233
    public function searchManager(): Manager
234
    {
235
        /** @var Manager $searchManager */
236
        $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...
237
        $searchManager
238
        ->like('subject', [
239
            'before' => true,
240
            'after' => true,
241
            'fieldMode' => 'OR',
242
            'comparison' => 'LIKE',
243
            'wildcardAny' => '*',
244
            'wildcardOne' => '?',
245
            'field' => ['subject'],
246
            'filterEmpty' => true,
247
        ])
248
        ->like('text', [
249
            'before' => true,
250
            'after' => true,
251
            'fieldMode' => 'OR',
252
            'comparison' => 'LIKE',
253
            'wildcardAny' => '*',
254
            'wildcardOne' => '?',
255
            'field' => ['text'],
256
            'filterEmpty' => true,
257
        ])
258
        ->value('name', ['filterEmpty' => true]);
259
260
        return $searchManager;
261
    }
262
263
    /**
264
     * Shorthand for reading an entry with full da516ta
265
     *
266
     * @param int $primaryKey key
267
     * @param array $options options
268
     * @return mixed Posting if found false otherwise
269
     */
270
    public function get($primaryKey, $options = [])
271
    {
272
        /** @var Entry */
273
        $result = $this->find('entry', ['complete' => true])
274
            ->where([$this->getAlias() . '.id' => $primaryKey])
275
            ->first();
276
277
        // @td throw exception here
278
        return empty($result) ? false : $result;
0 ignored issues
show
Bug Compatibility introduced by
The expression empty($result) ? false : $result; of type Cake\Datasource\EntityInterface|array adds the type array to the return on line 278 which is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.
Loading history...
279
    }
280
281
    /**
282
     * Implements the custom find type 'entry'
283
     *
284
     * @param Query $query query
285
     * @param array $options options
286
     * - 'complete' bool controls fieldset selected as in getFieldset($complete)
287
     * @return Query
288
     */
289
    public function findEntry(Query $query, array $options = [])
290
    {
291
        $options += ['complete' => false];
292
        $query
293
            ->select($this->getFieldset($options['complete']))
294
            ->contain(['Users', 'Categories']);
295
296
        return $query;
297
    }
298
299
    /**
300
     * Get list of fields required to display posting.:w
301
     *
302
     * You don't want to fetch every field for performance reasons.
303
     *
304
     * @param bool $complete Threadline if false; Full posting if true
305
     * @return array The fieldset
306
     */
307
    public function getFieldset(bool $complete = false): array
308
    {
309
        // field list necessary for displaying a thread_line
310
        $threadLineFieldList = [
311
            'Categories.accession',
312
            'Categories.category',
313
            'Categories.description',
314
            'Categories.id',
315
            'Entries.fixed',
316
            'Entries.id',
317
            'Entries.last_answer',
318
            'Entries.locked',
319
            'Entries.name',
320
            'Entries.pid',
321
            'Entries.solves',
322
            'Entries.subject',
323
            // Entry.text determines if Entry is n/t
324
            'Entries.text',
325
            'Entries.tid',
326
            'Entries.time',
327
            'Entries.user_id',
328
            'Entries.views',
329
            'Users.username',
330
        ];
331
332
        // fields additional to $threadLineFieldList to show complete entry
333
        $showEntryFieldListAdditional = [
334
            'Entries.category_id',
335
            'Entries.edited',
336
            'Entries.edited_by',
337
            'Entries.ip',
338
            'Users.avatar',
339
            'Users.id',
340
            'Users.signature',
341
            'Users.user_place'
342
        ];
343
344
        $fields = $threadLineFieldList;
345
        if ($complete) {
346
            $fields = array_merge($fields, $showEntryFieldListAdditional);
347
        }
348
349
        return $fields;
350
    }
351
352
    /**
353
     * Finds the thread-IT for a posting.
354
     *
355
     * @param int $id Posting-Id
356
     * @return int Thread-Id
357
     * @throws RecordNotFoundException If posting isn't found
358
     */
359
    public function getThreadId($id)
360
    {
361
        $entry = $this->find(
362
            'all',
363
            ['conditions' => ['id' => $id], 'fields' => 'tid']
364
        )->first();
365
        if (empty($entry)) {
366
            throw new RecordNotFoundException(
367
                'Posting not found. Posting-Id: ' . $id
368
            );
369
        }
370
371
        return $entry->get('tid');
372
    }
373
374
    /**
375
     * creates a new root or child entry for a node
376
     *
377
     * fields in $data are filtered
378
     *
379
     * @param array $data data
380
     * @return Entry|null on success, null otherwise
381
     */
382
    public function createEntry(array $data): ?Entry
383
    {
384
        $data['time'] = bDate();
385
        $data['last_answer'] = bDate();
386
387
        /** @var Entry */
388
        $posting = $this->newEntity($data);
389
        $errors = $posting->getErrors();
390
        if (!empty($errors)) {
391
            return $posting;
392
        }
393
394
        $posting = $this->save($posting);
395
        if (!$posting) {
396
            return null;
397
        }
398
399
        $id = $posting->get('id');
400
        /** @var Entry */
401
        $posting = $this->get($id, ['return' => 'Entity']);
402
403
        if ($posting->isRoot()) {
404
            // posting started a new thread, so set thread-ID to posting's own ID
405
            /** @var Entry */
406
            $posting = $this->patchEntity($posting, ['tid' => $id]);
0 ignored issues
show
Bug introduced by
It seems like $posting can also be of type array; however, Cake\ORM\Table::patchEntity() does only seem to accept object<Cake\Datasource\EntityInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
407
            if (!$this->save($posting)) {
408
                return $posting;
409
            }
410
411
            $this->_dispatchEvent('Model.Thread.create', ['subject' => $id, 'data' => $posting]);
412
        } else {
413
            // update last answer time of root entry
414
            $this->updateAll(
415
                ['last_answer' => $posting->get('last_answer')],
416
                ['id' => $posting->get('tid')]
417
            );
418
419
            $eventData = ['subject' => $posting->get('pid'), 'data' => $posting];
420
            $this->_dispatchEvent('Model.Entry.replyToEntry', $eventData);
421
            $this->_dispatchEvent('Model.Entry.replyToThread', $eventData);
422
        }
423
424
        return $posting;
425
    }
426
427
    /**
428
     * Updates a posting
429
     *
430
     * fields in $data are filtered except for $id!
431
     *
432
     * @param Entry $posting Entity
433
     * @param array $data data
434
     * @return Entry|null
435
     */
436
    public function updateEntry(Entry $posting, array $data): ?Entry
437
    {
438
        $data['id'] = $posting->get('id');
439
        $data['edited'] = bDate();
440
441
        /** @var Entry */
442
        $patched = $this->patchEntity($posting, $data);
443
        $errors = $patched->getErrors();
444
        if (!empty($errors)) {
445
            return $patched;
446
        }
447
448
        /** @var Entry */
449
        $new = $this->save($posting);
450
        if (empty($new)) {
451
            return null;
452
        }
453
454
        $this->_dispatchEvent(
455
            'Model.Entry.update',
456
            ['subject' => $posting->get('id'), 'data' => $posting]
457
        );
458
459
        return $new;
460
    }
461
462
    /**
463
     * Marks a sub-entry as solution to a root entry
464
     *
465
     * @param Entry $posting posting to toggle
466
     * @return bool success
467
     */
468
    public function toggleSolve(Entry $posting)
469
    {
470
        if ($posting->get('solves')) {
471
            $value = 0;
472
        } else {
473
            $value = $posting->get('tid');
474
        }
475
476
        $this->patchEntity($posting, ['solves' => $value]);
477
        if (!$this->save($posting)) {
478
            return false;
479
        }
480
481
        $this->_dispatchEvent(
482
            'Model.Entry.update',
483
            ['subject' => $posting->get('id'), 'data' => $posting]
484
        );
485
486
        return true;
487
    }
488
489
    /**
490
     * {@inheritDoc}
491
     */
492
    public function toggle($id, $key)
493
    {
494
        $result = parent::toggle($id, $key);
495
        if ($key === 'locked') {
496
            $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...
497
        }
498
499
        $entry = $this->get($id);
500
        $this->_dispatchEvent(
501
            'Model.Entry.update',
502
            [
503
                'subject' => $entry->get('id'),
504
                'data' => $entry
505
            ]
506
        );
507
508
        return $result;
509
    }
510
511
    /**
512
     * {@inheritDoc}
513
     */
514
    public function beforeMarshal(Event $event, \ArrayObject $data, \ArrayObject $options)
515
    {
516
        /// Trim whitespace on subject and text
517
        $toTrim = ['subject', 'text'];
518
        foreach ($toTrim as $field) {
519
            if (!empty($data[$field])) {
520
                $data[$field] = trim($data[$field]);
521
            }
522
        }
523
    }
524
525
    /**
526
     * Deletes posting incl. all its subposting and associated data
527
     *
528
     * @param array $idsToDelete Entry ids which should be deleted
529
     * @return bool
530
     */
531
    public function deleteWithIds(array $idsToDelete): bool
532
    {
533
        $success = $this->deleteAll(['id IN' => $idsToDelete]);
534
535
        if (!$success) {
536
            return false;
537
        }
538
539
        // @td Should be covered by dependent assoc. Add tests.
540
        $this->Bookmarks->deleteAll(['entry_id IN' => $idsToDelete]);
541
542
        $this->dispatchSaitoEvent(
543
            'Model.Saito.Postings.delete',
544
            ['subject' => $idsToDelete, 'table' => $this]
545
        );
546
547
        return true;
548
    }
549
550
    /**
551
     * Anonymizes the entries for a user
552
     *
553
     * @param int $userId user-ID
554
     * @return void
555
     */
556
    public function anonymizeEntriesFromUser(int $userId): void
557
    {
558
        // remove username from all entries and reassign to anonyme user
559
        $success = (bool)$this->updateAll(
560
            [
561
                'edited_by' => null,
562
                'ip' => null,
563
                'name' => null,
564
                'user_id' => 0,
565
            ],
566
            ['user_id' => $userId]
567
        );
568
569
        if ($success) {
570
            $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
571
        }
572
    }
573
574
    /**
575
     * Merge thread on to entry $targetId
576
     *
577
     * @param int $sourceId root-id of the posting that is merged onto another
578
     *     thread
579
     * @param int $targetId id of the posting the source-thread should be
580
     *     appended to
581
     * @return bool true if merge was successfull false otherwise
582
     */
583
    public function threadMerge($sourceId, $targetId)
584
    {
585
        $sourcePosting = $this->get($sourceId, ['return' => 'Entity']);
586
587
        // check that source is thread-root and not an subposting
588
        if (!$sourcePosting->isRoot()) {
589
            return false;
590
        }
591
592
        $targetPosting = $this->get($targetId);
593
594
        // check that target exists
595
        if (!$targetPosting) {
596
            return false;
597
        }
598
599
        // check that a thread is not merged onto itself
600
        if ($targetPosting->get('tid') === $sourcePosting->get('tid')) {
601
            return false;
602
        }
603
604
        // set target entry as new parent entry
605
        $this->patchEntity(
606
            $sourcePosting,
0 ignored issues
show
Bug introduced by
It seems like $sourcePosting defined by $this->get($sourceId, ar...('return' => 'Entity')) on line 585 can also be of type array; however, Cake\ORM\Table::patchEntity() does only seem to accept object<Cake\Datasource\EntityInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
607
            ['pid' => $targetPosting->get('id')]
608
        );
609
        if ($this->save($sourcePosting)) {
0 ignored issues
show
Bug introduced by
It seems like $sourcePosting defined by $this->get($sourceId, ar...('return' => 'Entity')) on line 585 can also be of type array; however, Cake\ORM\Table::save() does only seem to accept object<Cake\Datasource\EntityInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
610
            // associate all entries in source thread to target thread
611
            $this->updateAll(
612
                ['tid' => $targetPosting->get('tid')],
613
                ['tid' => $sourcePosting->get('tid')]
614
            );
615
616
            // appended source entries get category of target thread
617
            $this->_threadChangeCategory(
618
                $targetPosting->get('tid'),
619
                $targetPosting->get('category_id')
620
            );
621
622
            // update target thread last answer if source is newer
623
            $sourceLastAnswer = $sourcePosting->get('last_answer');
624
            $targetLastAnswer = $targetPosting->get('last_answer');
625
            if ($sourceLastAnswer->gt($targetLastAnswer)) {
626
                $targetRoot = $this->get(
627
                    $targetPosting->get('tid'),
628
                    ['return' => 'Entity']
629
                );
630
                $targetRoot = $this->patchEntity(
631
                    $targetRoot,
0 ignored issues
show
Bug introduced by
It seems like $targetRoot can also be of type array; however, Cake\ORM\Table::patchEntity() does only seem to accept object<Cake\Datasource\EntityInterface>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
632
                    ['last_answer' => $sourceLastAnswer]
633
                );
634
                $this->save($targetRoot);
635
            }
636
637
            // propagate pinned property from target to source
638
            $isTargetPinned = $targetPosting->isLocked();
639
            $isSourcePinned = $sourcePosting->isLocked();
640
            if ($isSourcePinned !== $isTargetPinned) {
641
                $this->_threadLock($targetPosting->get('tid'), $isTargetPinned);
642
            }
643
644
            $this->_dispatchEvent(
645
                'Model.Thread.change',
646
                ['subject' => $targetPosting->get('tid')]
647
            );
648
649
            return true;
650
        }
651
652
        return false;
653
    }
654
655
    /**
656
     * Implements the custom find type 'index paginator'
657
     *
658
     * @param Query $query query
659
     * @param array $options finder options
660
     * @return Query
661
     */
662
    public function findIndexPaginator(Query $query, array $options)
663
    {
664
        $query
665
            ->select(['id', 'pid', 'tid', 'time', 'last_answer', 'fixed'])
666
            ->where(['Entries.pid' => 0]);
667
668
        if (!empty($options['counter'])) {
669
            $query->counter($options['counter']);
670
        }
671
672
        return $query;
673
    }
674
675
    /**
676
     * Un-/Locks thread: sets posting in thread $tid to $locked
677
     *
678
     * @param int $tid thread-ID
679
     * @param bool $locked flag
680
     * @return void
681
     */
682
    protected function _threadLock($tid, $locked)
683
    {
684
        $this->updateAll(['locked' => $locked], ['tid' => $tid]);
685
    }
686
687
    /**
688
     * {@inheritDoc}
689
     */
690
    public function beforeSave(Event $event, Entity $entity)
691
    {
692
        $success = true;
693
694
        /// change category of thread if category of root entry changed
695
        if (!$entity->isNew() && $entity->isDirty('category_id')) {
696
            $success &= $this->_threadChangeCategory(
697
                // rules checks that only roots are allowed to change category, so tid = id
698
                $entity->get('id'),
699
                $entity->get('category_id')
700
            );
701
        }
702
703
        if (!$success) {
704
            $event->stopPropagation();
705
        }
706
    }
707
708
    /**
709
     * {@inheritDoc}
710
     */
711
    public function afterSave(Event $event, Entity $entity, \ArrayObject $options)
712
    {
713
        if ($entity->isNew()) {
714
            $this->Drafts->deleteDraftForPosting($entity);
715
        }
716
    }
717
718
    /**
719
     * Changes the category of a thread.
720
     *
721
     * Assigns the new category-id to all postings in that thread.
722
     *
723
     * @param int $tid thread-ID
724
     * @param int $newCategoryId id for new category
725
     * @return bool success
726
     * @throws NotFoundException
727
     */
728
    protected function _threadChangeCategory(int $tid, int $newCategoryId): bool
729
    {
730
        $exists = $this->Categories->exists($newCategoryId);
731
        if (!$exists) {
732
            throw new NotFoundException();
733
        }
734
        $affected = $this->updateAll(
735
            ['category_id' => $newCategoryId],
736
            ['tid' => $tid]
737
        );
738
739
        return $affected > 0;
740
    }
741
}
742