Completed
Push — 2.0 ( 4ea1b8...321c43 )
by Christopher
03:46
created

EavBehavior::dropColumn()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 18
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
c 0
b 0
f 0
nc 2
nop 2
dl 0
loc 18
rs 9.4285
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
     * Represents an entity that should be removed from the collection.
85
     *
86
     * @var int
87
     */
88
    const NULL_ENTITY = -1;
89
90
    /**
91
     * Default configuration.
92
     *
93
     * - enabled: Whether this behavior is active or not. Defaults true.
94
     *
95
     * - cache: EAV cache feature, see documentation. Defaults false.
96
     *
97
     * - hydrator: Callable function responsible of hydrate an entity with its
98
     *   virtual values, callable receives two arguments: the entity to hydrate and
99
     *   an array of virtual values, where each virtual value is an arrray composed
100
     *   of `property_name` and `value` keys.
101
     *
102
     * @var array
103
     */
104
    protected $_defaultConfig = [
105
        'status' => true,
106
        'cache' => false,
107
        'hydrator' => null,
108
        'queryScope' => [
109
            'Eav\\Model\\Behavior\\QueryScope\\SelectScope',
110
            'Eav\\Model\\Behavior\\QueryScope\\WhereScope',
111
            'Eav\\Model\\Behavior\\QueryScope\\OrderScope',
112
        ],
113
        'implementedMethods' => [
114
            'eav' => 'eav',
115
            'updateEavCache' => 'updateEavCache',
116
            'addColumn' => 'addColumn',
117
            'dropColumn' => 'dropColumn',
118
            'listColumns' => 'listColumns',
119
        ],
120
    ];
121
122
    /**
123
     * Query scopes objects to be applied indexed by unique ID.
124
     *
125
     * @var array
126
     */
127
    protected $_queryScopes = [];
128
129
    /**
130
     * Constructor.
131
     *
132
     * @param \Cake\ORM\Table $table The table this behavior is attached to
133
     * @param array $config Configuration array for this behavior
134
     */
135
    public function __construct(Table $table, array $config = [])
136
    {
137
        $this->_defaultConfig['hydrator'] = function (EntityInterface $entity, $values) {
138
            return $this->hydrateEntity($entity, $values);
139
        };
140
141
        $config['cacheMap'] = false; // private config, prevent user modifications
142
        $this->_toolbox = new EavToolbox($table);
143
        parent::__construct($table, $config);
144
145
        if ($this->config('cache')) {
146
            $info = $this->config('cache');
147
            $holders = []; // column => [list of virtual columns]
148
149
            if (is_string($info)) {
150
                $holders[$info] = ['*'];
151
            } elseif (is_array($info)) {
152
                foreach ($info as $column => $fields) {
153
                    if (is_integer($column)) {
154
                        $holders[$fields] = ['*'];
155
                    } else {
156
                        $holders[$column] = ($fields === '*') ? ['*'] : $fields;
157
                    }
158
                }
159
            }
160
161
            $this->config('cacheMap', $holders);
162
        }
163
    }
164
165
    /**
166
     * Gets/sets EAV status.
167
     *
168
     * - TRUE: Enables EAV behavior so virtual columns WILL be fetched from database.
169
     * - FALSE: Disables EAV behavior so virtual columns WLL NOT be fetched from database.
170
     *
171
     * @param bool|null $status EAV status to set, or null to get current state
172
     * @return void|bool Current status if `$status` is set to null
173
     */
174
    public function eav($status = null)
175
    {
176
        if ($status === null) {
177
            return $this->config('status');
178
        }
179
180
        $this->config('status', (bool)$status);
181
    }
