Completed
Pull Request — 2.0 (#161)
by Christopher
03:15
created

EavBehavior   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 637
Duplicated Lines 0.78 %

Coupling/Cohesion

Components 1
Dependencies 13

Importance

Changes 17
Bugs 9 Features 0
Metric Value
c 17
b 9
f 0
dl 5
loc 637
rs 3.202
wmc 84
lcom 1
cbo 13

16 Methods

Rating   Name   Duplication   Size   Complexity  
C __construct() 0 25 7
A enableEav() 0 4 1
A disableEav() 0 4 1
B addColumn() 0 43 5
A dropColumn() 0 18 2
A listColumns() 0 16 2
C updateEavCache() 0 64 13
B beforeFind() 5 18 5
A beforeMarshal() 0 13 4
C afterSave() 0 60 9
A afterDelete() 0 19 3
D hydrateEntities() 0 38 10
C _prepareSetValues() 0 50 8
B _prepareCachedColumns() 0 17 6
A _scopeQuery() 0 11 3
B _initScopes() 0 15 5

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 EavBehavior 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 EavBehavior, 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 Eav\Model\Behavior;
13
14
use Cake\Cache\Cache;
15
use Cake\Collection\CollectionInterface;
16
use Cake\Datasource\EntityInterface;
17
use Cake\Error\FatalErrorException;
18
use Cake\Event\Event;
19
use Cake\ORM\Behavior;
20
use Cake\ORM\Entity;
21
use Cake\ORM\Query;
22
use Cake\ORM\Table;
23
use Cake\ORM\TableRegistry;
24
use Eav\Model\Behavior\EavToolbox;
25
use Eav\Model\Behavior\QueryScope\QueryScopeInterface;
26
use Eav\Model\Behavior\QueryScope\SelectScope;
27
use Eav\Model\Behavior\QueryScope\WhereScope;
28
use Eav\Model\Entity\CachedColumn;
29
use \ArrayObject;
30
31
/**
32
 * EAV Behavior.
33
 *
34
 * Allows additional columns to be added to tables without altering its physical
35
 * schema.
36
 *
37
 * ### Usage:
38
 *
39
 * ```php
40
 * $this->addBehavior('Eav.Eav');
41
 * $this->addColumn('user-age', ['type' => 'integer']);
42
 * ```
43
 *
44
 * Using virtual attributes in WHERE clauses:
45
 *
46
 * ```php
47
 * $adults = $this->Users->find()
48
 *     ->where(['user-age >' => 18])
49
 *     ->all();
50
 * ```
51
 *
52
 * ### Using EAV Cache:
53
 *
54
 * ```php
55
 * $this->addBehavior('Eav.Eav', [
56
 *     'cache' => [
57
 *         'contact_info' => ['user-name', 'user-address'],
58
 *         'eav_all' => '*',
59
 *     ],
60
 * ]);
61
 * ```
62
 *
63
 * Cache all EAV values into a real column named `eav_all`:
64
 *
65
 * ```php
66
 * $this->addBehavior('Eav.Eav', [
67
 *     'cache' => 'eav_all',
68
 * ]);
69
 * ```
70
 *
71
 * @link https://github.com/quickapps/docs/blob/2.x/en/developers/field-api.rst
72
 */
73
class EavBehavior extends Behavior
74
{
75
76
    /**
77
     * Instance of EavToolbox.
78
     *
79
     * @var \Eav\Model\Behavior\EavToolbox
80
     */
81
    protected $_toolbox = null;
82
83
    /**
84
     * Default configuration.
85
     *
86
     * @var array
87
     */
88
    protected $_defaultConfig = [
89
        'enabled' => true,
90
        'cache' => false,
91
        'queryScope' => [
92
            'Eav\\Model\\Behavior\\QueryScope\\SelectScope',
93
            'Eav\\Model\\Behavior\\QueryScope\\WhereScope',
94
            'Eav\\Model\\Behavior\\QueryScope\\OrderScope',
95
        ],
96
        'implementedMethods' => [
97
            'enableEav' => 'enableEav',
98
            'disableEav' => 'disableEav',
99
            'updateEavCache' => 'updateEavCache',
100
            'addColumn' => 'addColumn',
101
            'dropColumn' => 'dropColumn',
102
            'listColumns' => 'listColumns',
103
        ],
104
    ];
105
106
    /**
107
     * Query scopes objects to be applied indexed by unique ID.
108
     *
109
     * @var array
110
     */
111
    protected $_queryScopes = [];
112
113
    /**
114
     * Constructor.
115
     *
116
     * @param \Cake\ORM\Table $table The table this behavior is attached to
117
     * @param array $config Configuration array for this behavior
118
     */
119
    public function __construct(Table $table, array $config = [])
120
    {
121
        $config['cacheMap'] = false; // private config, prevent user modifications
122
        $this->_toolbox = new EavToolbox($table);
123
        parent::__construct($table, $config);
124
125
        if ($this->config('cache')) {
126
            $info = $this->config('cache');
127
            $holders = []; // column => [list of virtual columns]
128
129
            if (is_string($info)) {
130
                $holders[$info] = ['*'];
131
            } elseif (is_array($info)) {
132
                foreach ($info as $column => $fields) {
133
                    if (is_integer($column)) {
134
                        $holders[$fields] = ['*'];
135
                    } else {
136
                        $holders[$column] = ($fields === '*') ? ['*'] : $fields;
137
                    }
138
                }
139
            }
140
141
            $this->config('cacheMap', $holders);
142
        }
143
    }
144
145
    /**
146
     * Enables EAV behavior so virtual columns WILL be fetched from database.
147
     *
148
     * @return void
149
     */
150
    public function enableEav()
151
    {
152
        $this->config('enabled', true);
153
    }
154
155
    /**
156
     * Disables EAV behavior so virtual columns WLL NOT be fetched from database.
157
     *
158
     * @return void
159
     */
160
    public function disableEav()
161
    {
162
        $this->config('enabled', false);
163
    }
164
165
    /**
166
     * Defines a new virtual-column, or update if already defined.
167
     *
168
     * ### Usage:
169
     *
170
     * ```php
171
     * $errors = $this->Users->addColumn('user-age', [
172
     *     'type' => 'integer',
173
     *     'bundle' => 'some-bundle-name',
174
     *     'extra' => [
175
     *         'option1' => 'value1'
176
     *     ]
177
     * ], true);
178
     *
179
     * if (empty($errors)) {
180
     *     // OK
181
     * } else {
182
     *     // ERROR
183
     *     debug($errors);
184
     * }
185
     * ```
186
     *
187
     * The third argument can be set to FALSE to get a boolean response:
188
     *
189
     * ```php
190
     * $success = $this->Users->addColumn('user-age', [
191
     *     'type' => 'integer',
192
     *     'bundle' => 'some-bundle-name',
193
     *     'extra' => [
194
     *         'option1' => 'value1'
195
     *     ]
196
     * ]);
197
     *
198
     * if ($success) {
199
     *     // OK
200
     * } else {
201
     *     // ERROR
202
     * }
203
     * ```
204
     *
205
     * @param string $name Column name. e.g. `user-age`
206
     * @param array $options Column configuration options
207
     * @param bool $errors If set to true will return an array list of errors
208
     *  instead of boolean response. Defaults to TRUE
209
     * @return bool|array True on success or array of error messages, depending on
210
     *  $error argument
211
     * @throws \Cake\Error\FatalErrorException When provided column name collides
212
     *  with existing column names. And when an invalid type is provided
213
     */
214
    public function addColumn($name, array $options = [], $errors = true)
215
    {
216
        if (in_array($name, (array)$this->_table->schema()->columns())) {
217
            throw new FatalErrorException(__d('eav', 'The column name "{0}" cannot be used as it is already defined in the table "{1}"', $name, $this->_table->alias()));
218
        }
219
220
        $data = $options + [
221
            'type' => 'string',
222
            'bundle' => null,
223
            'searchable' => true,
224
        ];
225
226
        $data['type'] = $this->_toolbox->mapType($data['type']);
227
        if (!in_array($data['type'], EavToolbox::$types)) {
228
            throw new FatalErrorException(__d('eav', 'The column {0}({1}) could not be created as "{2}" is not a valid type.', $name, $data['type'], $data['type']));
229
        }
230
231
        $data['name'] = $name;
232
        $data['table_alias'] = $this->_table->table();
233
        $attr = TableRegistry::get('Eav.EavAttributes')->find()
234
            ->where([
235
                'name' => $data['name'],
236
                'table_alias' => $data['table_alias'],
237
                'bundle IS' => $data['bundle'],
238
            ])
239
            ->limit(1)
240
            ->first();
241
242
        if ($attr) {
243
            $attr = TableRegistry::get('Eav.EavAttributes')->patchEntity($attr, $data);
244
        } else {
245
            $attr = TableRegistry::get('Eav.EavAttributes')->newEntity($data);
246
        }
247
248
        $success = (bool)TableRegistry::get('Eav.EavAttributes')->save($attr);
249
        Cache::clear(false, 'eav_table_attrs');
250
251
        if ($errors) {
252
            return (array)$attr->errors();
253
        }
254
255
        return (bool)$success;
256
    }
257
258
    /**
259
     * Drops an existing column.
260
     *
261
     * @param string $name Name of the column to drop
262
     * @param string|null $bundle Removes the column within a particular bundle
263
     * @return bool True on success, false otherwise
264
     */
265
    public function dropColumn($name, $bundle = null)
266
    {
267
        $attr = TableRegistry::get('Eav.EavAttributes')->find()
268
            ->where([
269
                'name' => $name,
270
                'table_alias' => $this->_table->table(),
271
                'bundle IS' => $bundle,
272
            ])
273
            ->limit(1)
274
            ->first();
275
276
        Cache::clear(false, 'eav_table_attrs');
277
        if ($attr) {
278
            return (bool)TableRegistry::get('Eav.EavAttributes')->delete($attr);
279
        }
280
281
        return false;
282
    }
283
284
    /**
285
     * Gets a list of virtual columns attached to this table.
286
     *
287
     * @param string|null $bundle Get attributes within given bundle, or all of them
288
     *  regardless of the bundle if not provided
289
     * @return array Columns information indexed by column name
290
     */
291
    public function listColumns($bundle = null)
292
    {
293
        $columns = [];
294
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
295
            $columns[$name] = [
296
                'id' => $attr->get('id'),
297
                'bundle' => $attr->get('bundle'),
298
                'name' => $name,
299
                'type' => $attr->get('type'),
300
                'searchable ' => $attr->get('searchable'),
301
                'extra ' => $attr->get('extra'),
302
            ];
303
        }
304
305
        return $columns;
306
    }
307
308
    /**
309
     * Update EAV cache for the specified $entity.
310
     *
311
     * @return bool Success
312
     */
313
    public function updateEavCache(EntityInterface $entity)
314
    {
315
        if (!$this->config('cacheMap')) {
316
            return false;
317
        }
318
319
        $attrsById = [];
320
        foreach ($this->_toolbox->attributes() as $attr) {
321
            $attrsById[$attr['id']] = $attr;
322
        }
323
324
        if (empty($attrsById)) {
325
            return true; // nothing to cache
326
        }
327
328
        $values = [];
329
        $query = TableRegistry::get('Eav.EavValues')
330
            ->find('all')
331
            ->where([
332
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
333
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
334
            ]);
335
336
        foreach ($query as $v) {
337
            $type = $attrsById[$v->get('eav_attribute_id')]->get('type');
338
            $name = $attrsById[$v->get('eav_attribute_id')]->get('name');
339
            $values[$name] = $this->_toolbox->marshal($v->get("value_{$type}"), $type);
340
        }
341
342
        $toUpdate = [];
343
        foreach ((array)$this->config('cacheMap') as $column => $fields) {
344
            $cache = [];
345
            if (in_array('*', $fields)) {
346
                $cache = $values;
347
            } else {
348
                foreach ($fields as $field) {
349
                    if (isset($values[$field])) {
350
                        $cache[$field] = $values[$field];
351
                    }
352
                }
353
            }
354
355
            $toUpdate[$column] = (string)serialize(new CachedColumn($cache));
356
        }
357
358
        if (!empty($toUpdate)) {
359
            $conditions = []; // scope to entity's PK (composed PK supported)
360
            $keys = $this->_table->primaryKey();
361
            $keys = !is_array($keys) ? [$keys] : $keys;
362
            foreach ($keys as $key) {
363
                // TODO: check key exists in entity's visible properties list.
364
                // Throw an error otherwise as PK MUST be correctly calculated.
365
                $conditions[$key] = $entity->get($key);
366
            }
367
368
            if (empty($conditions)) {
369
                return false;
370
            }
371
372
            return (bool)$this->_table->updateAll($toUpdate, $conditions);
373
        }
374
375
        return true;
376
    }
377
378
    /**
379
     * Attaches virtual properties to entities.
380
     *
381
     * This method iterates over each retrieved entity and invokes the
382
     * `attachEntityAttributes()` method. This method should return the altered
383
     * entity object with its virtual properties, however if this method returns
384
     * NULL the entity will be removed from the resulting collection. And if this
385
     * method returns FALSE will stop the find() operation.
386
     *
387
     * This method is also responsible of looking for virtual columns in SELECT and
388
     * WHERE clauses (if applicable) and properly scope the Query object. Query
389
     * scoping is performed by the `_scopeQuery()` method.
390
     *
391
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
392
     * @param \Cake\ORM\Query $query The original query to modify
393
     * @param \ArrayObject $options Additional options given as an array
394
     * @param bool $primary Whether this find is a primary query or not
395
     * @return bool|null
396
     */
397
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
398
    {
399 View Code Duplication
        if (!$this->config('enabled') ||
400
            (isset($options['eav']) && $options['eav'] === false)
401
        ) {
402
            return true;
403
        }
404
405
        if (!isset($options['bundle'])) {
406
            $options['bundle'] = null;
407
        }
408
409
        $query = $this->_scopeQuery($query, $options['bundle']);
410
411
        return $query->formatResults(function ($results) use ($event, $query, $options, $primary) {
412
            return $this->hydrateEntities($results, compact('event', 'query', 'options', 'primary'));
413
        });
414
    }
415
416
    /**
417
     * Triggered before data is converted into entities.
418
     *
419
     * Converts incoming POST data to its corresponding types.
420
     *
421
     * @param \Cake\Event\Event $event The event that was triggered
422
     * @param \ArrayObject $data The POST data to be merged with entity
423
     * @param \ArrayObject $options The options passed to the marshaller
424
     * @return void
425
     */
426
    public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
427
    {
428
        $bundle = !empty($options['bundle']) ? $options['bundle'] : null;
429
        $attrs = array_keys($this->_toolbox->attributes($bundle));
430
        foreach ($data as $property => $value) {
431
            if (!in_array($property, $attrs)) {
432
                continue;
433
            }
434
            $dataType = $this->_toolbox->getType($property);
435
            $marshaledValue = $this->_toolbox->marshal($value, $dataType);
436
            $data[$property] = $marshaledValue;
437
        }
438
    }
439
440
    /**
441
     * After an entity is saved.
442
     *
443
     * @param \Cake\Event\Event $event The event that was triggered
444
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
445
     * @param \ArrayObject $options Additional options given as an array
446
     * @return bool True always
447
     */
448
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
449
    {
450
        $attrsById = [];
451
        $updatedAttrs = [];
452
        $valuesTable = TableRegistry::get('Eav.EavValues');
453
454
        foreach ($this->_toolbox->attributes() as $name => $attr) {
455
            if (!$this->_toolbox->propertyExists($entity, $name)) {
0 ignored issues
show
Compatibility introduced by
$entity of type object<Cake\Datasource\EntityInterface> is not a sub-type of object<Cake\ORM\Entity>. It seems like you assume a concrete implementation of the interface Cake\Datasource\EntityInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
456
                continue;
457
            }
458
            $attrsById[$attr->get('id')] = $attr;
459
        }
460
461
        if (empty($attrsById)) {
462
            return true; // nothing to do
463
        }
464
465
        $values = $valuesTable
466
            ->find()
467
            ->where([
468
                'eav_attribute_id IN' => array_keys($attrsById),
469
                'entity_id' => $this->_toolbox->getEntityId($entity),
470
            ]);
471
472
        foreach ($values as $value) {
473
            $updatedAttrs[] = $value->get('eav_attribute_id');
474
            $info = $attrsById[$value->get('eav_attribute_id')];
475
            $type = $this->_toolbox->getType($info->get('name'));
476
477
            $marshaledValue = $this->_toolbox->marshal($entity->get($info->get('name')), $type);
478
            $value->set("value_{$type}", $marshaledValue);
479
            $entity->set($info->get('name'), $marshaledValue);
480
            $valuesTable->save($value);
481
        }
482
483
        foreach ($this->_toolbox->attributes() as $name => $attr) {
484
            if (!$this->_toolbox->propertyExists($entity, $name)) {
0 ignored issues
show
Compatibility introduced by
$entity of type object<Cake\Datasource\EntityInterface> is not a sub-type of object<Cake\ORM\Entity>. It seems like you assume a concrete implementation of the interface Cake\Datasource\EntityInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
485
                continue;
486
            }
487
488
            if (!in_array($attr->get('id'), $updatedAttrs)) {
489
                $type = $this->_toolbox->getType($name);
490
                $value = $valuesTable->newEntity([
491
                    'eav_attribute_id' => $attr->get('id'),
492
                    'entity_id' => $this->_toolbox->getEntityId($entity),
493
                ]);
494
495
                $marshaledValue = $this->_toolbox->marshal($entity->get($name), $type);
496
                $value->set("value_{$type}", $marshaledValue);
497
                $entity->set($name, $marshaledValue);
498
                $valuesTable->save($value);
499
            }
500
        }
501
502
        if ($this->config('cacheMap')) {
503
            $this->updateEavCache($entity);
504
        }
505
506
        return true;
507
    }
508
509
    /**
510
     * After an entity was removed from database. Here is when EAV values are
511
     * removed from DB.
512
     *
513
     * @param \Cake\Event\Event $event The event that was triggered
514
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
515
     * @param \ArrayObject $options Additional options given as an array
516
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
517
     * @return void
518
     */
519
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
520
    {
521
        if (!$options['atomic']) {
522
            throw new FatalErrorException(__d('eav', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
523
        }
524
525
        $valuesToDelete = TableRegistry::get('Eav.EavValues')
526
            ->find()
527
            ->contain(['EavAttribute'])
528
            ->where([
529
                'EavAttribute.table_alias' => $this->_table->table(),
530
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
531
            ])
532
            ->all();
533
534
        foreach ($valuesToDelete as $value) {
535
            TableRegistry::get('Eav.EavValues')->delete($value);
536
        }
537
    }
538
539
    /**
540
     * Attach EAV attributes for every entity in the provided result-set.
541
     *
542
     * @param \Cake\Collection\CollectionInterface $entities Set of entities to be
543
     *  processed
544
     * @param array $options Arguments given to `beforeFind()` method, possible keys
545
     *  are "event", "query", "options", "primary"
546
     * @return \Cake\Collection\CollectionInterface New set with altered entities
547
     */
548
    public function hydrateEntities(CollectionInterface $entities, array $options)
549
    {
550
        $values = $this->_prepareSetValues($entities, $options);
551
552
        return $entities->map(function ($entity) use ($values, $options) {
553
            if ($entity instanceof EntityInterface) {
554
                $entity = $this->_prepareCachedColumns($entity);
555
                $entityId = $this->_toolbox->getEntityId($entity);
556
557
                if (isset($values[$entityId])) {
558
                    $valuesForEntity = isset($values[$entityId]) ? $values[$entityId] : [];
559
                    $this->attachEntityAttributes($entity, $valuesForEntity);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class Eav\Model\Behavior\EavBehavior as the method attachEntityAttributes() does only exist in the following sub-classes of Eav\Model\Behavior\EavBehavior: Field\Model\Behavior\FieldableBehavior. 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...
560
                }
561
562
                // force cache-columns to be of the proper type as they might be NULL if
563
                // entity has not been updated yet.
564
                if ($this->config('cacheMap')) {
565
                    foreach ($this->config('cacheMap') as $column => $fields) {
566
                        if ($this->_toolbox->propertyExists($entity, $column) && !($entity->get($column) instanceof Entity)) {
0 ignored issues
show
Compatibility introduced by
$entity of type object<Cake\Datasource\EntityInterface> is not a sub-type of object<Cake\ORM\Entity>. It seems like you assume a concrete implementation of the interface Cake\Datasource\EntityInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
567
                            $entity->set($column, new Entity);
568
                        }
569
                    }
570
                }
571
            }
572
573
            if ($entity === false) {
574
                $options['event']->stopPropagation();
575
576
                return;
577
            }
578
579
            if ($entity === null) {
580
                return false;
581
            }
582
583
            return $entity;
584
        });
585
    }
586
587
    /**
588
     * Retrieves all virtual values of all the entities within the given result-set.
589
     *
590
     * @param \Cake\Collection\CollectionInterface $entities Set of entities
591
     * @param array $options Arguments given to `beforeFind()` method, possible keys
592
     *  are "event", "query", "options", "primary"
593
     * @return array Virtual values indexed by entity ID
594
     */
595
    protected function _prepareSetValues(CollectionInterface $entities, array $options)
596
    {
597
        $entityIds = $this->_toolbox->extractEntityIds($entities);
0 ignored issues
show
Compatibility introduced by
$entities of type object<Cake\Collection\CollectionInterface> is not a sub-type of object<Cake\ORM\ResultSet>. It seems like you assume a concrete implementation of the interface Cake\Collection\CollectionInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
598
        if (empty($entityIds)) {
599
            return [];
600
        }
601
602
        if (empty($this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope'])) {
603
            return [];
604
        }
605
606
        $bundle = !empty($options['bundle']) ? $options['bundle'] : null;
607
        $selectedVirtual = $this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope']->getVirtualColumns($options['query'], $bundle);
608
        $validColumns = array_values($selectedVirtual);
609
        $validNames = array_intersect($this->_toolbox->getAttributeNames($bundle), $validColumns);
610
        $attrsById = [];
611
612
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
613
            if (in_array($name, $validNames)) {
614
                $attrsById[$attr['id']] = $attr;
615
            }
616
        }
617
618
        if (empty($attrsById)) {
619
            return [];
620
        }
621
622
        return TableRegistry::get('Eav.EavValues')
623
            ->find('all')
624
            ->where([
625
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
626
                'EavValues.entity_id IN' => $entityIds,
627
            ])
628
            ->all()
629
            ->map(function ($value) use ($attrsById, $selectedVirtual) {
630
                $attrName = $attrsById[$value->get('eav_attribute_id')]->get('name');
0 ignored issues
show
Unused Code introduced by
$attrName is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
631
                $attrType = $attrsById[$value->get('eav_attribute_id')]->get('type');
0 ignored issues
show
Unused Code introduced by
$attrType is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
632
                $alias = array_search($name, $selectedVirtual);
0 ignored issues
show
Bug introduced by
The variable $name does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
633
634
                $data = [
0 ignored issues
show
Unused Code introduced by
$data is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
635
                    'property_name' => is_string($alias) ? $alias : $name,
636
                    'value' => $value->get("value_{$type}"),
0 ignored issues
show
Bug introduced by
The variable $type does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
637
                    ':metadata:' => $value
638
                ];
639
640
                return $value;
641
            })
642
            ->groupBy('entity_id')
643
            ->toArray();
644
    }
645
646
    /**
647
     * Prepares entity's cache-columns (those defined using `cache` option).
648
     *
649
     * @param \Cake\Datasource\EntityInterface $entity The entity to prepare
650
     * @return \Cake\Datasource\EntityInterfa Modified entity
651
     */
652
    protected function _prepareCachedColumns(EntityInterface $entity)
653
    {
654
        if ($this->config('cacheMap')) {
655
            foreach ((array)$this->config('cacheMap') as $column => $fields) {
656
                if (in_array($column, $entity->visibleProperties())) {
657
                    $string = $entity->get($column);
658
                    if ($string == serialize(false) || @unserialize($string) !== false) {
659
                        $entity->set($column, unserialize($string));
660
                    } else {
661
                        $entity->set($column, new CachedColumn());
662
                    }
663
                }
664
            }
665
        }
666
667
        return $entity;
668
    }
669
670
    /**
671
     * Look for virtual columns in some query's clauses.
672
     *
673
     * @param \Cake\ORM\Query $query The query to scope
674
     * @param string|null $bundle Consider attributes only for a specific bundle
675
     * @return \Cake\ORM\Query The modified query object
676
     */
677
    protected function _scopeQuery(Query $query, $bundle = null)
678
    {
679
        $this->_initScopes();
680
        foreach ($this->_queryScopes as $scope) {
681
            if ($scope instanceof QueryScopeInterface) {
682
                $query = $scope->scope($query, $bundle);
683
            }
684
        }
685
686
        return $query;
687
    }
688
689
    /**
690
     * Initializes the scope objects
691
     *
692
     * @return void
693
     */
694
    protected function _initScopes()
695
    {
696
        foreach ((array)$this->config('queryScope') as $className) {
697
            if (!empty($this->_queryScopes[$className])) {
698
                continue;
699
            }
700
701
            if (class_exists($className)) {
702
                $instance = new $className($this->_table);
703
                if ($instance instanceof QueryScopeInterface) {
704
                    $this->_queryScopes[$className] = $instance;
705
                }
706
            }
707
        }
708
    }
709
}
710