Completed
Branch feature/currentUserRefactoring (c13c1d)
by Schlaefer
09:08
created

EntriesTable::createEntry()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 43
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 24
nc 5
nop 1
dl 0
loc 43
rs 9.2248
c 0
b 0
f 0
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 function Cake\Validation\Validator::notEmpty() has been deprecated: 3.7.0 Use notEmptyString(), notEmptyArray(), notEmptyFile(), notEmptyDate(), notEmptyTime() or notEmptyDateTime() instead. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-deprecated  annotation

139
        /** @scrutinizer ignore-deprecated */ $validator

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

The explanatory message should give you some clue as to whether and when the function will be removed and what other function 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
The method searchManager() does not exist on Cake\ORM\Behavior. It seems like you code against a sub-type of Cake\ORM\Behavior such as Search\Model\Behavior\SearchBehavior. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

236
        $searchManager = $this->getBehavior('Search')->/** @scrutinizer ignore-call */ searchManager();
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 Best Practice introduced by
The expression return empty($result) ? false : $result also could return the type array|false which is incompatible with the return type mandated by Cake\Datasource\RepositoryInterface::get() of 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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $posting returns the type Cake\Datasource\EntityInterface which includes types incompatible with the type-hinted return App\Model\Entity\Entry|null.
Loading history...
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()) {
0 ignored issues
show
Bug introduced by
The method isRoot() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\Entry. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

403
        if ($posting->/** @scrutinizer ignore-call */ isRoot()) {
Loading history...
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]);
407
            if (!$this->save($posting)) {
408
                return $posting;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $posting returns the type Cake\Datasource\EntityInterface which includes types incompatible with the type-hinted return App\Model\Entity\Entry|null.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $posting returns the type array|false which is incompatible with the type-hinted return App\Model\Entity\Entry|null.
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $new returns the type Cake\Datasource\EntityInterface which includes types incompatible with the type-hinted return App\Model\Entity\Entry|null.
Loading history...
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
Bug introduced by
$result of type integer is incompatible with the type boolean expected by parameter $locked of App\Model\Table\EntriesTable::_threadLock(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

496
            $this->_threadLock($id, /** @scrutinizer ignore-type */ $result);
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)
0 ignored issues
show
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

514
    public function beforeMarshal(Event $event, \ArrayObject $data, /** @scrutinizer ignore-unused */ \ArrayObject $options)

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

Loading history...
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

514
    public function beforeMarshal(/** @scrutinizer ignore-unused */ Event $event, \ArrayObject $data, \ArrayObject $options)

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

Loading history...
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,
607
            ['pid' => $targetPosting->get('id')]
608
        );
609
        if ($this->save($sourcePosting)) {
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,
632
                    ['last_answer' => $sourceLastAnswer]
633
                );
634
                $this->save($targetRoot);
635
            }
636
637
            // propagate pinned property from target to source
638
            $isTargetPinned = $targetPosting->isLocked();
0 ignored issues
show
Bug introduced by
The method isLocked() does not exist on Cake\Datasource\EntityInterface. It seems like you code against a sub-type of Cake\Datasource\EntityInterface such as App\Model\Entity\User or App\Model\Entity\Entry. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

638
            /** @scrutinizer ignore-call */ 
639
            $isTargetPinned = $targetPosting->isLocked();
Loading history...
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)
0 ignored issues
show
Unused Code introduced by
The parameter $event is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

711
    public function afterSave(/** @scrutinizer ignore-unused */ Event $event, Entity $entity, \ArrayObject $options)

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

Loading history...
Unused Code introduced by
The parameter $options is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

711
    public function afterSave(Event $event, Entity $entity, /** @scrutinizer ignore-unused */ \ArrayObject $options)

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

Loading history...
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