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

EavBehavior::hydrateEntities()   D

Complexity

Conditions 10
Paths 1

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 19
c 1
b 0
f 0
nc 1
nop 2
dl 0
loc 36
rs 4.8196

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
/**
3
 * 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
        return $columns;
305
    }
306
307
    /**
308
     * Update EAV cache for the specified $entity.
309
     *
310
     * @return bool Success
311
     */
312
    public function updateEavCache(EntityInterface $entity)
313
    {
314
        if (!$this->config('cacheMap')) {
315
            return false;
316
        }
317
318
        $attrsById = [];
319
        foreach ($this->_toolbox->attributes() as $attr) {
320
            $attrsById[$attr['id']] = $attr;
321
        }
322
323
        if (empty($attrsById)) {
324
            return true; // nothing to cache
325
        }
326
327
        $values = [];
328
        $query = TableRegistry::get('Eav.EavValues')
329
            ->find('all')
330
            ->where([
331
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
332
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
333
            ]);
334
335
        foreach ($query as $v) {
336
            $type = $attrsById[$v->get('eav_attribute_id')]->get('type');
337
            $name = $attrsById[$v->get('eav_attribute_id')]->get('name');
338
            $values[$name] = $this->_toolbox->marshal($v->get("value_{$type}"), $type);
339
        }
340
341
        $toUpdate = [];
342
        foreach ((array)$this->config('cacheMap') as $column => $fields) {
343
            $cache = [];
344
            if (in_array('*', $fields)) {
345
                $cache = $values;
346
            } else {
347
                foreach ($fields as $field) {
348
                    if (isset($values[$field])) {
349
                        $cache[$field] = $values[$field];
350
                    }
351
                }
352
            }
353
354
            $toUpdate[$column] = (string)serialize(new CachedColumn($cache));
355
        }
356
357
        if (!empty($toUpdate)) {
358
            $conditions = []; // scope to entity's PK (composed PK supported)
359
            $keys = $this->_table->primaryKey();
360
            $keys = !is_array($keys) ? [$keys] : $keys;
361
            foreach ($keys as $key) {
362
                // TODO: check key exists in entity's visible properties list.
363
                // Throw an error otherwise as PK MUST be correctly calculated.
364
                $conditions[$key] = $entity->get($key);
365
            }
366
367
            if (empty($conditions)) {
368
                return false;
369
            }
370
371
            return (bool)$this->_table->updateAll($toUpdate, $conditions);
372
        }
373
374
        return true;
375
    }
376
377
    /**
378
     * Attaches virtual properties to entities.
379
     *
380
     * This method iterates over each retrieved entity and invokes the
381
     * `attachEntityAttributes()` method. This method should return the altered
382
     * entity object with its virtual properties, however if this method returns
383
     * NULL the entity will be removed from the resulting collection. And if this
384
     * method returns FALSE will stop the find() operation.
385
     *
386
     * This method is also responsible of looking for virtual columns in SELECT and
387
     * WHERE clauses (if applicable) and properly scope the Query object. Query
388
     * scoping is performed by the `_scopeQuery()` method.
389
     *
390
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
391
     * @param \Cake\ORM\Query $query The original query to modify
392
     * @param \ArrayObject $options Additional options given as an array
393
     * @param bool $primary Whether this find is a primary query or not
394
     * @return bool|null
395
     */
396
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
397
    {
398 View Code Duplication
        if (!$this->config('enabled') ||
399
            (isset($options['eav']) && $options['eav'] === false)
400
        ) {
401
            return true;
402
        }
403
404
        if (!isset($options['bundle'])) {
405
            $options['bundle'] = null;
406
        }
407
408
        $query = $this->_scopeQuery($query, $options['bundle']);
409
        return $query->formatResults(function ($results) use ($event, $query, $options, $primary) {
410
            return $this->hydrateEntities($results, compact('event', 'query', 'options', 'primary'));
411
        });
412
    }
413
414
    /**
415
     * Triggered before data is converted into entities.
416
     *
417
     * Converts incoming POST data to its corresponding types.
418
     *
419
     * @param \Cake\Event\Event $event The event that was triggered
420
     * @param \ArrayObject $data The POST data to be merged with entity
421
     * @param \ArrayObject $options The options passed to the marshaller
422
     * @return void
423
     */
424
    public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
