Completed
Push — 2.0 ( 3d24de...bbc260 )
by Christopher
03:18
created

EavBehavior::enableEav()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
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 = 2;
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
        'enabled' => 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
            'enableEav' => 'enableEav',
115
            'disableEav' => 'disableEav',
116
            'updateEavCache' => 'updateEavCache',
117
            'addColumn' => 'addColumn',
118
            'dropColumn' => 'dropColumn',
119
            'listColumns' => 'listColumns',
120
        ],
121
    ];
122
123
    /**
124
     * Query scopes objects to be applied indexed by unique ID.
125
     *
126
     * @var array
127
     */
128
    protected $_queryScopes = [];
129
130
    /**
131
     * Constructor.
132
     *
133
     * @param \Cake\ORM\Table $table The table this behavior is attached to
134
     * @param array $config Configuration array for this behavior
135
     */
136
    public function __construct(Table $table, array $config = [])
137
    {
138
        $this->_defaultConfig['hydrator'] = function (EntityInterface $entity, $values) {
139
            return $this->hydrateEntity($entity, $values);
140
        };
141
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
     * Enables EAV behavior so virtual columns WILL be fetched from database.
168
     *
169
     * @return void
170
     */
171
    public function enableEav()
172
    {
173
        $this->config('enabled', true);
174
    }
175
176
    /**
177
     * Disables EAV behavior so virtual columns WLL NOT be fetched from database.
178
     *
179
     * @return void
180
     */
181
    public function disableEav()
182
    {
183
        $this->config('enabled', false);
184
    }
185
186
    /**
187
     * Defines a new virtual-column, or update if already defined.
188
     *
189
     * ### Usage:
190
     *
191
     * ```php
192
     * $errors = $this->Users->addColumn('user-age', [
193
     *     'type' => 'integer',
194
     *     'bundle' => 'some-bundle-name',
195
     *     'extra' => [
196
     *         'option1' => 'value1'
197
     *     ]
198
     * ], true);
199
     *
200
     * if (empty($errors)) {
201
     *     // OK
202
     * } else {
203
     *     // ERROR
204
     *     debug($errors);
205
     * }
206
     * ```
207
     *
208
     * The third argument can be set to FALSE to get a boolean response:
209
     *
210
     * ```php
211
     * $success = $this->Users->addColumn('user-age', [
212
     *     'type' => 'integer',
213
     *     'bundle' => 'some-bundle-name',
214
     *     'extra' => [
215
     *         'option1' => 'value1'
216
     *     ]
217
     * ]);
218
     *
219
     * if ($success) {
220
     *     // OK
221
     * } else {
222
     *     // ERROR
223
     * }
224
     * ```
225
     *
226
     * @param string $name Column name. e.g. `user-age`
227
     * @param array $options Column configuration options
228
     * @param bool $errors If set to true will return an array list of errors
229
     *  instead of boolean response. Defaults to TRUE
230
     * @return bool|array True on success or array of error messages, depending on
231
     *  $error argument
232
     * @throws \Cake\Error\FatalErrorException When provided column name collides
233
     *  with existing column names. And when an invalid type is provided
234
     */
235
    public function addColumn($name, array $options = [], $errors = true)
236
    {
237
        if (in_array($name, (array)$this->_table->schema()->columns())) {
238
            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()));
239
        }
240
241
        $data = $options + [
242
            'type' => 'string',
243
            'bundle' => null,
244
            'searchable' => true,
245
        ];
246
247
        $data['type'] = $this->_toolbox->mapType($data['type']);
248
        if (!in_array($data['type'], EavToolbox::$types)) {
249
            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']));
250
        }
251
252
        $data['name'] = $name;
253
        $data['table_alias'] = $this->_table->table();
254
        $attr = TableRegistry::get('Eav.EavAttributes')->find()
255
            ->where([
256
                'name' => $data['name'],
257
                'table_alias' => $data['table_alias'],
258
                'bundle IS' => $data['bundle'],
259
            ])
260
            ->limit(1)
261
            ->first();
262
263
        if ($attr) {
264
            $attr = TableRegistry::get('Eav.EavAttributes')->patchEntity($attr, $data);
265
        } else {
266
            $attr = TableRegistry::get('Eav.EavAttributes')->newEntity($data);
267
        }
268
269
        $success = (bool)TableRegistry::get('Eav.EavAttributes')->save($attr);
270
        Cache::clear(false, 'eav_table_attrs');
