Completed
Push — 2.0 ( aaf296...5e3f46 )
by Christopher
02:49
created

ContentsTable   B

Complexity

Total Complexity 53

Size/Duplication

Total Lines 389
Duplicated Lines 8.74 %

Coupling/Cohesion

Components 2
Dependencies 10

Importance

Changes 4
Bugs 0 Features 0
Metric Value
wmc 53
c 4
b 0
f 0
lcom 2
cbo 10
dl 34
loc 389
rs 7.4757

10 Methods

Rating   Name   Duplication   Size   Complexity  
A implementedEvents() 0 11 1
C initialize() 0 99 7
A validationDefault() 16 16 1
A beforeSave() 0 6 1
B _saveRevision() 0 31 5
D _ensureStatus() 0 30 9
C operatorPromote() 9 24 7
B operatorAuthor() 0 21 5
B operatorterm() 9 31 6
C _calculateHash() 0 23 11

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ContentsTable 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 ContentsTable, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Content\Model\Table;
13
14
use Cake\Event\Event;
15
use Cake\ORM\Entity;
16
use Cake\ORM\Query;
17
use Cake\ORM\Table;
18
use Cake\ORM\TableRegistry;
19
use Cake\Validation\Validator;
20
use Search\Parser\TokenInterface;
21
use \ArrayObject;
22
23
/**
24
 * Represents "contents" database table.
25
 *
26
 * @property \Content\Model\Table\ContentTypesTable $ContentTypes
27
 * @property \Content\Model\Table\ContentsTable $TranslationOf
28
 * @property \User\Model\Table\RolesTable $Roles
29
 * @property \User\Model\Table\ContentRevisionsTable $ContentRevisions
30
 * @property \User\Model\Table\ContentsTable $Translations
31
 * @property \User\Model\Table\UsersTable $Author
32
 * @property \User\Model\Table\UsersTable $ModifiedBy
33
 * @method \Search\Engine\BaseEngine searchEngine(\Search\Engine\BaseEngine $engine = null)
34
 * @method \Cake\ORM\Query search(string $criteria, \Cake\ORM\Query|null $query = null)
35
 * @method \Cake\ORM\Query applySearchOperator(\Cake\ORM\Query $query, \Search\Parser\TokenInterface $token)
36
 * @method void addSearchOperator(string $name, mixed $handler, array $options = [])
37
 * @method void enableSearchOperator(string $name)
38
 * @method void disableSearchOperator(string $name)
39
 * @method void bindComments()
40
 * @method void unbindComments()
41
 * @method void configureFieldable(array $config)
42
 * @method void bindFieldable()
43
 * @method void unbindFieldable()
44
 * @method \Cake\Datasource\EntityInterface attachFields(\Cake\Datasource\EntityInterface $entity)
45
 * @method \Cake\Datasource\ResultSetDecorator findComments(\Cake\ORM\Query $query, $options)
46
 */
