Completed
Branch #338-Save_posting_before_movin... (60770e)
by Schlaefer
02:30
created

EntriesTable::validationDefault()   B

Complexity

Conditions 1
Paths 1

Size

Total Lines 71

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
nc 1
nop 1
dl 0
loc 71
rs 8.6327
c 0
b 0
f 0

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\Validation\Validator;
26
use Saito\App\Registry;
27
use Saito\Posting\Posting;
28
use Saito\RememberTrait;
29
use Saito\User\CurrentUser\CurrentUserInterface;
30
use Search\Manager;
31
use Stopwatch\Lib\Stopwatch;
32
33
/**
34
 * Stores postings
35
 *
36
 * Field notes:
37
 * - `edited_by` - Came from mylittleforum. @td Should by migrated to User.id.
38
 * - `name` - Came from mylittleforum. Is still used in fulltext index.
39
 *
40
 * @property BookmarksTable $Bookmarks
41
 * @property CategoriesTable $Categories
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
 */
47
class EntriesTable extends AppTable
48
{
49
    use RememberTrait;
50
51
    /**
52
     * Fields for search plugin
53
     *
54
     * @var array
55
     */
56
    public $filterArgs = [
57
        'subject' => ['type' => 'like'],
58
        'text' => ['type' => 'like'],
59
        'name' => ['type' => 'like'],
60
        'category' => ['type' => 'value'],
61
    ];
62
63
    /**
64
     * field list necessary for displaying a thread_line
65
     *
66
     * Entry.text determine if Entry is n/t
67
     *
68
     * @var array
69
     */
70
    public $threadLineFieldList = [
71
        'Entries.id',
72
        'Entries.pid',
73
        'Entries.tid',
74
        'Entries.subject',
75
        'Entries.text',
76
        'Entries.time',
77
        'Entries.fixed',
78
        'Entries.last_answer',
79
        'Entries.views',
80
        'Entries.user_id',
81
        'Entries.locked',
82
        'Entries.name',
83
        'Entries.solves',
84
        'Users.username',
85
        'Categories.id',
86
        'Categories.accession',
87
        'Categories.category',
88
        'Categories.description'
89
    ];
90
91
    /**
92
     * fields additional to $threadLineFieldList to show complete entry
93
     *
94
     * @var array
95
     */
96
    public $showEntryFieldListAdditional = [
97
        'Entries.edited',
98
        'Entries.edited_by',
99
        'Entries.ip',
100
        'Entries.category_id',
101
        'Users.id',
102
        'Users.avatar',
103
        'Users.signature',
104
        'Users.user_place'
105
    ];
106
107
    protected $_defaultConfig = [
108
        'subject_maxlength' => 100
109
    ];
110
111
    /**
112
     * {@inheritDoc}
113
     */
114
    public function initialize(array $config)
115
    {
116
        $this->setPrimaryKey('id');
117
118
        $this->addBehavior('Posting');
119
        $this->addBehavior('IpLogging');
120
        $this->addBehavior('Timestamp');
121
        $this->addBehavior('Tree');
122
123
        $this->addBehavior(
124
            'CounterCache',
125
            [
126
                // cache how many postings a user has
127
                'Users' => ['entry_count'],
128
                // cache how many threads a category has
129
                'Categories' => [
130
                    'thread_count' => function ($event, Entry $entity, $table, $original) {
131
                        if (!$entity->isRoot()) {
132
                            return false;
133
                        }
134
                        // posting is moved to new category…
135
                        if ($original) {
136
                            // update old category (should decrement counter)
137
                            $categoryId = $entity->getOriginal('category_id');
138
                        } else {
139
                            // update new category (increment counter)
140
                            $categoryId = $entity->get('category_id');
141
                        }
142
143
                        $query = $table->find('all', ['conditions' => [
144
                            'pid' => 0, 'category_id' => $categoryId
145
                        ]]);
146
                        $count = $query->count();
147
148
                        return $count;
149
                    }
150
                ]
151
            ]
152
        );
153
154
        $this->belongsTo('Categories', ['foreignKey' => 'category_id']);
155
        $this->belongsTo('Users', ['foreignKey' => 'user_id']);
156
157
        $this->hasMany(
158
            'Bookmarks',
159
            ['foreignKey' => 'entry_id', 'dependent' => true]
160
        );
161
    }
162
163
    /**
164
     * {@inheritDoc}
165
     */
166
    public function validationDefault(Validator $validator)
167
    {
168
        $validator->setProvider(
169
            'saito',
170
            'Saito\Validation\SaitoValidationProvider'
171
        );
172
173
        /// category_id
174
        $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...
175
            ->notEmpty('category_id')
176
            ->requirePresence('category_id', 'create')
177
            ->add(
178
                'category_id',
179
                [
180
                    'numeric' => ['rule' => 'numeric'],
181
                    'assoc' => [
182
                        'rule' => ['validateAssoc', 'Categories'],
183
                        'last' => true,
184
                        'provider' => 'saito'
185
                    ]
186
                ]
187
            );
188
189
        /// last_answer
190
        $validator
191
            ->requirePresence('last_answer', 'create')
192
            ->notEmptyDateTime('last_answer', null, 'create');
193
194
        /// name
195
        $validator
196
            ->requirePresence('name', 'create')
197
            ->notEmptyString('name', null, 'create');
198
199
        /// pid
200
        $validator->requirePresence('pid', 'create');
201
202
        /// subject
203
        $validator
204
            ->notEmptyString('subject', __d('validation', 'entries.subject.notEmpty'))
205
            ->requirePresence('subject', 'create')
206
            ->add(
207
                'subject',
208
                [
209
                    'maxLength' => [
210
                        'rule' => [$this, 'validateSubjectMaxLength'],
211
                        'message' => __d(
212
                            'validation',
213
                            'entries.subject.maxlength'
214
                        )
215
                    ]
216
                ]
217
            );
218
219
        /// time
220
        $validator
221
            ->requirePresence('time', 'create')
222
            ->notEmptyDateTime('time', null, 'create');
223
224
        /// user_id
225
        $validator
226
            ->requirePresence('user_id', 'create')
227
            ->add('user_id', ['numeric' => ['rule' => 'numeric']]);
228
229
        /// views
230
        $validator->add(
231
            'views',
232
            ['comparison' => ['rule' => ['comparison', '>=', 0]]]
233
        );
234
235
        return $validator;
236
    }
237
238
    /**
239
     * Advanced search configuration from SaitoSearch plugin
240
     *
241
     * @see https://github.com/FriendsOfCake/search
242
     *
243
     * @return Manager
244
     */
245
    public function searchManager(): Manager
246
    {
247
        /** @var Manager $searchManager */
248
        $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...
249
        $searchManager
250
        ->like('subject', [
251
            'before' => true,
252
            'after' => true,
253
            'fieldMode' => 'OR',
254
            'comparison' => 'LIKE',
255
            'wildcardAny' => '*',
256
            'wildcardOne' => '?',
257
            'field' => ['subject'],
258
            'filterEmpty' => true,
259
        ])
260
        ->like('text', [
261
            'before' => true,
262
            'after' => true,
263
            'fieldMode' => 'OR',
264
            'comparison' => 'LIKE',
265
            'wildcardAny' => '*',
266
            'wildcardOne' => '?',
267
            'field' => ['text'],
268
            'filterEmpty' => true,
269
        ])
270
        ->value('name', ['filterEmpty' => true]);
271
272
        return $searchManager;
273
    }
274
275
    /**
276
     * Get recent postings
277
     *
278
     * ### Options:
279
     *
280
     * - `user_id` int|<null> If provided finds only postings of that user.
281
     * - `limit` int <10> Number of postings to find.
282
     *
283
     * @param CurrentUserInterface $User User who has access to postings
284
     * @param array $options find options
285
     *
286
     * @return array Array of Postings
287
     */
288
    public function getRecentEntries(
289
        CurrentUserInterface $User,
290
        array $options = []
291
    ) {
292
        Stopwatch::start('Model->User->getRecentEntries()');
293
294
        $options += [
295
            'user_id' => null,
296
            'limit' => 10,
297
        ];
298
299
        $options['category_id'] = $User->getCategories()->getAll('read');
300
301
        $read = function () use ($options) {
302
            $conditions = [];
303
            if ($options['user_id'] !== null) {
304
                $conditions[]['Entries.user_id'] = $options['user_id'];
305
            }
306
            if ($options['category_id'] !== null) {
307
                $conditions[]['Entries.category_id IN'] = $options['category_id'];
308
            };
309
310
            $result = $this
311
                ->find(
312
                    'all',
313
                    [
314
                        'contain' => ['Users', 'Categories'],
315
                        'fields' => $this->threadLineFieldList,
316
                        'conditions' => $conditions,
317
                        'limit' => $options['limit'],
318
                        'order' => ['time' => 'DESC']
319
                    ]
320
                )
321
                // hydrating kills performance
322
                ->enableHydration(false)
323
                ->all();
324
325
            return $result;
326
        };
327
328
        $key = 'Entry.recentEntries-' . md5(serialize($options));
329
        $results = Cache::remember($key, $read, 'entries');
330
331
        $threads = [];
332
        foreach ($results as $result) {
333
            $threads[$result['id']] = Registry::newInstance(
334
                '\Saito\Posting\Posting',
335
                ['rawData' => $result]
336
            );
337
        }
338
339
        Stopwatch::stop('Model->User->getRecentEntries()');
340
341
        return $threads;
342
    }
343
344
    /**
345
     * Finds the thread-id for a posting
346
     *
347
     * @param int $id Posting-Id
348
     * @return int Thread-Id
349
     * @throws \UnexpectedValueException
350
     */
351
    public function getThreadId($id)
352
    {
353
        $entry = $this->find(
354
            'all',
355
            ['conditions' => ['id' => $id], 'fields' => 'tid']
356
        )->first();
357
        if (empty($entry)) {
358
            throw new \UnexpectedValueException(
359
                'Posting not found. Posting-Id: ' . $id
360
            );
361
        }
362
363
        return $entry->get('tid');
364
    }
365
366
    /**
367
     * Shorthand for reading an entry with full data
368
     *
369
     * @param int $primaryKey key
370
     * @param array $options options
371
     * @return mixed Posting if found false otherwise
372
     */
373
    public function get($primaryKey, $options = [])
374
    {
375
        $options += ['return' => 'Posting'];
376
        $return = $options['return'];
377
        unset($options['return']);
378
379
        /** @var Entry */
380
        $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 393 which is incompatible with the return type declared by the interface Cake\Datasource\RepositoryInterface::get of type Cake\Datasource\EntityInterface.
Loading history...
381
            ->where([$this->getAlias() . '.id' => $primaryKey])
382
            ->first();
383
384
        if (!$result) {
385
            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...
386
        }
387
388
        switch ($return) {
389
            case 'Posting':
390
                return $result->toPosting();
391
            case 'Entity':
392
            default:
393
                return $result;
394
        }
395
    }
396
397
    /**
398
     * get parent id
399
     *
400
     * @param int $id id
401
     * @return mixed
402
     * @throws \UnexpectedValueException
403
     */
404
    public function getParentId($id)
405
    {
406
        $entry = $this->find()->select('pid')->where(['id' => $id])->first();
407
        if (!$entry) {
408
            throw new \UnexpectedValueException(
409
                'Posting not found. Posting-Id: ' . $id
410
            );
411
        }
412
413
        return $entry->get('pid');
414
    }
415
416
    /**
417
     * creates a new root or child entry for a node
418
     *
419
     * fields in $data are filtered
420
     *
421
     * @param array $data data
422
     * @return Entry|null on success, null otherwise
423
     */
424
    public function createEntry(array $data): ?Entry
425
    {
426
        $data['time'] = bDate();
427
        $data['last_answer'] = bDate();
428
429
        /** @var Entry */
430
        $posting = $this->newEntity($data);
431
        $errors = $posting->getErrors();
432
        if (!empty($errors)) {
433
            return $posting;
434
        }
435
436
        $posting = $this->save($posting);
437
        if (!$posting) {
438
            return null;
439
        }
440
441
        $id = $posting->get('id');
442
        /** @var Entry */
443
        $posting = $this->get($id, ['return' => 'Entity']);
444
445
        if ($posting->isRoot()) {
446
            // posting started a new thread, so set thread-ID to posting's own ID
447
            /** @var Entry */
448
            $posting = $this->patchEntity($posting, ['tid' => $id]);
449
            if (!$this->save($posting)) {
450
                return $posting;
451
            }
452
453
            $this->_dispatchEvent('Model.Thread.create', ['subject' => $id, 'data' => $posting]);
454
        } else {
455
            // update last answer time of root entry
456
            $this->updateAll(
457
                ['last_answer' => $posting->get('last_answer')],
458
                ['id' => $posting->get('tid')]
459
            );
460
461
            $eventData = ['subject' => $posting->get('pid'), 'data' => $posting];
462
            $this->_dispatchEvent('Model.Entry.replyToEntry', $eventData);
463
            $this->_dispatchEvent('Model.Entry.replyToThread', $eventData);
464
        }
465
466
        return $posting;
467
    }
468
469
    /**
470
     * Updates a posting
471
     *
472
     * fields in $data are filtered except for $id!
473
     *
474
     * @param Entry $posting Entity
475
     * @param array $data data
476
     * @return Entry|null
477
     */
478
    public function updateEntry(Entry $posting, array $data): ?Entry
479
    {
480
        // prevents normal user of changing category of complete thread when answering
481
        // @td this should be refactored together with the change category handling in beforeSave()
482
        if (!$posting->isRoot()) {
483
            unset($data['category_id']);
484
        }
485
486
        $data['id'] = $posting->get('id');
487
        $data['edited'] = bDate();
488
489
        /** @var Entry */
490
        $patched = $this->patchEntity($posting, $data);
491
        $errors = $patched->getErrors();
492
        if (!empty($errors)) {
493
            return $patched;
494
        }
495
496
        /** @var Entry */
497
        $new = $this->save($posting);
498
        if (!$new) {
499
            return null;
500
        }
501
502
        $this->_dispatchEvent(
503
            'Model.Entry.update',
504
            ['subject' => $posting->get('id'), 'data' => $posting]
505
        );
506
507
        return $new;
508
    }
509
510
    /**
511
     * tree of a single node and its subentries
512
     *
513
     * $options = array(
514
     *    'root' => true // performance improvements if it's a known thread-root
515
     * );
516
     *
517
     * @param int $id id
518
     * @param array $options options
519
     * @return Posting|null tree or null if nothing found
520
     */
521
    public function treeForNode(int $id, ?array $options = []): ?Posting
522
    {
523
        $options += [
524
            'root' => false,
525
            'complete' => false
526
        ];
527
528
        if ($options['root']) {
529
            $tid = $id;
530
        } else {
531
            $tid = $this->getThreadId($id);
532
        }
533
534
        $fields = null;
535
        if ($options['complete']) {
536
            $fields = array_merge(
537
                $this->threadLineFieldList,
538
                $this->showEntryFieldListAdditional
539
            );
540
        }
541
542
        $tree = $this->treesForThreads([$tid], null, $fields);
543
544
        if (!$tree) {
545
            return null;
546
        }
547
548
        $tree = reset($tree);
549
550
        //= extract subtree
551
        if ((int)$tid !== (int)$id) {
552
            $tree = $tree->getThread()->get($id);
553
        }
554
555
        return $tree;
556
    }
557
558
    /**
559
     * trees for multiple tids
560
     *
561
     * @param array $ids ids
562
     * @param array $order order
563
     * @param array $fieldlist fieldlist
564
     * @return array|null array of Postings, null if nothing found
565
     */
566
    public function treesForThreads(array $ids, ?array $order = null, array $fieldlist = null): ?array
567
    {
568
        if (empty($ids)) {
569
            return [];
570
        }
571
572
        if (empty($order)) {
573
            $order = ['last_answer' => 'ASC'];
574
        }
575
576
        if ($fieldlist === null) {
577
            $fieldlist = $this->threadLineFieldList;
578
        }
579
580
        Stopwatch::start('EntriesTable::treesForThreads() DB');
581
        $postings = $this->_getThreadEntries(
582
            $ids,
583
            ['order' => $order, 'fields' => $fieldlist]
584
        );
585
        Stopwatch::stop('EntriesTable::treesForThreads() DB');
586
587
        if (!$postings->count()) {
588
            return null;
589
        }
590
591
        Stopwatch::start('EntriesTable::treesForThreads() CPU');
592
        $threads = [];
593
        $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...
594
        foreach ($postings as $thread) {
595
            $id = $thread['tid'];
596
            $threads[$id] = $thread;
597
            $threads[$id] = Registry::newInstance(
598
                '\Saito\Posting\Posting',
599
                ['rawData' => $thread]
600
            );
601
        }
602
        Stopwatch::stop('EntriesTable::treesForThreads() CPU');
603
604
        return $threads;
605
    }
606
607
    /**
608
     * Returns all entries of threads $tid
609
     *
610
     * @param array $tid ids
611
     * @param array $params params
612
     * - 'fields' array of thread-ids: [1, 2, 5]
613
     * - 'order' sort order for threads ['time' => 'ASC'],
614
     * @return mixed unhydrated result set
615
     */
616
    protected function _getThreadEntries(array $tid, array $params = [])
617
    {
618
        $params += [
619
            'fields' => $this->threadLineFieldList,
620
            'order' => ['last_answer' => 'ASC']
621
        ];
622
623
        $threads = $this
624
            ->find(
625
                'all',
626
                [
627
                    'conditions' => ['tid IN' => $tid],
628
                    'contain' => ['Users', 'Categories'],
629
                    'fields' => $params['fields'],
630
                    'order' => $params['order']
631
                ]
632
            )
633
            // hydrating kills performance
634
            ->enableHydration(false);
635
636
        return $threads;
637
    }
638
639
    /**
640
     * Marks a sub-entry as solution to a root entry
641
     *
642
     * @param Entry $posting posting to toggle
643
     * @return bool success
644
     */
645
    public function toggleSolve(Entry $posting)
646
    {
647
        if ($posting->get('solves')) {
648
            $value = 0;
649
        } else {
650
            $value = $posting->get('tid');
651
        }
652
653
        $this->patchEntity($posting, ['solves' => $value]);
654
        if (!$this->save($posting)) {
655
            return false;
656
        }
657
658
        $this->_dispatchEvent(
659
            'Model.Entry.update',
660
            ['subject' => $posting->get('id'), 'data' => $posting]
661
        );
662
663
        return true;
664
    }
665
666
    /**
667
     * {@inheritDoc}
668
     */
669
    public function toggle($id, $key)
670
    {
671
        $result = parent::toggle($id, $key);
672
        if ($key === 'locked') {
673
            $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...
674
        }
675
676
        $entry = $this->get($id);
677
        $this->_dispatchEvent(
678
            'Model.Entry.update',
679
            [
680
                'subject' => $entry->get('id'),
681
                'data' => $entry
682
            ]
683
        );
684
685
        return $result;
686
    }
687
688
    /**
689
     * {@inheritDoc}
690
     */
691
    public function beforeValidate(
692
        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...
693
        Entity $entity,
694
        \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...
695
        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...
696
    ) {
697
        //= in n/t posting delete unnecessary body text
698
        if ($entity->isDirty('text')) {
699
            $entity->set('text', rtrim($entity->get('text')));
700
        }
701
    }
702
703
    /**
704
     * Deletes posting incl. all its subposting and associated data
705
     *
706
     * @param int $id id
707
     * @throws \InvalidArgumentException
708
     * @throws \Exception
709
     * @return bool
710
     */
711
    public function treeDeleteNode($id)
712
    {
713
        $root = $this->treeForNode((int)$id);
714
715
        if (empty($root)) {
716
            throw new \Exception;
717
        }
718
719
        $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...
720
        $nodesToDelete = array_merge($nodesToDelete, $root->getAllChildren());
721
722
        $idsToDelete = [];
723
        foreach ($nodesToDelete as $node) {
724
            $idsToDelete[] = $node->get('id');
725
        };
726
727
        $success = $this->deleteAll(['id IN' => $idsToDelete]);
728
729
        if (!$success) {
730
            return false;
731
        }
732
733
        $this->Bookmarks->deleteAll(['entry_id IN' => $idsToDelete]);
734
735
        $this->dispatchSaitoEvent(
736
            'Model.Saito.Posting.delete',
737
            ['subject' => $root, 'table' => $this]
738
        );
739
740
        return true;
741
    }
742
743
    /**
744
     * Anonymizes the entries for a user
745
     *
746
     * @param int $userId user-ID
747
     * @return void
748
     */
749
    public function anonymizeEntriesFromUser(int $userId): void
750
    {
751
        // remove username from all entries and reassign to anonyme user
752
        $success = (bool)$this->updateAll(
753
            [
754
                'edited_by' => null,
755
                'ip' => null,
756
                'name' => null,
757
                'user_id' => 0,
758
            ],
759
            ['user_id' => $userId]
760
        );
761
762
        if ($success) {
763
            $this->_dispatchEvent('Cmd.Cache.clear', ['cache' => 'Thread']);
764
        }
765
    }
766
767
    /**
768
     * Merge thread on to entry $targetId
769
     *
770
     * @param int $sourceId root-id of the posting that is merged onto another
771
     *     thread
772
     * @param int $targetId id of the posting the source-thread should be
773
     *     appended to
774
     * @return bool true if merge was successfull false otherwise
775
     */
776
    public function threadMerge($sourceId, $targetId)
777
    {
778
        $sourcePosting = $this->get($sourceId, ['return' => 'Entity']);
779
780
        // check that source is thread-root and not an subposting
781
        if (!$sourcePosting->isRoot()) {
782
            return false;
783
        }
784
785
        $targetPosting = $this->get($targetId);
786
787
        // check that target exists
788
        if (!$targetPosting) {
789
            return false;
790
        }
791
792
        // check that a thread is not merged onto itself
793
        if ($targetPosting->get('tid') === $sourcePosting->get('tid')) {
794
            return false;
795
        }
796
797
        // set target entry as new parent entry
798
        $this->patchEntity(
799
            $sourcePosting,
800
            ['pid' => $targetPosting->get('id')]
801
        );
802
        if ($this->save($sourcePosting)) {
803
            // associate all entries in source thread to target thread
804
            $this->updateAll(
805
                ['tid' => $targetPosting->get('tid')],
806
                ['tid' => $sourcePosting->get('tid')]
807
            );
808
809
            // appended source entries get category of target thread
810
            $this->_threadChangeCategory(
811
                $targetPosting->get('tid'),
812
                $targetPosting->get('category_id')
813
            );
814
815
            // update target thread last answer if source is newer
816
            $sourceLastAnswer = $sourcePosting->get('last_answer');
817
            $targetLastAnswer = $targetPosting->get('last_answer');
818
            if ($sourceLastAnswer->gt($targetLastAnswer)) {
819
                $targetRoot = $this->get(
820
                    $targetPosting->get('tid'),
821
                    ['return' => 'Entity']
822
                );
823
                $targetRoot = $this->patchEntity(
824
                    $targetRoot,
825
                    ['last_answer' => $sourceLastAnswer]
826
                );
827
                $this->save($targetRoot);
828
            }
829
830
            // propagate pinned property from target to source
831
            $isTargetPinned = $targetPosting->isLocked();
832
            $isSourcePinned = $sourcePosting->isLocked();
833
            if ($isSourcePinned !== $isTargetPinned) {
834
                $this->_threadLock($targetPosting->get('tid'), $isTargetPinned);
835
            }
836
837
            $this->_dispatchEvent(
838
                'Model.Thread.change',
839
                ['subject' => $targetPosting->get('tid')]
840
            );
841
842
            return true;
843
        }
844
845
        return false;
846
    }
847
848
    /**
849
     * Implements the custom find type 'entry'
850
     *
851
     * @param Query $query query
852
     * @return Query
853
     */
854
    public function findEntry(Query $query)
855
    {
856
        $fields = array_merge(
857
            $this->threadLineFieldList,
858
            $this->showEntryFieldListAdditional
859
        );
860
        $query->select($fields)->contain(['Users', 'Categories']);
861
862
        return $query;
863
    }
864
865
    /**
866
     * Implements the custom find type 'index paginator'
867
     *
868
     * @param Query $query query
869
     * @param array $options finder options
870
     * @return Query
871
     */
872
    public function findIndexPaginator(Query $query, array $options)
873
    {
874
        $query
875
            ->select(['id', 'pid', 'tid', 'time', 'last_answer', 'fixed'])
876
            ->where(['Entries.pid' => 0]);
877
878
        if (!empty($options['counter'])) {
879
            $query->counter($options['counter']);
880
        }
881
882
        return $query;
883
    }
884
885
    /**
886
     * Un-/Locks thread: sets posting in thread $tid to $locked
887
     *
888
     * @param int $tid thread-ID
889
     * @param bool $locked flag
890
     * @return void
891
     */
892
    protected function _threadLock($tid, $locked)
893
    {
894
        $this->updateAll(['locked' => $locked], ['tid' => $tid]);
895
    }
896
897
    /**
898
     * {@inheritDoc}
899
     */
900
    public function beforeSave(Event $event, Entity $entity)
901
    {
902
        $success = true;
903
904
        //= change category of thread if category of root entry changed
905
        if ($entity->isDirty('category_id')) {
906
            /** @var Entry */
907
            $oldEntry = $this->find()
908
                ->select(['pid', 'tid', 'category_id'])
909
                ->where(['id' => $entity->get('id')])
910
                ->first();
911
912
            if ($oldEntry && $oldEntry->isRoot()) {
913
                $newCateogry = $entity->get('category_id');
914
                $oldCategory = $oldEntry->get('category_id');
915
                if ($newCateogry !== $oldCategory) {
916
                    $success = $success && $this
917
                            ->_threadChangeCategory(
918
                                $oldEntry->get('tid'),
919
                                $entity->get('category_id')
920
                            );
921
                }
922
            }
923
        }
924
925
        if (!$success) {
926
            $event->stopPropagation();
927
        }
928
    }
929
930
    /**
931
     * check subject max length
932
     *
933
     * @param mixed $subject subject
934
     * @return bool
935
     */
936
    public function validateSubjectMaxLength($subject)
937
    {
938
        return mb_strlen($subject) <= $this->getConfig('subject_maxlength');
939
    }
940
941
    /**
942
     * Changes the category of a thread.
943
     *
944
     * Assigns the new category-id to all postings in that thread.
945
     *
946
     * @param int $tid thread-ID
947
     * @param int $newCategoryId id for new category
948
     * @return bool success
949
     * @throws NotFoundException
950
     */
951
    protected function _threadChangeCategory(int $tid, int $newCategoryId): bool
952
    {
953
        $exists = $this->Categories->exists($newCategoryId);
954
        if (!$exists) {
955
            throw new NotFoundException();
956
        }
957
        $affected = $this->updateAll(
958
            ['category_id' => $newCategoryId],
959
            ['tid' => $tid]
960
        );
961
962
        return $affected > 0;
963
    }
964
}
965