182
183
    /**
184
     * Defines a new virtual-column, or update if already defined.
185
     *
186
     * ### Usage:
187
     *
188
     * ```php
189
     * $errors = $this->Users->addColumn('user-age', [
190
     *     'type' => 'integer',
191
     *     'bundle' => 'some-bundle-name',
192
     *     'extra' => [
193
     *         'option1' => 'value1'
194
     *     ]
195
     * ], true);
196
     *
197
     * if (empty($errors)) {
198
     *     // OK
199
     * } else {
200
     *     // ERROR
201
     *     debug($errors);
202
     * }
203
     * ```
204
     *
205
     * The third argument can be set to FALSE to get a boolean response:
206
     *
207
     * ```php
208
     * $success = $this->Users->addColumn('user-age', [
209
     *     'type' => 'integer',
210
     *     'bundle' => 'some-bundle-name',
211
     *     'extra' => [
212
     *         'option1' => 'value1'
213
     *     ]
214
     * ]);
215
     *
216
     * if ($success) {
217
     *     // OK
218
     * } else {
219
     *     // ERROR
220
     * }
221
     * ```
222
     *
223
     * @param string $name Column name. e.g. `user-age`
224
     * @param array $options Column configuration options
225
     * @param bool $errors If set to true will return an array list of errors
226
     *  instead of boolean response. Defaults to TRUE
227
     * @return bool|array True on success or array of error messages, depending on
228
     *  $error argument
229
     * @throws \Cake\Error\FatalErrorException When provided column name collides
230
     *  with existing column names. And when an invalid type is provided
231
     */
232
    public function addColumn($name, array $options = [], $errors = true)
233
    {
234
        if (in_array($name, (array)$this->_table->schema()->columns())) {
235
            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()));
236
        }
237
238
        $data = $options + [
239
            'type' => 'string',
240
            'bundle' => null,
241
            'searchable' => true,
242
            'overwrite' => false,
243
        ];
244
245
        $data['type'] = $this->_toolbox->mapType($data['type']);
246
        if (!in_array($data['type'], EavToolbox::$types)) {
247
            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']));
248
        }
249
250
        $data['name'] = $name;
251
        $data['table_alias'] = $this->_table->table();
252
        $attr = TableRegistry::get('Eav.EavAttributes')->find()
253
            ->where([
254
                'name' => $data['name'],
255
                'table_alias' => $data['table_alias'],
256
                'bundle IS' => $data['bundle'],
257
            ])
258
            ->limit(1)
259
            ->first();
260
261
        if ($attr && !$data['overwrite']) {
262
            throw new FatalErrorException(__d('eav', 'Virtual column "{0}" already defined, use the "overwrite" option if you want to change it.', $name));
263
        }
264
265
        if ($attr) {
266
            $attr = TableRegistry::get('Eav.EavAttributes')->patchEntity($attr, $data);
267
        } else {
268
            $attr = TableRegistry::get('Eav.EavAttributes')->newEntity($data);
269
        }
270
271
        $success = (bool)TableRegistry::get('Eav.EavAttributes')->save($attr);
272
        Cache::clear(false, 'eav_table_attrs');
273
274
        if ($errors) {
275
            return (array)$attr->errors();
276
        }
277
278
        return (bool)$success;
279
    }
280
281
    /**
282
     * Drops an existing column.
283
     *
284
     * @param string $name Name of the column to drop
285
     * @param string|null $bundle Removes the column within a particular bundle
286
     * @return bool True on success, false otherwise
287
     */
288
    public function dropColumn($name, $bundle = null)
289
    {
290
        $attr = TableRegistry::get('Eav.EavAttributes')->find()
291
            ->where([
292
                'name' => $name,
293
                'table_alias' => $this->_table->table(),
294
                'bundle IS' => $bundle,
295
            ])
296
            ->limit(1)
297
            ->first();
298
299
        Cache::clear(false, 'eav_table_attrs');
300
        if ($attr) {
301
            return (bool)TableRegistry::get('Eav.EavAttributes')->delete($attr);
302
        }
303
304
        return false;
305
    }