271
272
        if ($errors) {
273
            return (array)$attr->errors();
274
        }
275
276
        return (bool)$success;
277
    }
278
279
    /**
280
     * Drops an existing column.
281
     *
282
     * @param string $name Name of the column to drop
283
     * @param string|null $bundle Removes the column within a particular bundle
284
     * @return bool True on success, false otherwise
285
     */
286
    public function dropColumn($name, $bundle = null)
287
    {
288
        $attr = TableRegistry::get('Eav.EavAttributes')->find()
289
            ->where([
290
                'name' => $name,
291
                'table_alias' => $this->_table->table(),
292
                'bundle IS' => $bundle,
293
            ])
294
            ->limit(1)
295
            ->first();
296
297
        Cache::clear(false, 'eav_table_attrs');
298
        if ($attr) {
299
            return (bool)TableRegistry::get('Eav.EavAttributes')->delete($attr);
300
        }
301
302
        return false;
303
    }
304
305
    /**
306
     * Gets a list of virtual columns attached to this table.
307
     *
308
     * @param string|null $bundle Get attributes within given bundle, or all of them
309
     *  regardless of the bundle if not provided
310
     * @return array Columns information indexed by column name
311
     */
312
    public function listColumns($bundle = null)
313
    {
314
        $columns = [];
315
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
316
            $columns[$name] = [
317
                'id' => $attr->get('id'),
318
                'bundle' => $attr->get('bundle'),
319
                'name' => $name,
320
                'type' => $attr->get('type'),
321
                'searchable ' => $attr->get('searchable'),
322
                'extra ' => $attr->get('extra'),
323
            ];
324
        }
325
326
        return $columns;
327
    }
328
329
    /**
330
     * Update EAV cache for the specified $entity.
331
     *
332
     * @return bool Success
333
     */
334
    public function updateEavCache(EntityInterface $entity)
335
    {
336
        if (!$this->config('cacheMap')) {
337
            return false;
338
        }
339
340
        $attrsById = [];
341
        foreach ($this->_toolbox->attributes() as $attr) {
342
            $attrsById[$attr['id']] = $attr;
343
        }
344
345
        if (empty($attrsById)) {
346
            return true; // nothing to cache
347
        }
348
349
        $values = [];
350
        $query = TableRegistry::get('Eav.EavValues')
351
            ->find('all')
352
            ->where([
353
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
354
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
355
            ]);
356
357
        foreach ($query as $v) {
358
            $type = $attrsById[$v->get('eav_attribute_id')]->get('type');
359
            $name = $attrsById[$v->get('eav_attribute_id')]->get('name');
360
            $values[$name] = $this->_toolbox->marshal($v->get("value_{$type}"), $type);
361
        }
362
363
        $toUpdate = [];
364
        foreach ((array)$this->config('cacheMap') as $column => $fields) {
365
            $cache = [];
366
            if (in_array('*', $fields)) {
367
                $cache = $values;
368
            } else {
369
                foreach ($fields as $field) {
370
                    if (isset($values[$field])) {
371
                        $cache[$field] = $values[$field];
372
                    }
373
                }
374
            }
375
376
            $toUpdate[$column] = (string)serialize(new CachedColumn($cache));
377
        }
378
379
        if (!empty($toUpdate)) {
380
            $conditions = []; // scope to entity's PK (composed PK supported)
381
            $keys = $this->_table->primaryKey();
382
            $keys = !is_array($keys) ? [$keys] : $keys;
383
            foreach ($keys as $key) {
384
                // TODO: check key exists in entity's visible properties list.
385
                // Throw an error otherwise as PK MUST be correctly calculated.
386
                $conditions[$key] = $entity->get($key);
387
            }
388
389
            if (empty($conditions)) {
390
                return false;
391
            }
392
393
            return (bool)$this->_table->updateAll($toUpdate, $conditions);
394
        }
395
396
        return true;
397
    }
398
399
    /**
400
     * Attaches virtual properties to entities.
401
     *
402
     * This method is also responsible of looking for virtual columns in SELECT and
403
     * WHERE clauses (if applicable) and properly scope the Query object. Query
404
     * scoping is performed by the `_scopeQuery()` method.
405
     *
406
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
407
     * @param \Cake\ORM\Query $query The original query to modify
408
     * @param \ArrayObject $options Additional options given as an array
409
     * @param bool $primary Whether this find is a primary query or not
410
     * @return bool|null
411
     */
