Completed
Pull Request — master (#14010)
by
unknown
03:34
created

CounterCacheBehavior   A

Complexity

Total Complexity 34

Size/Duplication

Total Lines 188
Duplicated Lines 17.55 %

Coupling/Cohesion

Components 1
Dependencies 5

Importance

Changes 0
Metric Value
dl 33
loc 188
rs 9.68
c 0
b 0
f 0
wmc 34
lcom 1
cbo 5

6 Methods

Rating   Name   Duplication   Size   Complexity  
A afterSave() 9 9 3
A afterDelete() 8 8 3
A _processAssociations() 0 7 2
B beforeSave() 0 27 10
C _processAssociation() 16 52 13
A _getCount() 0 16 3

How to fix   Duplicated Code   

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:

1
<?php
2
/**
3
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
4
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
5
 *
6
 * Licensed under The MIT License
7
 * For full copyright and license information, please see the LICENSE.txt
8
 * Redistributions of files must retain the above copyright notice.
9
 *
10
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
11
 * @link          https://cakephp.org CakePHP(tm) Project
12
 * @since         3.0.0
13
 * @license       https://opensource.org/licenses/mit-license.php MIT License
14
 */
15
namespace Cake\ORM\Behavior;
16
17
use Cake\Datasource\EntityInterface;
18
use Cake\Event\Event;
19
use Cake\ORM\Association;
20
use Cake\ORM\Behavior;
21
use RuntimeException;
22
23
/**
24
 * CounterCache behavior
25
 *
26
 * Enables models to cache the amount of connections in a given relation.
27
 *
28
 * Examples with Post model belonging to User model
29
 *
30
 * Regular counter cache
31
 * ```
32
 * [
33
 *     'Users' => [
34
 *         'post_count'
35
 *     ]
36
 * ]
37
 * ```
38
 *
39
 * Counter cache with scope
40
 * ```
41
 * [
42
 *     'Users' => [
43
 *         'posts_published' => [
44
 *             'conditions' => [
45
 *                 'published' => true
46
 *             ]
47
 *         ]
48
 *     ]
49
 * ]
50
 * ```
51
 *
52
 * Counter cache using custom find
53
 * ```
54
 * [
55
 *     'Users' => [
56
 *         'posts_published' => [
57
 *             'finder' => 'published' // Will be using findPublished()
58
 *         ]
59
 *     ]
60
 * ]
61
 * ```
62
 *
63
 * Counter cache using lambda function returning the count
64
 * This is equivalent to example #2
65
 *
66
 * ```
67
 * [
68
 *     'Users' => [
69
 *         'posts_published' => function (Event $event, EntityInterface $entity, Table $table) {
70
 *             $query = $table->find('all')->where([
71
 *                 'published' => true,
72
 *                 'user_id' => $entity->get('user_id')
73
 *             ]);
74
 *             return $query->count();
75
 *          }
76
 *     ]
77
 * ]
78
 * ```
79
 *
80
 * When using a lambda function you can return `false` to disable updating the counter value
81
 * for the current operation.
82
 *
83
 * Ignore updating the field if it is dirty
84
 * ```
85
 * [
86
 *     'Users' => [
87
 *         'posts_published' => [
88
 *             'ignoreDirty' => true
89
 *         ]
90
 *     ]
91
 * ]
92
 * ```
93
 *
94
 * You can disable counter updates entirely by sending the `ignoreCounterCache` option
95
 * to your save operation:
96
 *
97
 * ```
98
 * $this->Articles->save($article, ['ignoreCounterCache' => true]);
99
 * ```
100
 */
101
class CounterCacheBehavior extends Behavior
102
{
103
    /**
104
     * Store the fields which should be ignored
105
     *
106
     * @var array
107
     */
108
    protected $_ignoreDirty = [];
109
110
    /**
111
     * beforeSave callback.
112
     *
113
     * Check if a field, which should be ignored, is dirty
114
     *
115
     * @param \Cake\Event\Event $event The beforeSave event that was fired
116
     * @param \Cake\Datasource\EntityInterface $entity The entity that is going to be saved
117
     * @param \ArrayObject $options The options for the query
118
     * @return void
119
     */
120
    public function beforeSave(Event $event, EntityInterface $entity, $options)
121
    {
122
        if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
123
            return;
124
        }
125
126
        foreach ($this->_config as $assoc => $settings) {
127
            $assoc = $this->_table->getAssociation($assoc);
128
            foreach ($settings as $field => $config) {
129
                if (is_int($field)) {
130
                    continue;
131
                }
132
133
                $registryAlias = $assoc->getTarget()->getRegistryAlias();
134
                $entityAlias = $assoc->getProperty();
135
136
                if (
137
                    !is_callable($config) &&
138
                    isset($config['ignoreDirty']) &&
139
                    $config['ignoreDirty'] === true &&
140
                    $entity->$entityAlias->isDirty($field)
141
                ) {
142
                    $this->_ignoreDirty[$registryAlias][$field] = true;
143
                }
144
            }
145
        }
146
    }
147
148
    /**
149
     * afterSave callback.
150
     *
151
     * Makes sure to update counter cache when a new record is created or updated.
152
     *
153
     * @param \Cake\Event\Event $event The afterSave event that was fired.
154
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved.
155
     * @param \ArrayObject $options The options for the query
156
     * @return void
157
     */
158 View Code Duplication
    public function afterSave(Event $event, EntityInterface $entity, $options)
159
    {
160
        if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
161
            return;
162
        }
163
164
        $this->_processAssociations($event, $entity);
165
        $this->_ignoreDirty = [];
166
    }