306
307
    /**
308
     * Gets a list of virtual columns attached to this table.
309
     *
310
     * @param string|null $bundle Get attributes within given bundle, or all of them
311
     *  regardless of the bundle if not provided
312
     * @return array Columns information indexed by column name
313
     */
314
    public function listColumns($bundle = null)
315
    {
316
        $columns = [];
317
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
318
            $columns[$name] = [
319
                'id' => $attr->get('id'),
320
                'bundle' => $attr->get('bundle'),
321
                'name' => $name,
322
                'type' => $attr->get('type'),
323
                'searchable ' => $attr->get('searchable'),
324
                'extra ' => $attr->get('extra'),
325
            ];
326
        }
327
328
        return $columns;
329
    }
330
331
    /**
332
     * Update EAV cache for the specified $entity.
333
     *
334
     * @return bool Success
335
     */
336
    public function updateEavCache(EntityInterface $entity)
337
    {
338
        if (!$this->config('cacheMap')) {
339
            return false;
340
        }
341
342
        $attrsById = [];
343
        foreach ($this->_toolbox->attributes() as $attr) {
344
            $attrsById[$attr['id']] = $attr;
345
        }
346
347
        if (empty($attrsById)) {
348
            return true; // nothing to cache
349
        }
350
351
        $query = TableRegistry::get('Eav.EavValues')
352
            ->find('all')
353
            ->bufferResults(false)
354
            ->where([
355
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
356
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
357
            ])
358
            ->toArray();
359
360
        $values = [];
361
        foreach ($query as $v) {
362
            $type = $attrsById[$v->get('eav_attribute_id')]->get('type');
363
            $name = $attrsById[$v->get('eav_attribute_id')]->get('name');
364
            $values[$name] = $this->_toolbox->marshal($v->get("value_{$type}"), $type);
365
        }
366
367
        $toUpdate = [];
368
        foreach ((array)$this->config('cacheMap') as $column => $fields) {
369
            $cache = [];
370
            if (in_array('*', $fields)) {
371
                $cache = $values;
372
            } else {
373
                foreach ($fields as $field) {
374
                    if (isset($values[$field])) {
375
                        $cache[$field] = $values[$field];
376
                    }
377
                }
378
            }
379
380
            $toUpdate[$column] = (string)serialize(new CachedColumn($cache));
381
        }
382
383
        if (!empty($toUpdate)) {
384
            $conditions = []; // scope to entity's PK (composed PK supported)
385
            $keys = $this->_table->primaryKey();
386
            $keys = !is_array($keys) ? [$keys] : $keys;
387
            foreach ($keys as $key) {
388
                // TODO: check key exists in entity's visible properties list.
389
                // Throw an error otherwise as PK MUST be correctly calculated.
390
                $conditions[$key] = $entity->get($key);
391
            }
392
393
            if (empty($conditions)) {
394
                return false;
395
            }
396
397
            return (bool)$this->_table->updateAll($toUpdate, $conditions);
398
        }
399
400
        return true;
401
    }
402
403
    /**
404
     * Attaches virtual properties to entities.
405
     *
406
     * This method is also responsible of looking for virtual columns in SELECT and
407
     * WHERE clauses (if applicable) and properly scope the Query object. Query
408
     * scoping is performed by the `_scopeQuery()` method.
409
     *
410
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
411
     * @param \Cake\ORM\Query $query The original query to modify
412
     * @param \ArrayObject $options Additional options given as an array
413
     * @param bool $primary Whether this find is a primary query or not
414
     * @return bool|null
415
     */
