Completed
Push — 2.0 ( d2a799...01dd29 )
by Christopher
06:28
created

EavBehavior::_prepareSetValues()   C

Complexity

Conditions 8
Paths 13

Size

Total Lines 64
Code Lines 43

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 8
eloc 43
nc 13
nop 2
dl 0
loc 64
rs 6.8232
c 0
b 0
f 0

How to fix   Long Method   

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