Completed
Push — 2.0 ( e262db...d2a799 )
by Christopher
02:30
created

EavBehavior::hydrateEntity()   C

Complexity

Conditions 8
Paths 8

Size

Total Lines 29
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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