416
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
417
    {
418
        if (!$this->config('status') ||
419
            (isset($options['eav']) && $options['eav'] === false)
420
        ) {
421
            return true;
422
        }
423
424
        $options['bundle'] = !isset($options['bundle']) ? null : $options['bundle'];
425
        $query = $this->_scopeQuery($query, $options['bundle']);
426
427
        if (empty($this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope'])) {
428
            return $query;
429
        }
430
431
        $selectedVirtual = $this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope']->getVirtualColumns($query, $options['bundle']);
432
        $args = compact('options', 'primary', 'selectedVirtual');
433
434
        return $query->formatResults(function ($results) use ($args) {
435
            return $this->_hydrateEntities($results, $args);
436
        }, Query::PREPEND);
437
    }
438
439
    /**
440
     * Attach EAV attributes for every entity in the provided result-set.
441
     *
442
     * This method iterates over each retrieved entity and invokes the
443
     * `hydrateEntity()` method. This last should return the altered entity object
444
     * with all its virtual properties, however if this method returns NULL the
445
     * entity will be removed from the resulting collection.
446
     *
447
     * @param \Cake\Collection\CollectionInterface $entities Set of entities to be
448
     *  processed
449
     * @param array $args Contains three keys: "options" and "primary" given to the
450
     *  originating beforeFind(), and "selectedVirtual", a list of virtual columns
451
     *  selected in the originating find query
452
     * @return \Cake\Collection\CollectionInterface New set with altered entities
453
     */
454
    protected function _hydrateEntities(CollectionInterface $entities, array $args)
455
    {
456
        $values = $this->_prepareSetValues($entities, $args);
457
458
        return $entities->map(function ($entity) use ($values) {
459
            if ($entity instanceof EntityInterface) {
460
                $entity = $this->_prepareCachedColumns($entity);
461
                $entityId = $this->_toolbox->getEntityId($entity);
462
                $entityValues = isset($values[$entityId]) ? $values[$entityId] : [];
463
                $hydrator = $this->config('hydrator');
464
                $entity = $hydrator($entity, $entityValues);
465
466
                if ($entity === null) {
467
                    // mark as NULL_ENTITY
468
                    $entity = self::NULL_ENTITY;
469
                }
470
            }
471
472
            return $entity;
473
        })
474
        ->filter(function ($entity) {
475
            // remove all entities marked as NULL_ENTITY
476
            return $entity !== self::NULL_ENTITY;
477
        });
478
    }
479
480
    /**
481
     * Hydrates a single entity and returns it.
482
     *
483
     * Returning NULL indicates the entity should be removed from the resulting
484
     * collection.
485
     *
486
     * @param \Cake\Datasource\EntityInterface $entity The entity to hydrate
487
     * @param array $values Holds stored virtual values for this particular entity
488
     * @return bool|null|\Cake\Datasource\EntityInterface
489
     */
490
    public function hydrateEntity(EntityInterface $entity, array $values)
491
    {
492
        foreach ($values as $value) {
493
            if (!$this->_toolbox->propertyExists($entity, $value['property_name'])) {
494
                $entity->set($value['property_name'], $value['value']);
495
                $entity->dirty($value['property_name'], false);
496
            }
497
        }
498
499
        // force cache-columns to be of the proper type as they might be NULL if
500
        // entity has not been updated yet.
501
        if ($this->config('cacheMap')) {
502
            foreach ($this->config('cacheMap') as $column => $fields) {
503
                if ($this->_toolbox->propertyExists($entity, $column) && !($entity->get($column) instanceof Entity)) {
504
                    $entity->set($column, new Entity);
505
                }
506
            }
507
        }
508
509
        return $entity;
510
    }
511
512
    /**
513
     * Retrieves all virtual values of all the entities within the given result-set.
514
     *
515
     * @param \Cake\Collection\CollectionInterface $entities Set of entities
516
     * @param array $args Contains two keys: "options" and "primary" given to the
517
     *  originating beforeFind(), and "selectedVirtual" a list of virtual columns
518
     *  selected in the originating find query
519
     * @return array Virtual values indexed by entity ID
520
     */
521
    protected function _prepareSetValues(CollectionInterface $entities, array $args)
522
    {
523
        $entityIds = $this->_toolbox->extractEntityIds($entities);
524
        if (empty($entityIds)) {
525
            return [];
526
        }
527
528
        $selectedVirtual = $args['selectedVirtual'];
529
        $bundle = $args['options']['bundle'];
530
        $validColumns = array_values($selectedVirtual);
531
        $validNames = array_intersect($this->_toolbox->getAttributeNames($bundle), $validColumns);
532
        $attrsById = [];
533
534
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
535
            if (in_array($name, $validNames)) {
536
                $attrsById[$attr['id']] = $attr;
537
            }
538
        }
539
540
        if (empty($attrsById)) {
541
            return [];
542
        }
543
544
        return TableRegistry::get('Eav.EavValues')
545
            ->find('all')
546
            ->bufferResults(false)
547
            ->where([
548
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
549
                'EavValues.entity_id IN' => $entityIds,
550
            ])
551
            ->all()
552
            ->map(function ($value) use ($attrsById, $selectedVirtual) {
553
                $attrName = $attrsById[$value->get('eav_attribute_id')]->get('name');
554
                $attrType = $attrsById[$value->get('eav_attribute_id')]->get('type');
555
                $alias = array_search($attrName, $selectedVirtual);
556
557
                return [
558
                    'entity_id' => $value->get('entity_id'),
559
                    'property_name' => is_string($alias) ? $alias : $attrName,
560
                    'value' => $this->_toolbox->marshal($value->get("value_{$attrType}"), $attrType),
561
                ];
562
            })
563
            ->groupBy('entity_id')
564
            ->toArray();
565
    }