167
168
    /**
169
     * afterDelete callback.
170
     *
171
     * Makes sure to update counter cache when a record is deleted.
172
     *
173
     * @param \Cake\Event\Event $event The afterDelete event that was fired.
174
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted.
175
     * @param \ArrayObject $options The options for the query
176
     * @return void
177
     */
178 View Code Duplication
    public function afterDelete(Event $event, EntityInterface $entity, $options)
179
    {
180
        if (isset($options['ignoreCounterCache']) && $options['ignoreCounterCache'] === true) {
181
            return;
182
        }
183
184
        $this->_processAssociations($event, $entity);
185
    }
186
187
    /**
188
     * Iterate all associations and update counter caches.
189
     *
190
     * @param \Cake\Event\Event $event Event instance.
191
     * @param \Cake\Datasource\EntityInterface $entity Entity.
192
     * @return void
193
     */
194
    protected function _processAssociations(Event $event, EntityInterface $entity)
195
    {
196
        foreach ($this->_config as $assoc => $settings) {
197
            $assoc = $this->_table->getAssociation($assoc);
198
            $this->_processAssociation($event, $entity, $assoc, $settings);
199
        }
200
    }
201
202
    /**
203
     * Updates counter cache for a single association
204
     *
205
     * @param \Cake\Event\Event $event Event instance.
206
     * @param \Cake\Datasource\EntityInterface $entity Entity
207
     * @param \Cake\ORM\Association $assoc The association object
208
     * @param array $settings The settings for for counter cache for this association
209
     * @return void
210
     * @throws \RuntimeException If invalid callable is passed.
211
     */
212
    protected function _processAssociation(Event $event, EntityInterface $entity, Association $assoc, array $settings)
213
    {
214
        $foreignKeys = (array)$assoc->getForeignKey();
215
        $primaryKeys = (array)$assoc->getBindingKey();
216
        $countConditions = $entity->extract($foreignKeys);
217
        $updateConditions = array_combine($primaryKeys, $countConditions);
218
        $countOriginalConditions = $entity->extractOriginalChanged($foreignKeys);
0 ignored issues
show
Bug introduced by
The method extractOriginalChanged() does not exist on Cake\Datasource\EntityInterface. Did you maybe mean extract()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
219
220
        if ($countOriginalConditions !== []) {
221
            $updateOriginalConditions = array_combine($primaryKeys, $countOriginalConditions);
222
        }
223
224
        foreach ($settings as $field => $config) {
225
            if (is_int($field)) {
226
                $field = $config;
227
                $config = [];
228
            }
229
230
            if (
231
                isset($this->_ignoreDirty[$assoc->getTarget()->getRegistryAlias()][$field]) &&
232
                $this->_ignoreDirty[$assoc->getTarget()->getRegistryAlias()][$field] === true
233
            ) {
234
                continue;
235
            }
236
237 View Code Duplication
            if (is_callable($config)) {
238
                if (is_string($config)) {
239
                    throw new RuntimeException('You must not use a string as callable.');
240
                }
241
                $count = $config($event, $entity, $this->_table, false);
242
            } else {
243
                $count = $this->_getCount($config, $countConditions);
244
            }
245
            if ($count !== false) {
246
                $assoc->getTarget()->updateAll([$field => $count], $updateConditions);
247
            }
248
249
            if (isset($updateOriginalConditions)) {
250 View Code Duplication
                if (is_callable($config)) {
251
                    if (is_string($config)) {
252
                        throw new RuntimeException('You must not use a string as callable.');
253
                    }
254
                    $count = $config($event, $entity, $this->_table, true);
255
                } else {
256
                    $count = $this->_getCount($config, $countOriginalConditions);
257
                }
258
                if ($count !== false) {
259
                    $assoc->getTarget()->updateAll([$field => $count], $updateOriginalConditions);
260
                }
261
            }
262
        }
263
    }
264
265
    /**
266
     * Fetches and returns the count for a single field in an association
267
     *
268
     * @param array $config The counter cache configuration for a single field
269
     * @param array $conditions Additional conditions given to the query
270
     * @return int The number of relations matching the given config and conditions
271
     */
272
    protected function _getCount(array $config, array $conditions)
273
    {
274
        $finder = 'all';
275
        if (!empty($config['finder'])) {
276
            $finder = $config['finder'];
277
            unset($config['finder']);
278
        }
279
280
        if (!isset($config['conditions'])) {
281
            $config['conditions'] = [];
282
        }
283
        $config['conditions'] = array_merge($conditions, $config['conditions']);
284
        $query = $this->_table->find($finder, $config);
285
286
        return $query->count();
287
    }
288
}
289