425
    {
426
        $bundle = !empty($options['bundle']) ? $options['bundle'] : null;
427
        $attrs = array_keys($this->_toolbox->attributes($bundle));
428
        foreach ($data as $property => $value) {
429
            if (!in_array($property, $attrs)) {
430
                continue;
431
            }
432
            $dataType = $this->_toolbox->getType($property);
433
            $marshaledValue = $this->_toolbox->marshal($value, $dataType);
434
            $data[$property] = $marshaledValue;
435
        }
436
    }
437
438
    /**
439
     * After an entity is saved.
440
     *
441
     * @param \Cake\Event\Event $event The event that was triggered
442
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
443
     * @param \ArrayObject $options Additional options given as an array
444
     * @return bool True always
445
     */
446
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
447
    {
448
        $attrsById = [];
449
        $updatedAttrs = [];
450
        $valuesTable = TableRegistry::get('Eav.EavValues');
451
452
        foreach ($this->_toolbox->attributes() as $name => $attr) {
453
            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...
454
                continue;
455
            }
456
            $attrsById[$attr->get('id')] = $attr;
457
        }
458
459
        if (empty($attrsById)) {
460
            return true; // nothing to do
461
        }
462
463
        $values = $valuesTable
464
            ->find()
465
            ->where([
466
                'eav_attribute_id IN' => array_keys($attrsById),
467
                'entity_id' => $this->_toolbox->getEntityId($entity),
468
            ]);
469
470
        foreach ($values as $value) {
471
            $updatedAttrs[] = $value->get('eav_attribute_id');
472
            $info = $attrsById[$value->get('eav_attribute_id')];
473
            $type = $this->_toolbox->getType($info->get('name'));
474
475
            $marshaledValue = $this->_toolbox->marshal($entity->get($info->get('name')), $type);
476
            $value->set("value_{$type}", $marshaledValue);
477
            $entity->set($info->get('name'), $marshaledValue);
478
            $valuesTable->save($value);
479
        }
480
481
        foreach ($this->_toolbox->attributes() as $name => $attr) {
482
            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...
483
                continue;
484
            }
485
486
            if (!in_array($attr->get('id'), $updatedAttrs)) {
487
                $type = $this->_toolbox->getType($name);
488
                $value = $valuesTable->newEntity([
489
                    'eav_attribute_id' => $attr->get('id'),
490
                    'entity_id' => $this->_toolbox->getEntityId($entity),
491
                ]);
492
493
                $marshaledValue = $this->_toolbox->marshal($entity->get($name), $type);
494
                $value->set("value_{$type}", $marshaledValue);
495
                $entity->set($name, $marshaledValue);
496
                $valuesTable->save($value);
497
            }
498
        }
499
500
        if ($this->config('cacheMap')) {
501
            $this->updateEavCache($entity);
502
        }
503
504
        return true;
505
    }
506
507
    /**
508
     * After an entity was removed from database. Here is when EAV values are
509
     * removed from DB.
510
     *
511
     * @param \Cake\Event\Event $event The event that was triggered
512
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
513
     * @param \ArrayObject $options Additional options given as an array
514
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
515
     * @return void
516
     */
