Completed
Pull Request — 2.0 (#161)
by Christopher
03:15
created

FieldableBehavior::beforeFind()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 14
Code Lines 7

Duplication

Lines 5
Ratio 35.71 %

Importance

Changes 2
Bugs 2 Features 0
Metric Value
cc 5
eloc 7
c 2
b 2
f 0
nc 3
nop 4
dl 5
loc 14
rs 8.8571
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 Field\Model\Behavior;
13
14
use Cake\Collection\CollectionInterface;
15
use Cake\Datasource\EntityInterface;
16
use Cake\Error\FatalErrorException;
17
use Cake\Event\Event;
18
use Cake\ORM\Behavior;
19
use Cake\ORM\Entity;
20
use Cake\ORM\Query;
21
use Cake\ORM\Table;
22
use Cake\ORM\TableRegistry;
23
use Cake\Validation\Validator;
24
use Eav\Model\Behavior\EavBehavior;
25
use Field\Collection\FieldCollection;
26
use Field\Model\Entity\Field;
27
use \ArrayObject;
28
29
/**
30
 * Fieldable Behavior.
31
 *
32
 * A more flexible EAV approach. Allows additional fields to be attached to Tables.
33
 * Any Table (Contents, Users, etc.) can use this behavior to make itself `fieldable`
34
 * and thus allow fields to be attached to it.
35
 *
36
 * The Field API defines two primary data structures, FieldInstance and FieldValue:
37
 *
38
 * - FieldInstance: is a Field attached to a single Table. (Schema equivalent: column)
39
 * - FieldValue: is the stored data for a particular [FieldInstance, Entity]
40
 *   tuple of your Table. (Schema equivalent: cell value)
41
 *
42
 * **This behavior allows you to add _virtual columns_ to your table schema.**
43
 *
44
 * @link https://github.com/quickapps/docs/blob/2.x/en/developers/field-api.rst
45
 */
46
class FieldableBehavior extends EavBehavior
47
{
48
49
    /**
50
     * Used for reduce BD queries and allow inter-method communication.
51
     * Example, it allows to pass some information from beforeDelete() to
52
     * afterDelete().
53
     *
54
     * @var array
55
     */
56
    protected $_cache = [];
57
58
    /**
59
     * Default configuration.
60
     *
61
     * These are merged with user-provided configuration when the behavior is used.
62
     * Available options are:
63
     *
64
     * - `bundle`: Bundle within this the table. Can be a string or a callable
65
     *   method that must return a string to use as bundle. Default null. If set to
66
     *   a callable function, it will receive the entity being saved as first
67
     *   argument, so you can calculate a bundle name for each particular entity.
68
     *
69
     * - `enabled`: True enables this behavior or false for disable. Default to
70
     *   true.
71
     *
72
     * - `cache`: Column-based cache. See EAV plugin's documentation.
73
     *
74
     * Bundles are usually set to dynamic values. For example, for the "contents"
75
     * table we have "content" entities, but we may have "article contents", "page
76
     * contents", etc. depending on the "type of content" they are; is said that
77
     * "article" and "page" **are bundles** of "contents" table.
78
     *
79
     * @var array
80
     */
81
    protected $_fieldableDefaultConfig = [
82
        'bundle' => null,
83
        'implementedMethods' => [
84
            'configureFieldable' => 'configureFieldable',
85
            'attachFields' => 'attachEntityFields',
86
            'unbindFieldable' => 'unbindFieldable',
87
            'bindFieldable' => 'bindFieldable',
88
        ],
89
    ];
90
91
    /**
92
     * Instance of EavAttributes table.
93
     *
94
     * @var \Eav\Model\Table\EavAttributesTable
95
     */
96
    public $Attributes = null;
97
98
    /**
99
     * Constructor.
100
     *
101
     * @param \Cake\ORM\Table $table The table this behavior is attached to
102
     * @param array $config Configuration array for this behavior
103
     */
104
    public function __construct(Table $table, array $config = [])
105
    {
106
        $this->_defaultConfig = array_merge($this->_defaultConfig, $this->_fieldableDefaultConfig);
107
        $this->Attributes = TableRegistry::get('Eav.EavAttributes');
108
        $this->Attributes->hasOne('Instance', [
109
            'className' => 'Field.FieldInstances',
110
            'foreignKey' => 'eav_attribute_id',
111
            'propertyName' => 'instance',
112
        ]);
113
        parent::__construct($table, $config);
114
    }
115
116
    /**
117
     * Returns a list of events this class is implementing. When the class is
118
     * registered in an event manager, each individual method will be associated
119
     * with the respective event.
120
     *
121
     * @return void
122
     */
123
    public function implementedEvents()
124
    {
125
        $events = [
126
            'Model.beforeFind' => ['callable' => 'beforeFind', 'priority' => 15],
127
            'Model.beforeSave' => ['callable' => 'beforeSave', 'priority' => 15],
128
            'Model.afterSave' => ['callable' => 'afterSave', 'priority' => 15],
129
            'Model.beforeDelete' => ['callable' => 'beforeDelete', 'priority' => 15],
130
            'Model.afterDelete' => ['callable' => 'afterDelete', 'priority' => 15],
131
        ];
132
133
        return $events;
134
    }
135
136
    /**
137
     * Modifies the query object in order to merge custom fields records
138
     * into each entity under the `_fields` property.
139
     *
140
     * ### Events Triggered:
141
     *
142
     * - `Field.<FieldHandler>.Entity.beforeFind`: This event is triggered for each
143
     *    entity in the resulting collection and for each field attached to these
144
     *    entities. It receives three arguments, a field entity representing the
145
     *    field being processed, an options array and boolean value indicating
146
     *    whether the query that initialized the event is part of a primary find
147
     *    operation or not. Returning false will cause the entity to be removed from
148
     *    the resulting collection, also will stop event propagation, so other
149
     *    fields won't be able to listen this event. If the event is stopped using
150
     *    the event API, will halt the entire find operation.
151
     *
152
     * You can enable or disable this behavior for a single `find()` or `get()`
153
     * operation by setting `fieldable` or `eav` to false in the options array for
154
     * find method. e.g.:
155
     *
156
     * ```php
157
     * $contents = $this->Contents->find('all', ['fieldable' => false]);
158
     * $content = $this->Contents->get($id, ['fieldable' => false]);
159
     * ```
160
     *
161
     * It also looks for custom fields in WHERE clause. This will search entities in
162
     * all bundles this table may have, if you need to restrict the search to an
163
     * specific bundle you must use the `bundle` key in find()'s options:
164
     *
165
     * ```php
166
     * $this->Contents
167
     *     ->find('all', ['bundle' => 'articles'])
168
     *     ->where(['article-title' => 'My first article!']);
169
     * ```
170
     *
171
     * The `bundle` option has no effects if no custom fields are given in the
172
     * WHERE clause.
173
     *
174
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
175
     * @param \Cake\ORM\Query $query The original query to modify
176
     * @param \ArrayObject $options Additional options given as an array
177
     * @param bool $primary Whether this find is a primary query or not
178
     * @return void
179
     */
180
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
181
    {
182 View Code Duplication
        if ((isset($options['fieldable']) && $options['fieldable'] === false) ||
183
            !$this->config('enabled')
184
        ) {
185
            return true;
186
        }
187
188
        if (array_key_exists('eav', $options)) {
189
            unset($options['eav']);
190
        }
191
192
        return parent::beforeFind($event, $query, $options, $primary);
193
    }
194
195
196
    /**
197
     * {@inheritDoc}
198
     */
199
    public function hydrateEntities(CollectionInterface $entities, array $options)
200
    {
201
        return $entities->map(function ($entity) use ($options) {
202
            if ($entity instanceof EntityInterface) {
203
                $entity = $this->_prepareCachedColumns($entity);
204
                $entity = $this->attachEntityAttributes($entity, $options);
205
            }
206
207
            if ($entity === false) {
208
                $options['event']->stopPropagation();
209
210
                return;
211
            }
212
213
            if ($entity === null) {
214
                return false;
215
            }
216
217
            return $entity;
218
        });
219
    }
220
221
    /**
222
     * {@inheritDoc}
223
     *
224
     * Attaches entity's field under the `_fields` property, this method is invoked
225
     * by `beforeFind()` when iterating results sets.
226
     */
227
    public function attachEntityAttributes(EntityInterface $entity, array $options)
228
    {
229
        $entity = $this->attachEntityFields($entity);
230
        foreach ($entity->get('_fields') as $field) {
231
            $result = $field->beforeFind((array)$options['options'], $options['primary']);
232
            if ($result === null) {
233
                return null; // remove entity from collection
234
            } elseif ($result === false) {
235
                return false; // abort find() operation
236
            }
237
        }
238
239
        return $entity;
240
    }
241
242
    /**
243
     * Before an entity is saved.
244
     *
245
     * Here is where we dispatch each custom field's `$_POST` information to its
246
     * corresponding Field Handler, so they can operate over their values.
247
     *
248
     * Fields Handler's `beforeSave()` method is automatically invoked for each
249
     * attached field for the entity being processed, your field handler should look
250
     * as follow:
251
     *
252
     * ```php
253
     * use Field\Handler;
254
     *
255
     * class TextField extends Handler
256
     * {
257
     *     public function beforeSave(Field $field, $post)
258
     *     {
259
     *          // alter $field, and do nifty things with $post
260
     *          // return FALSE; will halt the operation
261
     *     }
262
     * }
263
     * ```
264
     *
265
     * Field Handlers should **alter** `$field->value` and `$field->extra` according
266
     * to its needs using the provided **$post** argument.
267
     *
268
     * **NOTE:** Returning boolean FALSE will halt the whole Entity's save operation.
269
     *
270
     * @param \Cake\Event\Event $event The event that was triggered
271
     * @param \Cake\Datasource\EntityInterface $entity The entity being saved
272
     * @param \ArrayObject $options Additional options given as an array
273
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
274
     * @return bool True if save operation should continue
275
     */
276
    public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
277
    {
278
        if (!$this->config('enabled')) {
279
            return true;
280
        }
281
282
        if (!$options['atomic']) {
283
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be saved using transaction. Set [atomic = true]'));
284
        }
285
286
        if (!$this->_validation($entity)) {
287
            return false;
288
        }
289
290
        $this->_cache['createValues'] = [];
291
        foreach ($this->_attributesForEntity($entity) as $attr) {
292
            if (!$this->_toolbox->propertyExists($entity, $attr->get('name'))) {
0 ignored issues
show
Compatibility introduced by
$entity of type object<Cake\Datasource\EntityInterface> is not a sub-type of object<Cake\ORM\Entity>. It seems like you assume a concrete implementation of the interface Cake\Datasource\EntityInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
293
                continue;
294
            }
295
296
            $field = $this->_prepareMockField($entity, $attr);
297
            $result = $field->beforeSave($this->_fetchPost($field));
298
299
            if ($result === false) {
300
                $this->attachEntityFields($entity);
301
302
                return false;
303
            }
304
305
            $data = [
306
                'eav_attribute_id' => $field->get('metadata')->get('attribute_id'),
307
                'entity_id' => $this->_toolbox->getEntityId($entity),
308
                "value_{$field->metadata['type']}" => $field->get('value'),
309
                'extra' => $field->get('extra'),
310
            ];
311
312
            if ($field->get('metadata')->get('value_id')) {
313
                $valueEntity = TableRegistry::get('Eav.EavValues')->get($field->get('metadata')->get('value_id'));
314
                $valueEntity = TableRegistry::get('Eav.EavValues')->patchEntity($valueEntity, $data, ['validate' => false]);
315
            } else {
316
                $valueEntity = TableRegistry::get('Eav.EavValues')->newEntity($data, ['validate' => false]);
317
            }
318
319
            if ($entity->isNew() || $valueEntity->isNew()) {
320
                $this->_cache['createValues'][] = $valueEntity;
321
            } elseif (!TableRegistry::get('Eav.EavValues')->save($valueEntity)) {
322
                $this->attachEntityFields($entity);
323
                $event->stopPropagation();
324
325
                return false;
326
            }
327
        }
328
329
        $this->attachEntityFields($entity);
330
331
        return true;
332
    }
333
334
    /**
335
     * After an entity is saved.
336
     *
337
     * ### Events Triggered:
338
     *
339
     * - `Field.<FieldHandler>.Entity.afterSave`: Will be triggered after a
340
     *   successful insert or save, listeners will receive two arguments, the field
341
     *   entity and the options array. The type of operation performed (insert or
342
     *   update) can be infer by checking the field entity's method `isNew`, true
343
     *   meaning an insert and false an update.
344
     *
345
     * @param \Cake\Event\Event $event The event that was triggered
346
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
347
     * @param \ArrayObject $options Additional options given as an array
348
     * @return bool True always
349
     */
350
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
351
    {
352
        if (!$this->config('enabled')) {
353
            return true;
354
        }
355
356
        // as we don't know entity's ID on beforeSave, we must delay values storage;
357
        // all this occurs inside a transaction so we are safe
358
        if (!empty($this->_cache['createValues'])) {
359
            foreach ($this->_cache['createValues'] as $valueEntity) {
360
                $valueEntity->set('entity_id', $this->_toolbox->getEntityId($entity));
361
                $valueEntity->unsetProperty('id');
362
                TableRegistry::get('Eav.EavValues')->save($valueEntity);
363
            }
364
            $this->_cache['createValues'] = [];
365
        }
366
367
        foreach ($this->_attributesForEntity($entity) as $attr) {
368
            $field = $this->_prepareMockField($entity, $attr);
369
            $field->afterSave();
370
        }
371
372
        if ($this->config('cacheMap')) {
373
            $this->updateEavCache($entity);
374
        }
375
376
        return true;
377
    }
378
379
    /**
380
     * Deletes an entity from a fieldable table.
381
     *
382
     * ### Events Triggered:
383
     *
384
     * - `Field.<FieldHandler>.Entity.beforeDelete`: Fired before the delete occurs.
385
     *    If stopped the delete will be aborted. Receives as arguments the field
386
     *    entity and options array.
387
     *
388
     * @param \Cake\Event\Event $event The event that was triggered
389
     * @param \Cake\Datasource\EntityInterface $entity The entity being deleted
390
     * @param \ArrayObject $options Additional options given as an array
391
     * @return bool
392
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
393
     */
394
    public function beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)
395
    {
396
        if (!$this->config('enabled')) {
397
            return true;
398
        }
399
400
        if (!$options['atomic']) {
401
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transaction. Set [atomic = true]'));
402
        }
403
404
        foreach ($this->_attributesForEntity($entity) as $attr) {
405
            $field = $this->_prepareMockField($entity, $attr);
406
            $result = $field->beforeDelete();
407
408
            if ($result === false) {
409
                $event->stopPropagation();
410
411
                return false;
412
            }
413
414
            // holds in cache field mocks, so we can catch them on afterDelete
415
            $this->_cache['afterDelete'][] = $field;
416
        }
417
418
        return true;
419
    }
420
421
    /**
422
     * After an entity was removed from database.
423
     *
424
     * ### Events Triggered:
425
     *
426
     * - `Field.<FieldHandler>.Entity.afterDelete`: Fired after the delete has been
427
     *    successful. Receives as arguments the field entity and options array.
428
     *
429
     * **NOTE:** This method automatically removes all field values from
430
     * `eav_values` database table for each entity.
431
     *
432
     * @param \Cake\Event\Event $event The event that was triggered
433
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
434
     * @param \ArrayObject $options Additional options given as an array
435
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
436
     * @return void
437
     */
438
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
439
    {
440
        if (!$this->config('enabled')) {
441
            return;
442
        }
443
444
        if (!$options['atomic']) {
445
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
446
        }
447
448
        if (!empty($this->_cache['afterDelete'])) {
449
            foreach ((array)$this->_cache['afterDelete'] as $field) {
450
                $field->afterDelete();
451
            }
452
            $this->_cache['afterDelete'] = [];
453
        }
454
455
        parent::afterDelete($event, $entity, $options);
456
    }
457
458
    /**
459
     * Changes behavior's configuration parameters on the fly.
460
     *
461
     * @param array $config Configuration parameters as `key` => `value`
462
     * @return void
463
     */
464
    public function configureFieldable($config)
465
    {
466
        $this->config($config);
467
    }
468
469
    /**
470
     * Enables this behavior.
471
     *
472
     * @return void
473
     */
474
    public function bindFieldable()
475
    {
476
        $this->config('enabled', true);
477
    }
478
479
    /**
480
     * Disables this behavior.
481
     *
482
     * @return void
483
     */
484
    public function unbindFieldable()
485
    {
486
        $this->config('enabled', false);
487
    }
488
489
    /**
490
     * The method which actually fetches custom fields.
491
     *
492
     * Fetches all Entity's fields under the `_fields` property.
493
     *
494
     * @param \Cake\Datasource\EntityInterface $entity The entity where to fetch fields
495
     * @return \Cake\Datasource\EntityInterface
496
     */
497
    public function attachEntityFields(EntityInterface $entity)
498
    {
499
        $_fields = [];
500
        foreach ($this->_attributesForEntity($entity) as $attr) {
501
            $field = $this->_prepareMockField($entity, $attr);
502
            if ($entity->has($field->get('name'))) {
503
                $this->_fetchPost($field);
504
            }
505
506
            $field->fieldAttached();
507
            $_fields[] = $field;
508
        }
509
510
        $entity->set('_fields', new FieldCollection($_fields));
511
512
        return $entity;
513
    }
514
515
    /**
516
     * Triggers before/after validate events.
517
     *
518
     * @param \Cake\Datasource\EntityInterface $entity The entity being validated
519
     * @return bool True if save operation should continue, false otherwise
520
     */
521
    protected function _validation(EntityInterface $entity)
522
    {
523
        $validator = new Validator();
524
        $hasErrors = false;
525
526
        foreach ($this->_attributesForEntity($entity) as $attr) {
527
            $field = $this->_prepareMockField($entity, $attr);
528
            $result = $field->validate($validator);
529
530
            if ($result === false) {
531
                $this->attachEntityFields($entity);
532
533
                return false;
534
            }
535
536
            $errors = $validator->errors($entity->toArray(), $entity->isNew());
537
            $entity->errors($errors);
538
539
            if (!empty($errors)) {
540
                $hasErrors = true;
541
                if ($entity->has('_fields')) {
542
                    $entityErrors = $entity->errors();
543
                    foreach ($entity->get('_fields') as $field) {
544
                        $postData = $entity->get($field->name);
545
                        if (!empty($entityErrors[$field->name])) {
546
                            $field->set('value', $postData);
547
                            $field->metadata->set('errors', (array)$entityErrors[$field->name]);
548
                        }
549
                    }
550
                }
551
            }
552
        }
553
554
        return !$hasErrors;
555
    }
556
557
    /**
558
     * Alters the given $field and fetches incoming POST data, both "value" and
559
     * "extra" property will be automatically filled for the given $field entity.
560
     *
561
     * @param \Field\Model\Entity\Field $field The field entity for which
562
     *  fetch POST information
563
     * @return mixed Raw POST information
564
     */
565
    protected function _fetchPost(Field $field)
566
    {
567
        $post = $field
568
            ->get('metadata')
569
            ->get('entity')
570
            ->get($field->get('name'));
571
572
        // auto-magic
573
        if (is_array($post)) {
574
            $field->set('extra', $post);
575
            $field->set('value', null);
576
        } else {
577
            $field->set('extra', null);
578
            $field->set('value', $post);
579
        }
580
581
        return $post;
582
    }
583
584
    /**
585
     * Gets all attributes that should be attached to the given entity, this entity
586
     * will be used as context to calculate the proper bundle.
587
     *
588
     * @param \Cake\Datasource\EntityInterface $entity Entity context
589
     * @return array
590
     */
591
    protected function _attributesForEntity(EntityInterface $entity)
592
    {
593
        $bundle = $this->_resolveBundle($entity);
594
        $attrs = $this->_toolbox->attributes($bundle);
595
        $attrByIds = []; // attrs indexed by id
596
        $attrByNames = []; // attrs indexed by name
597
598
        foreach ($attrs as $name => $attr) {
599
            $attrByNames[$name] = $attr;
600
            $attrByIds[$attr->get('id')] = $attr;
601
            $attr->set(':value', null);
602
        }
603
604
        if (!empty($attrByIds)) {
605
            $instances = $this->Attributes->Instance
606
                ->find()
607
                ->where(['eav_attribute_id IN' => array_keys($attrByIds)])
608
                ->all();
609
            foreach ($instances as $instance) {
610
                if (!empty($attrByIds[$instance->get('eav_attribute_id')])) {
611
                    $attr = $attrByIds[$instance->get('eav_attribute_id')];
612
                    if (!$attr->has('instance')) {
613
                        $attr->set('instance', $instance);
614
                    }
615
                }
616
            }
617
        }
618
619
        $values = $this->_fetchValues($entity, array_keys($attrByNames));
620
        foreach ($values as $value) {
621
            if (!empty($attrByNames[$value->get('eav_attribute')->get('name')])) {
622
                $attrByNames[$value->get('eav_attribute')->get('name')]->set(':value', $value);
623
            }
624
        }
625
626
        return $this->_toolbox->attributes($bundle);
627
    }
628
629
    /**
630
     * Retrives stored values for all virtual properties by name. This gets all
631
     * values at once.
632
     *
633
     * This method is used to reduce the number of SQl queries, so we get all
634
     * values at once in a single Select instead of creating a select for every
635
     * field attached to the given entity.
636
     *
637
     * @param \Cake\Datasource\EntityInterface $entity The entuity for which
638
     *  get related values
639
     * @param array $attrNames List of attribute names for which get their
640
     *  values
641
     * @return \Cake\Datasource\ResultSetInterface
642
     */
643
    protected function _fetchValues(EntityInterface $entity, array $attrNames = [])
644
    {
645
        $bundle = $this->_resolveBundle($entity);
646
        $conditions = [
647
            'EavAttribute.table_alias' => $this->_table->table(),
648
            'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
649
        ];
650
651
        if ($bundle) {
652
            $conditions['EavAttribute.bundle'] = $bundle;
653
        }
654
655
        if (!empty($attrNames)) {
656
            $conditions['EavAttribute.name IN'] = $attrNames;
657
        }
658
659
        $storedValues = TableRegistry::get('Eav.EavValues')
660
            ->find()
661
            ->contain(['EavAttribute'])
662
            ->where($conditions)
663
            ->all();
664
665
        return $storedValues;
666
    }
667
668
    /**
669
     * Creates a new Virtual "Field" to be attached to the given entity.
670
     *
671
     * This mock Field represents a new property (table column) of the entity.
672
     *
673
     * @param \Cake\Datasource\EntityInterface $entity The entity where the
674
     *  generated virtual field will be attached
675
     * @param \Cake\Datasource\EntityInterface $attribute The attribute where to get
676
     *  the information when creating the mock field.
677
     * @return \Field\Model\Entity\Field
678
     */
679
    protected function _prepareMockField(EntityInterface $entity, EntityInterface $attribute)
680
    {
681
        $type = $this->_toolbox->mapType($attribute->get('type'));
682
        if (!$attribute->has(':value')) {
683
            $bundle = $this->_resolveBundle($entity);
684
            $conditions = [
685
                'EavAttribute.table_alias' => $this->_table->table(),
686
                'EavAttribute.name' => $attribute->get('name'),
687
                'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
688
            ];
689
690
            if ($bundle) {
691
                $conditions['EavAttribute.bundle'] = $bundle;
692
            }
693
694
            $storedValue = TableRegistry::get('Eav.EavValues')
695
                ->find()
696
                ->contain(['EavAttribute'])
697
                ->select(['id', "value_{$type}", 'extra'])
698
                ->where($conditions)
699
                ->limit(1)
700
                ->first();
701
        } else {
702
            $storedValue = $attribute->get(':value');
703
        }
704
705
        $mockField = new Field([
706
            'name' => $attribute->get('name'),
707
            'label' => $attribute->get('instance')->get('label'),
708
            'value' => null,
709
            'extra' => null,
710
            'metadata' => new Entity([
711
                'value_id' => null,
712
                'instance_id' => $attribute->get('instance')->get('id'),
713
                'attribute_id' => $attribute->get('id'),
714
                'entity_id' => $this->_toolbox->getEntityId($entity),
715
                'table_alias' => $attribute->get('table_alias'),
716
                'type' => $type,
717
                'bundle' => $attribute->get('bundle'),
718
                'handler' => $attribute->get('instance')->get('handler'),
719
                'required' => $attribute->get('instance')->required,
720
                'description' => $attribute->get('instance')->description,
721
                'settings' => $attribute->get('instance')->settings,
722
                'view_modes' => $attribute->get('instance')->view_modes,
723
                'entity' => $entity,
724
                'errors' => [],
725
            ]),
726
        ]);
727
728
        if ($storedValue) {
729
            $mockField->set('value', $this->_toolbox->marshal($storedValue->get("value_{$type}"), $type));
730
            $mockField->set('extra', $storedValue->get('extra'));
731
            $mockField->metadata->set('value_id', $storedValue->id);
732
        }
733
734
        $mockField->isNew($entity->isNew());
735
736
        return $mockField;
737
    }
738
739
    /**
740
     * Resolves `bundle` name using $entity as context.
741
     *
742
     * @param \Cake\Datasource\EntityInterface $entity Entity to use as context when
743
     *  resolving bundle
744
     * @return string Bundle name as string value, it may be an empty string if no
745
     *  bundle should be applied
746
     */
747
    protected function _resolveBundle(EntityInterface $entity)
748
    {
749
        $bundle = $this->config('bundle');
750
        if (is_callable($bundle)) {
751
            $callable = $this->config('bundle');
752
            $bundle = $callable($entity);
753
        }
754
755
        return (string)$bundle;
756
    }
757
}
758