566
567
    /**
568
     * Triggered before data is converted into entities.
569
     *
570
     * Converts incoming POST data to its corresponding types.
571
     *
572
     * @param \Cake\Event\Event $event The event that was triggered
573
     * @param \ArrayObject $data The POST data to be merged with entity
574
     * @param \ArrayObject $options The options passed to the marshaller
575
     * @return void
576
     */
577
    public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
578
    {
579
        $bundle = !empty($options['bundle']) ? $options['bundle'] : null;
580
        $attrs = array_keys($this->_toolbox->attributes($bundle));
581
        foreach ($data as $property => $value) {
582
            if (!in_array($property, $attrs)) {
583
                continue;
584
            }
585
            $dataType = $this->_toolbox->getType($property);
586
            $marshaledValue = $this->_toolbox->marshal($value, $dataType);
587
            $data[$property] = $marshaledValue;
588
        }
589
    }
590
591
    /**
592
     * Save virtual values after an entity's real values were saved.
593
     *
594
     * @param \Cake\Event\Event $event The event that was triggered
595
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
596
     * @param \ArrayObject $options Additional options given as an array
597
     * @return bool True always
598
     */
599
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
600
    {
601
        $attrsById = [];
602
        $updatedAttrs = [];
603
        $valuesTable = TableRegistry::get('Eav.EavValues');
604
605
        foreach ($this->_toolbox->attributes() as $name => $attr) {
606
            if (!$this->_toolbox->propertyExists($entity, $name)) {
607
                continue;
608
            }
609
            $attrsById[$attr->get('id')] = $attr;
610
        }
611
612
        if (empty($attrsById)) {
613
            return true; // nothing to do
614
        }
615
616
        $values = $valuesTable
617
            ->find()
618
            ->bufferResults(false)
619
            ->where([
620
                'eav_attribute_id IN' => array_keys($attrsById),
621
                'entity_id' => $this->_toolbox->getEntityId($entity),
622
            ])
623
            ->toArray();
624
625
        foreach ($values as $value) {
626
            $updatedAttrs[] = $value->get('eav_attribute_id');
627
            $info = $attrsById[$value->get('eav_attribute_id')];
628
            $type = $this->_toolbox->getType($info->get('name'));
629
630
            $marshaledValue = $this->_toolbox->marshal($entity->get($info->get('name')), $type);
631
            $value->set("value_{$type}", $marshaledValue);
632
            $entity->set($info->get('name'), $marshaledValue);
633
            $valuesTable->save($value);
634
        }
635
636
        foreach ($this->_toolbox->attributes() as $name => $attr) {
637
            if (!$this->_toolbox->propertyExists($entity, $name)) {
638
                continue;
639
            }
640
641
            if (!in_array($attr->get('id'), $updatedAttrs)) {
642
                $type = $this->_toolbox->getType($name);
643
                $value = $valuesTable->newEntity([
644
                    'eav_attribute_id' => $attr->get('id'),
645
                    'entity_id' => $this->_toolbox->getEntityId($entity),
646
                ]);
647
648
                $marshaledValue = $this->_toolbox->marshal($entity->get($name), $type);
649
                $value->set("value_{$type}", $marshaledValue);
650
                $entity->set($name, $marshaledValue);
651
                $valuesTable->save($value);
652
            }
653
        }
654
655
        if ($this->config('cacheMap')) {
656
            $this->updateEavCache($entity);
657
        }
658
659
        return true;
660
    }