412
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
413
    {
414 View Code Duplication
        if (!$this->config('enabled') ||
415
            (isset($options['eav']) && $options['eav'] === false)
416
        ) {
417
            return true;
418
        }
419
420
        $options['bundle'] = !isset($options['bundle']) ? null : $options['bundle'];
421
        $query = $this->_scopeQuery($query, $options['bundle']);
422
423
        if (empty($this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope'])) {
424
            return $query;
425
        }
426
427
        $selectedVirtual = $this->_queryScopes['Eav\\Model\\Behavior\\QueryScope\\SelectScope']->getVirtualColumns($query, $options['bundle']);
428
        $args = compact('options', 'primary', 'selectedVirtual');
429
430
        return $query->formatResults(function ($results) use($args) {
431
            return $this->_hydrateEntities($results, $args);
432
        }, Query::PREPEND);
433
    }
434
435
436
    /**
437
     * Attach EAV attributes for every entity in the provided result-set.
438
     *
439
     * This method iterates over each retrieved entity and invokes the
440
     * `hydrateEntity()` method. This last should return the altered entity object
441
     * with all its virtual properties, however if this method returns NULL the
442
     * entity will be removed from the resulting collection.
443
     *
444
     * @param \Cake\Collection\CollectionInterface $entities Set of entities to be
445
     *  processed
446
     * @param array $args Contains two keys: "options" and "primary" given to the
447
     *  originating beforeFind(), and "selectedVirtual" a list of virtual columns
448
     *  selected in the originating find query
449
     * @return \Cake\Collection\CollectionInterface New set with altered entities
450
     */
451
    protected function _hydrateEntities(CollectionInterface $entities, array $args)
452
    {
453
        $values = $this->_prepareSetValues($entities, $args);
454
        return $entities->map(function ($entity) use ($values) {
455
            if ($entity instanceof EntityInterface) {
456
                $entity = $this->_prepareCachedColumns($entity);
457
                $entityId = $this->_toolbox->getEntityId($entity);
458
459
                if (isset($values[$entityId])) {
460
                    $values = isset($values[$entityId]) ? $values[$entityId] : [];
0 ignored issues
show
Bug introduced by
Consider using a different name than the imported variable $values, or did you forget to import by reference?

It seems like you are assigning to a variable which was imported through a use statement which was not imported by reference.

For clarity, we suggest to use a different name or import by reference depending on whether you would like to have the change visibile in outer-scope.

Change not visible in outer-scope

$x = 1;
$callable = function() use ($x) {
    $x = 2; // Not visible in outer scope. If you would like this, how
            // about using a different variable name than $x?
};

$callable();
var_dump($x); // integer(1)

Change visible in outer-scope

$x = 1;
$callable = function() use (&$x) {
    $x = 2;
};

$callable();
var_dump($x); // integer(2)
Loading history...
461
                    $hydrator = $this->config('hydrator');
462
                    $entity = $hydrator($entity, $values);
463
                }
464
            }
465
466
            // remove from collection
467
            if ($entity === null) {
468
                return self::NULL_ENTITY;
469
            }
470
471
            return $entity;
472
        })
473
        ->filter(function ($entity) {
474
            return $entity !== self::NULL_ENTITY;
475
        });
476
    }
477
478
    /**
479
     * Hydrates a single entity and returns it.
480
     *
481
     * Returning NULL indicates the entity should be removed from the resulting
482
     * collection.
483
     *
484
     * @param \Cake\Datasource\EntityInterface $entity The entity to hydrate
485
     * @param array $values Holds stored virtual values for this particular entity
486
     * @return bool|null|\Cake\Datasource\EntityInterface
487
     */
488
    public function hydrateEntity(EntityInterface $entity, array $values)
489
    {
490
        foreach ($values as $value) {
491
            if (!$this->_toolbox->propertyExists($entity, $value['property_name'])) {
492
                $entity->set($value['property_name'], $value['value']);
493
                $entity->dirty($value['property_name'], false);
494
            }
495
        }
496
497
        // force cache-columns to be of the proper type as they might be NULL if
498
        // entity has not been updated yet.
499
        if ($this->config('cacheMap')) {
500
            foreach ($this->config('cacheMap') as $column => $fields) {
501
                if ($this->_toolbox->propertyExists($entity, $column) && !($entity->get($column) instanceof Entity)) {
502
                    $entity->set($column, new Entity);
503
                }
504
            }
505
        }
506
507
        return $entity;
508
    }
509
510
    /**
511
     * Retrieves all virtual values of all the entities within the given result-set.
512
     *
513
     * @param \Cake\Collection\CollectionInterface $entities Set of entities
514
     * @param array $args Contains two keys: "options" and "primary" given to the
515
     *  originating beforeFind(), and "selectedVirtual" a list of virtual columns
516
     *  selected in the originating find query
517
     * @return array Virtual values indexed by entity ID
518
     */
519
    protected function _prepareSetValues(CollectionInterface $entities, array $args)
520
    {
521
        $entityIds = $this->_toolbox->extractEntityIds($entities);
522
        if (empty($entityIds)) {
523
            return [];
524
        }
525
526
        $selectedVirtual = $args['selectedVirtual'];
527
        $bundle = $args['options']['bundle'];
528
        $validColumns = array_values($selectedVirtual);
529
        $validNames = array_intersect($this->_toolbox->getAttributeNames($bundle), $validColumns);
530
        $attrsById = [];
531
532
        foreach ($this->_toolbox->attributes($bundle) as $name => $attr) {
533
            if (in_array($name, $validNames)) {
534
                $attrsById[$attr['id']] = $attr;
535
            }
536
        }
537
538
        if (empty($attrsById)) {
539
            return [];
540
        }
541
542
        return TableRegistry::get('Eav.EavValues')
543
            ->find('all')
544
            ->bufferResults(false)
545
            ->where([
546
                'EavValues.eav_attribute_id IN' => array_keys($attrsById),
547
                'EavValues.entity_id IN' => $entityIds,
548
            ])
549
            ->all()
550
            ->map(function ($value) use ($attrsById, $selectedVirtual) {
551
                $attrName = $attrsById[$value->get('eav_attribute_id')]->get('name');
552
                $attrType = $attrsById[$value->get('eav_attribute_id')]->get('type');
553
                $alias = array_search($attrName, $selectedVirtual);
554
555
                return [
556
                    'entity_id' => $value->get('entity_id'),
557
                    'property_name' => is_string($alias) ? $alias : $attrName,
558
                    'value' => $this->_toolbox->marshal($value->get("value_{$attrType}"), $attrType),
559
                ];
560
            })
561
            ->groupBy('entity_id')
562
            ->toArray();
563
    }
564
565
    /**
566
     * Triggered before data is converted into entities.
567
     *
568
     * Converts incoming POST data to its corresponding types.
569
     *
570
     * @param \Cake\Event\Event $event The event that was triggered
571
     * @param \ArrayObject $data The POST data to be merged with entity
572
     * @param \ArrayObject $options The options passed to the marshaller
573
     * @return void
574
     */
575
    public function beforeMarshal(Event $event, ArrayObject $data, ArrayObject $options)
576
    {
577
        $bundle = !empty($options['bundle']) ? $options['bundle'] : null;
578
        $attrs = array_keys($this->_toolbox->attributes($bundle));
579
        foreach ($data as $property => $value) {
580
            if (!in_array($property, $attrs)) {
581
                continue;
582
            }
583
            $dataType = $this->_toolbox->getType($property);
584
            $marshaledValue = $this->_toolbox->marshal($value, $dataType);
585
            $data[$property] = $marshaledValue;
586
        }
587
    }
588
589
    /**
590
     * Save virtual values after an entity's real values were saved.
591
     *
592
     * @param \Cake\Event\Event $event The event that was triggered
593
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
594
     * @param \ArrayObject $options Additional options given as an array
595
     * @return bool True always
596
     */
597
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
598
    {
599
        $attrsById = [];
600
        $updatedAttrs = [];
601
        $valuesTable = TableRegistry::get('Eav.EavValues');
602
603
        foreach ($this->_toolbox->attributes() as $name => $attr) {
604
            if (!$this->_toolbox->propertyExists($entity, $name)) {
605
                continue;
606
            }
607
            $attrsById[$attr->get('id')] = $attr;
608
        }
609
610
        if (empty($attrsById)) {
611
            return true; // nothing to do
612
        }
613
614
        $values = $valuesTable
615
            ->find()
616
            ->bufferResults(false)
617
            ->where([
618
                'eav_attribute_id IN' => array_keys($attrsById),
619
                'entity_id' => $this->_toolbox->getEntityId($entity),
620
            ])
621
            ->toArray();
622
623
        foreach ($values as $value) {
624
            $updatedAttrs[] = $value->get('eav_attribute_id');
625
            $info = $attrsById[$value->get('eav_attribute_id')];
626
            $type = $this->_toolbox->getType($info->get('name'));
627
628
            $marshaledValue = $this->_toolbox->marshal($entity->get($info->get('name')), $type);
629
            $value->set("value_{$type}", $marshaledValue);
630
            $entity->set($info->get('name'), $marshaledValue);
631
            $valuesTable->save($value);
632
        }
633
634
        foreach ($this->_toolbox->attributes() as $name => $attr) {
635
            if (!$this->_toolbox->propertyExists($entity, $name)) {
636
                continue;
637
            }
638
639
            if (!in_array($attr->get('id'), $updatedAttrs)) {
640
                $type = $this->_toolbox->getType($name);
641
                $value = $valuesTable->newEntity([
642
                    'eav_attribute_id' => $attr->get('id'),
643
                    'entity_id' => $this->_toolbox->getEntityId($entity),
644
                ]);
645
646
                $marshaledValue = $this->_toolbox->marshal($entity->get($name), $type);
647
                $value->set("value_{$type}", $marshaledValue);
648
                $entity->set($name, $marshaledValue);
649
                $valuesTable->save($value);
650
            }
651
        }
652
653
        if ($this->config('cacheMap')) {
654
            $this->updateEavCache($entity);
655
        }
656
657
        return true;
658
    }
659
660
    /**
661
     * After an entity was removed from database. Here is when EAV values are
662
     * removed from DB.
663
     *
664
     * @param \Cake\Event\Event $event The event that was triggered
665
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
666
     * @param \ArrayObject $options Additional options given as an array
667
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
668
     * @return void
669
     */
670
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
671
    {
672
        if (!$options['atomic']) {
673
            throw new FatalErrorException(__d('eav', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
674
        }
675
676
        $valuesToDelete = TableRegistry::get('Eav.EavValues')
677
            ->find()
678
            ->contain(['EavAttribute'])
679
            ->where([
680
                'EavAttribute.table_alias' => $this->_table->table(),
681
                'EavValues.entity_id' => $this->_toolbox->getEntityId($entity),
682
            ])
683
            ->all();
684
685
        foreach ($valuesToDelete as $value) {
686
            TableRegistry::get('Eav.EavValues')->delete($value);
687
        }
688
    }
689
690
    /**
691
     * Prepares entity's cache-columns (those defined using `cache` option).
692
     *
693
     * @param \Cake\Datasource\EntityInterface $entity The entity to prepare
694
     * @return \Cake\Datasource\EntityInterfa Modified entity
695
     */
696
    protected function _prepareCachedColumns(EntityInterface $entity)
697
    {
698
        if ($this->config('cacheMap')) {
699
            foreach ((array)$this->config('cacheMap') as $column => $fields) {
700
                if (in_array($column, $entity->visibleProperties())) {
701
                    $string = $entity->get($column);
702
                    if ($string == serialize(false) || @unserialize($string) !== false) {
703
                        $entity->set($column, unserialize($string));
704
                    } else {
705
                        $entity->set($column, new CachedColumn());
706
                    }
707
                }
708
            }
709
        }
710
711
        return $entity;
712
    }
713
714
    /**
715
     * Look for virtual columns in some query's clauses.
716
     *
717
     * @param \Cake\ORM\Query $query The query to scope
718
     * @param string|null $bundle Consider attributes only for a specific bundle
719
     * @return \Cake\ORM\Query The modified query object
720
     */
721
    protected function _scopeQuery(Query $query, $bundle = null)
722
    {
723
        $this->_initScopes();
724
        foreach ($this->_queryScopes as $scope) {
725
            if ($scope instanceof QueryScopeInterface) {
726
                $query = $scope->scope($query, $bundle);
727
            }
728
        }
729
730
        return $query;
731
    }
732
733
    /**
734
     * Initializes the scope objects
735
     *
736
     * @return void
737
     */
738
    protected function _initScopes()
739
    {
740
        foreach ((array)$this->config('queryScope') as $className) {
741
            if (!empty($this->_queryScopes[$className])) {
742
                continue;
743
            }
744
745
            if (class_exists($className)) {
746
                $instance = new $className($this->_table);
747
                if ($instance instanceof QueryScopeInterface) {
748
                    $this->_queryScopes[$className] = $instance;
749
                }
750
            }
751
        }
752
    }
753
}
754