517
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
518
    {
519
        if (!$options['atomic']) {
520
            throw new FatalErrorException(__d('eav', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
521
        }
522
523
        $valuesToDelete = TableRegistry::get('Eav.EavValues')
524
            ->find()
525
            ->contain(['EavAttribute'])
526
            ->where([
527
                'EavAttribute.table_alias' => $this->_table->table(),
528
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
529
            ])
530
            ->all();
531
532
        foreach ($valuesToDelete as $value) {
533
            TableRegistry::get('Eav.EavValues')->delete($value);
534
        }
535
    }
536
537
    /**
538
     * Attach EAV attributes for every entity in the provided result-set.
539
     *
540
     * @param \Cake\Collection\CollectionInterface $entities Set of entities to be
541
     *  processed
542
     * @param array $options Arguments given to `beforeFind()` method, possible keys
543
     *  are "event", "query", "options", "primary"
544
     * @return \Cake\Collection\CollectionInterface New set with altered entities
545
     */
546
    public function hydrateEntities(CollectionInterface $entities, array $options)
547
    {
548
        $values = $this->_prepareSetValues($entities, $options);
549
        return $entities->map(function ($entity) use($values, $options) {
550
            if ($entity instanceof EntityInterface) {
551
                $entity = $this->_prepareCachedColumns($entity);
552
                $entityId = $this->_toolbox->getEntityId($entity);
553
554
                if (isset($values[$entityId])) {
555
                    $valuesForEntity = isset($values[$entityId]) ? $values[$entityId] : [];
556
                    $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...
557
                }
558
559
                // force cache-columns to be of the proper type as they might be NULL if
560
                // entity has not been updated yet.
561
                if ($this->config('cacheMap')) {
562
                    foreach ($this->config('cacheMap') as $column => $fields) {
563
                        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...
564
                            $entity->set($column, new Entity);
565
                        }
566
                    }
567
                }
568
            }
569
570
            if ($entity === false) {
571
                $options['event']->stopPropagation();
572
                return;
573
            }
574
575
            if ($entity === null) {
576
                return false;
577
            }
578
579
            return $entity;
580
        });
581
    }
582
583
    /**
584
     * Retrieves all virtual values of all the entities within the given result-set.
585
     *
586
     * @param \Cake\Collection\CollectionInterface $entities Set of entities
587
     * @param array $options Arguments given to `beforeFind()` method, possible keys
588
     *  are "event", "query", "options", "primary"
589
     * @return array Virtual values indexed by entity ID
590
     */
591
    protected function _prepareSetValues(CollectionInterface $entities, array $options)
592
    {
593
        $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...
594
        if (empty($entityIds)) {
595
            return [];
596
        }
597
598
        if (empty($this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope'])) {
599
            return [];
600
        }
601
602
        $bundle = !empty($options['bundle']) ? $options['bundle'] : null;
603
        $selectedVirtual = $this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope']->getVirtualColumns($options['query'], $bundle);
604
        $validColumns = array_values($selectedVirtual);
605
        $validNames = array_intersect($this->_toolbox->getAttributeNames($bundle), $validColumns);
606
        $attrsById = [];
607
608
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
609
            if (in_array($name, $validNames)) {
610
                $attrsById[$attr['id']] = $attr;
611
            }
612
        }
613
614
        if (empty($attrsById)) {
615
            return [];
616
        }
617
618
        return TableRegistry::get('Eav.EavValues')
619
            ->find('all')
620
            ->where([
621
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
622
                'EavValues.entity_id IN' => $entityIds,
623
            ])
624
            ->all()
625
            ->map(function ($value) use($attrsById, $selectedVirtual) {
626
                $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...
627
                $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...
628
                $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...
629
630
                $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...
631
                    'property_name' => is_string($alias) ? $alias : $name,
632
                    '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...
633
                    ':metadata:' => $value
634
                ];
635
636
                return $value;
637
            })
638
            ->groupBy('entity_id')
639
            ->toArray();
640
    }
641
642
    /**
643
     * Prepares entity's cache-columns (those defined using `cache` option).
644
     *
645
     * @param \Cake\Datasource\EntityInterface $entity The entity to prepare
646
     * @return \Cake\Datasource\EntityInterfa Modified entity
647
     */
648
    protected function _prepareCachedColumns(EntityInterface $entity)
649
    {
650
        if ($this->config('cacheMap')) {
651
            foreach ((array)$this->config('cacheMap') as $column => $fields) {
652
                if (in_array($column, $entity->visibleProperties())) {
653
                    $string = $entity->get($column);
654
                    if ($string == serialize(false) || @unserialize($string) !== false) {
655
                        $entity->set($column, unserialize($string));
656
                    } else {
657
                        $entity->set($column, new CachedColumn());
658
                    }
659
                }
660
            }
661
        }
662
663
        return $entity;
664
    }
665
666
    /**
667
     * Look for virtual columns in some query's clauses.
668
     *
669
     * @param \Cake\ORM\Query $query The query to scope
670
     * @param string|null $bundle Consider attributes only for a specific bundle
671
     * @return \Cake\ORM\Query The modified query object
672
     */
673
    protected function _scopeQuery(Query $query, $bundle = null)
674
    {
675
        $this->_initScopes();
676
        foreach ($this->_queryScopes as $scope) {
677
            if ($scope instanceof QueryScopeInterface) {
678
                $query = $scope->scope($query, $bundle);
679
            }
680
        }
681
        return $query;
682
    }
683
684
    /**
685
     * Initializes the scope objects
686
     *
687
     * @return void
688
     */
689
    protected function _initScopes()
690
    {
691
        foreach ((array)$this->config('queryScope') as $className) {
692
            if (!empty($this->_queryScopes[$className])) {
693
                continue;
694
            }
695
696
            if (class_exists($className)) {
697
                $instance = new $className($this->_table);
698
                if ($instance instanceof QueryScopeInterface) {
699
                    $this->_queryScopes[$className] = $instance;
700
                }
701
            }
702
        }
703
    }
704
}
705