47
class ContentsTable extends Table
48
{
49
50
    /**
51
     * List of implemented events.
52
     *
53
     * @return array
54
     */
55
    public function implementedEvents()
56
    {
57
        $events = [
58
            'Model.beforeSave' => [
59
                'callable' => 'beforeSave',
60
                'priority' => 16 // after Fieldable Behavior
61
            ]
62
        ];
63
64
        return $events;
65
    }
66
67
    /**
68
     * Initialize a table instance. Called after the constructor.
69
     *
70
     * @param array $config Configuration options passed to the constructor
71
     * @return void
72
     */
73
    public function initialize(array $config)
74
    {
75
        $this->belongsTo('ContentTypes', [
76
            'className' => 'Content.ContentTypes',
77
            'propertyName' => 'content_type',
78
            'fields' => ['slug', 'name', 'description'],
79
            'conditions' => ['Contents.content_type_slug = ContentTypes.slug']
80
        ]);
81
82
        $this->belongsTo('TranslationOf', [
83
            'className' => 'Content\Model\Table\ContentsTable',
84
            'foreignKey' => 'translation_for',
85
            'propertyName' => 'translation_of',
86
            'fields' => ['slug', 'title', 'description'],
87
        ]);
88
89
        $this->belongsToMany('Roles', [
90
            'className' => 'User.Roles',
91
            'propertyName' => 'roles',
92
            'through' => 'Content.ContentsRoles',
93
        ]);
94
95
        $this->hasMany('ContentRevisions', [
96
            'className' => 'Content.ContentRevisions',
97
            'dependent' => true,
98
        ]);
99
100
        $this->hasMany('Translations', [
101
            'className' => 'Content\Model\Table\ContentsTable',
102
            'foreignKey' => 'translation_for',
103
            'dependent' => true,
104
        ]);
105
106
        $this->belongsTo('Author', [
107
            'className' => 'User.Users',
108
            'foreignKey' => 'created_by',
109
            'fields' => ['id', 'name', 'username']
110
        ]);
111
112
        $this->addBehavior('Timestamp');
113
        $this->addBehavior('Comment.Commentable');
114
        $this->addBehavior('Sluggable');
115
        $this->addBehavior('User.WhoDidIt', [
116
            'idCallable' => function () {
117
                return user()->get('id');
118
            }
119
        ]);
120
        $this->addBehavior('Field.Fieldable', [
121
            'bundle' => function ($entity) {
122
                if ($entity->has('content_type_slug')) {
123
                    return $entity->content_type_slug;
124
                }
125
126
                if ($entity->has('id')) {
127
                    return $this
128
                        ->get($entity->id, [
129
                            'fields' => ['id', 'content_type_slug'],
130
                            'fieldable' => false,
131
                        ])
132
                        ->content_type_slug;
133
                }
134
135
                return '';
136
            }
137
        ]);
138
139
        $this->addBehavior('Search.Searchable', [
140
            'fields' => function ($content) {
141
                $words = '';
142
                if ($content->has('title')) {
143
                    $words .= " {$content->title}";
144
                }
145
146
                if ($content->has('description')) {
147
                    $words .= " {$content->description}";
148
                }
149
150
                if (!$content->has('_fields')) {
151
                    return $words;
152
                }
153
154
                foreach ($content->_fields as $virtualField) {
155
                    $words .= " {$virtualField}";
156
                }
157
158
                return $words;
159
            }
160
        ]);
161
162
        $this->addSearchOperator('promote', 'operatorPromote');
163
        $this->addSearchOperator('author', 'operatorAuthor');
164
        $this->addSearchOperator('limit', 'Search.Limit');
165
        $this->addSearchOperator('modified', 'Search.Date', ['field' => 'modified']);
166
        $this->addSearchOperator('created', 'Search.Date', ['field' => 'created']);
167
        $this->addSearchOperator('type', 'Search.Generic', ['field' => 'content_type_slug', 'conjunction' => 'auto']);
168
        $this->addSearchOperator('term', 'operatorTerm');
169
        $this->addSearchOperator('language', 'Search.Generic', ['field' => 'language', 'conjunction' => 'auto']);
170
        $this->addSearchOperator('order', 'Search.Order', ['fields' => ['slug', 'title', 'description', 'sticky', 'created', 'modified']]);
171
    }
172
173
    /**
174
     * Default validation rules set.
175
     *
176
     * @param \Cake\Validation\Validator $validator The validator object
177
     * @return \Cake\Validation\Validator
178
     */
179 View Code Duplication
    public function validationDefault(Validator $validator)
180
    {
181
        $validator
182
            ->add('title', [
183
                'notBlank' => [
184
                    'rule' => 'notBlank',
185
                    'message' => __d('content', 'You need to provide a title.'),
186
                ],
187
                'length' => [
188
                    'rule' => ['minLength', 3],
189
                    'message' => __d('content', 'Title need to be at least 3 characters long.'),
190
                ],
191
            ]);
192
193
        return $validator;
194
    }
195
196
    /**
197
     * This callback performs two action, saving revisions and checking publishing
198
     * constraints.
199
     *
200
     * - Saves a revision version of each content being saved if it has changed.
201
     *
202
     * - Verifies the publishing status and forces to be "false" if use has no
203
     *   publishing permissions for this content type.
204
     *
205
     * @param \Cake\Event\Event $event The event that was triggered
206
     * @param \Content\Model\Entity\Content $entity The entity being saved
207
     * @param \ArrayObject $options Array of options
208
     * @return bool True on success
209
     */
210
    public function beforeSave(Event $event, Entity $entity, ArrayObject $options = null)
211
    {
212
        $this->_saveRevision($entity);
213
        $this->_ensureStatus($entity);
214
        return true;
215
    }
216
217
    /**
218
     * Tries to create a revision for the given content.
219
     *
220
     * @param \Cake\ORM\Entity $entity The content
221
     * @return void
222
     */
223
    protected function _saveRevision(Entity $entity)
224
    {
225
        if ($entity->isNew()) {
226
            return;
227
        }
228
229
        try {
230
            $prev = TableRegistry::get('Content.Contents')->get($entity->id);
231
            $hash = $this->_calculateHash($prev);
232
            $exists = $this->ContentRevisions->exists([
233
                'ContentRevisions.content_id' => $entity->id,
234
                'ContentRevisions.hash' => $hash,
235
            ]);
236
237
            if (!$exists) {
238
                $revision = $this->ContentRevisions->newEntity([
239
                    'content_id' => $prev->id,
240
                    'summary' => $entity->get('edit_summary'),
241
                    'data' => $prev,
242
                    'hash' => $hash,
243
                ]);
244
245
                if (!$this->ContentRevisions->hasBehavior('Timestamp')) {
246
                    $this->ContentRevisions->addBehavior('Timestamp');
247
                }
248
                $this->ContentRevisions->save($revision);
249
            }
250
        } catch (\Exception $ex) {
251
            // unable to create content's review
252
        }
253
    }
254
255
    /**
256
     * Ensures that content content has the correct publishing status based in content
257
     * type restrictions.
258
     *
259
     * If it's a new content it will set the correct status. However if it's an
260
     * existing content and user has no publishing permissions this method will not
261
     * change content's status, so it will remain published if it was already
262
     * published by an administrator.
263
     *
264
     * @param \Cake\ORM\Entity $entity The content
265
     * @return void
266
     */
267
    protected function _ensureStatus(Entity $entity)
268
    {
269
        if (!$entity->has('status')) {
270
            return;
271
        }
272
273
        if (!$entity->has('content_type') &&
274
            ($entity->has('content_type_id') || $entity->has('content_type_slug'))
275
        ) {
276
            if ($entity->has('content_type_id')) {
277
                $type = $this->ContentTypes->get($entity->get('content_type_id'));
278
            } else {
279
                $type = $this->ContentTypes
280
                    ->find()
281
                    ->where(['content_type_slug' => $entity->get('content_type_id')])
282
                    ->limit(1)
283
                    ->first();
284
            }
285
        } else {
286
            $type = $entity->get('content_type');
287
        }
288
289
        if ($type && !$type->userAllowed('publish')) {
290
            if ($entity->isNew()) {
291
                $entity->set('status', false);
292
            } else {
293
                $entity->unsetProperty('status');
294
            }
295
        }
296
    }
297
298
    /**
299
     * Handles "promote" search operator.
300
     *
301
     *     promote:<true|false>
302
     *
303
     * @param \Cake\ORM\Query $query The query object
304
     * @param \Search\Parser\TokenInterface $token Operator token
305
     * @return \Cake\ORM\Query
306
     */
307
    public function operatorPromote(Query $query, TokenInterface $token)
308
    {
309
        $value = strtolower($token->value());
310
        $conjunction = $token->negated() ? '<>' : '';
311
        $conditions = [];
312
313
        if ($value === 'true') {
314
            $conditions = ["Contents.promote {$conjunction}" => 1];
315
        } elseif ($value === 'false') {
316
            $conditions = ['Contents.promote {$conjunction}' => 0];
317
        }
318
319 View Code Duplication
        if (!empty($conditions)) {
320
            if ($token->where() === 'or') {
321
                $query->orWhere($conditions);
322
            } elseif ($token->where() === 'and') {
323
                $query->andWhere($conditions);
324
            } else {
325
                $query->where($conditions);
326
            }
327
        }
328
329
        return $query;
330
    }
331
332
    /**
333
     * Handles "author" search operator.
334
     *
335
     *     author:<username1>,<username2>, ...
336
     *
337
     * @param \Cake\ORM\Query $query The query object
338
     * @param \Search\Parser\TokenInterface $token Operator token
339
     * @return \Cake\ORM\Query
340
     */
341
    public function operatorAuthor(Query $query, TokenInterface $token)
342
    {
343
        $value = explode(',', $token->value());
344
345
        if (!empty($value)) {
346
            $conjunction = $token->negated() ? 'NOT IN' : 'IN';
347
            $subQuery = TableRegistry::get('User.Users')->find()
348
                ->select(['id'])
349
                ->where(["Users.username {$conjunction}" => $value]);
350
351
            if ($token->where() === 'or') {
352
                $query->orWhere(['Contents.created_by IN' => $subQuery]);
353
            } elseif ($token->where() === 'and') {
354
                $query->andWhere(['Contents.created_by IN' => $subQuery]);
355
            } else {
356
                $query->where(['Contents.created_by IN' => $subQuery]);
357
            }
358
        }
359
360
        return $query;
361
    }
362
363
    /**
364
     * Handles "term" search operator.
365
     *
366
     *     term:term1-slug,term2-slug,...
367
     *
368
     * @param \Cake\ORM\Query $query The query object
369
     * @param \Search\Parser\TokenInterface $token Operator token
370
     * @return \Cake\ORM\Query
371
     */
372
    public function operatorterm(Query $query, TokenInterface $token)
373
    {
374
        $terms = explode(',', strtolower($token->value()));
375
        $conjunction = $token->negated() ? 'NOT IN' : 'IN';
376
377
        if (empty($terms)) {
378
            return $query;
379
        }
380
381
        $conditions = [
382
            "Contents.id {$conjunction}" => TableRegistry::get('Taxonomy.EntitiesTerms')
383
                ->find()
384
                ->select(['EntitiesTerms.entity_id'])
385
                ->where(['EntitiesTerms.table_alias' => $this->alias()])
386
                ->matching('Terms', function ($q) use ($terms) {
387
                    return $q->where(['Terms.slug IN' => $terms]);
388
                })
389
        ];
390
391 View Code Duplication
        if (!empty($conditions)) {
392
            if ($token->where() === 'or') {
393
                $query->orWhere($conditions);
394
            } elseif ($token->where() === 'and') {
395
                $query->andWhere($conditions);
396
            } else {
397
                $query->where($conditions);
398
            }
399
        }
400
401
        return $query;
402
    }
403
404
    /**
405
     * Generates a unique hash for the given entity.
406
     *
407
     * Used by revision system to detect if an entity has changed or not.
408
     *
409
     * @param \Cake\Datasource\EntityInterface $entity The entity for which calculate its hash
410
     * @return string MD5 hash for this particular entity
411
     */
412
    protected function _calculateHash($entity)
413
    {
414
        $hash = [];
415
        foreach ($entity->visibleProperties() as $property) {
416
            if (strpos($property, 'created') === false &&
417
                strpos($property, 'created_by') === false &&
418
                strpos($property, 'modified') === false &&
419
                strpos($property, 'modified_by') === false
420
            ) {
421
                if ($property == '_fields') {
422
                    foreach ($entity->get('_fields') as $field) {
423
                        if ($field instanceof \Field\Model\Entity\Field) {
424
                            $hash[] = is_object($field->value) || is_array($field->value) ? md5(serialize($field->value)) : md5($field->value);
425
                        }
426
                    }
427
                } else {
428
                    $hash[] = $entity->get($property);
429
                }
430
            }
431
        }
432
433
        return md5(serialize($hash));
434
    }
435
}
436