EavBehavior::updateEavCache()   C
last analyzed

Complexity

Conditions 13
Paths 111

Size

Total Lines 65
Code Lines 39

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 13
eloc 39
nc 111
nop 1
dl 0
loc 65
rs 5.6665
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

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