661
662
    /**
663
     * After an entity was removed from database. Here is when EAV values are
664
     * removed from DB.
665
     *
666
     * @param \Cake\Event\Event $event The event that was triggered
667
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
668
     * @param \ArrayObject $options Additional options given as an array
669
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
670
     * @return void
671
     */
672
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
673
    {
674
        if (!$options['atomic']) {
675
            throw new FatalErrorException(__d('eav', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
676
        }
677
678
        $valuesToDelete = TableRegistry::get('Eav.EavValues')
679
            ->find()
680
            ->bufferResults(false)
681
            ->contain(['EavAttribute'])
682
            ->where([
683
                'EavAttribute.table_alias' => $this->_table->table(),
684
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
685
            ])
686
            ->toArray();
687
688
        foreach ($valuesToDelete as $value) {
689
            TableRegistry::get('Eav.EavValues')->delete($value);
690
        }
691
    }
692
693
    /**
694
     * Prepares entity's cache-columns (those defined using `cache` option).
695
     *
696
     * @param \Cake\Datasource\EntityInterface $entity The entity to prepare
697
     * @return \Cake\Datasource\EntityInterfa Modified entity
698
     */
699
    protected function _prepareCachedColumns(EntityInterface $entity)
700
    {
701
        if ($this->config('cacheMap')) {
702
            foreach ((array)$this->config('cacheMap') as $column => $fields) {
703
                if (in_array($column, $entity->visibleProperties())) {
704
                    $string = $entity->get($column);
705
                    if ($string == serialize(false) || @unserialize($string) !== false) {
706
                        $entity->set($column, unserialize($string));
707
                    } else {
708
                        $entity->set($column, new CachedColumn());
709
                    }
710
                }
711
            }
712
        }
713
714
        return $entity;
715
    }
716
717
    /**
718
     * Look for virtual columns in some query's clauses.
719
     *
720
     * @param \Cake\ORM\Query $query The query to scope
721
     * @param string|null $bundle Consider attributes only for a specific bundle
722
     * @return \Cake\ORM\Query The modified query object
723
     */
724
    protected function _scopeQuery(Query $query, $bundle = null)
725
    {
726
        $this->_initScopes();
727
        foreach ($this->_queryScopes as $scope) {
728
            if ($scope instanceof QueryScopeInterface) {
729
                $query = $scope->scope($query, $bundle);
730
            }
731
        }
732
733
        return $query;
734
    }
735
736
    /**
737
     * Initializes the scope objects
738
     *
739
     * @return void
740
     */
741
    protected function _initScopes()
742
    {
743
        foreach ((array)$this->config('queryScope') as $className) {
744
            if (!empty($this->_queryScopes[$className])) {
745
                continue;
746
            }
747
748
            if (class_exists($className)) {
749
                $instance = new $className($this->_table);
750
                if ($instance instanceof QueryScopeInterface) {
751
                    $this->_queryScopes[$className] = $instance;
752
                }
753
            }
754
        }
755
    }
756
}
757