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

FieldableBehavior::_validation()   C

Complexity

Conditions 7
Paths 7

Size

Total Lines 35
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 21
nc 7
nop 1
dl 0
loc 35
rs 6.7272
c 0
b 0
f 0
1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace 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
    protected function _hydrateEntities(CollectionInterface $entities, array $args)
200
    {
201
        return $entities->map(function ($entity) use ($args) {
202
            if ($entity instanceof EntityInterface) {
203
                $entity = $this->_prepareCachedColumns($entity);
204
                $entity = $this->_attachEntityFields($entity, $args);
205
            }
206
207
            if ($entity === null) {
208
                return self::NULL_ENTITY;
209
            }
210
211
            return $entity;
212
        })
213
        ->filter(function ($entity) {
214
            return $entity !== self::NULL_ENTITY;
215
        });
216
    }
217
218
    /**
219
     * Attaches entity's field under the `_fields` property, this method is invoked
220
     * by `beforeFind()` when iterating results sets.
221
     *
222
     * @param \Cake\Datasource\EntityInterface $entity The entity being altered
223
     * @param array $args Arguments given to the originating `beforeFind()`
224
     */
225
    protected function _attachEntityFields(EntityInterface $entity, array $args)
226
    {
227
        $entity = $this->attachEntityFields($entity);
228
        foreach ($entity->get('_fields') as $field) {
229
            $result = $field->beforeFind((array)$args['options'], $args['primary']);
230
            if ($result === null) {
231
                return null; // remove entity from collection
232
            } elseif ($result === false) {
233
                return false; // abort find() operation
234
            }
235
        }
236
237
        return $entity;
238
    }
239
240
    /**
241
     * Before an entity is saved.
242
     *
243
     * Here is where we dispatch each custom field's `$_POST` information to its
244
     * corresponding Field Handler, so they can operate over their values.
245
     *
246
     * Fields Handler's `beforeSave()` method is automatically invoked for each
247
     * attached field for the entity being processed, your field handler should look
248
     * as follow:
249
     *
250
     * ```php
251
     * use Field\Handler;
252
     *
253
     * class TextField extends Handler
254
     * {
255
     *     public function beforeSave(Field $field, $post)
256
     *     {
257
     *          // alter $field, and do nifty things with $post
258
     *          // return FALSE; will halt the operation
259
     *     }
260
     * }
261
     * ```
262
     *
263
     * Field Handlers should **alter** `$field->value` and `$field->extra` according
264
     * to its needs using the provided **$post** argument.
265
     *
266
     * **NOTE:** Returning boolean FALSE will halt the whole Entity's save operation.
267
     *
268
     * @param \Cake\Event\Event $event The event that was triggered
269
     * @param \Cake\Datasource\EntityInterface $entity The entity being saved
270
     * @param \ArrayObject $options Additional options given as an array
271
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
272
     * @return bool True if save operation should continue
273
     */
274
    public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
275
    {
276
        if (!$this->config('enabled')) {
277
            return true;
278
        }
279
280
        if (!$options['atomic']) {
281
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be saved using transaction. Set [atomic = true]'));
282
        }
283
284
        if (!$this->_validation($entity)) {
285
            return false;
286
        }
287
288
        $this->_cache['createValues'] = [];
289
        foreach ($this->_attributesForEntity($entity) as $attr) {
290
            if (!$this->_toolbox->propertyExists($entity, $attr->get('name'))) {
291
                continue;
292
            }
293
294
            $field = $this->_prepareMockField($entity, $attr);
295
            $result = $field->beforeSave($this->_fetchPost($field));
296
297
            if ($result === false) {
298
                $this->attachEntityFields($entity);
299
300
                return false;
301
            }
302
303
            $data = [
304
                'eav_attribute_id' => $field->get('metadata')->get('attribute_id'),
305
                'entity_id' => $this->_toolbox->getEntityId($entity),
306
                "value_{$field->metadata['type']}" => $field->get('value'),
307
                'extra' => $field->get('extra'),
308
            ];
309
310
            if ($field->get('metadata')->get('value_id')) {
311
                $valueEntity = TableRegistry::get('Eav.EavValues')->get($field->get('metadata')->get('value_id'));
312
                $valueEntity = TableRegistry::get('Eav.EavValues')->patchEntity($valueEntity, $data, ['validate' => false]);
313
            } else {
314
                $valueEntity = TableRegistry::get('Eav.EavValues')->newEntity($data, ['validate' => false]);
315
            }
316
317
            if ($entity->isNew() || $valueEntity->isNew()) {
318
                $this->_cache['createValues'][] = $valueEntity;
319
            } elseif (!TableRegistry::get('Eav.EavValues')->save($valueEntity)) {
320
                $this->attachEntityFields($entity);
321
                $event->stopPropagation();
322
323
                return false;
324
            }
325
        }
326
327
        $this->attachEntityFields($entity);
328
329
        return true;
330
    }
331
332
    /**
333
     * After an entity is saved.
334
     *
335
     * ### Events Triggered:
336
     *
337
     * - `Field.<FieldHandler>.Entity.afterSave`: Will be triggered after a
338
     *   successful insert or save, listeners will receive two arguments, the field
339
     *   entity and the options array. The type of operation performed (insert or
340
     *   update) can be infer by checking the field entity's method `isNew`, true
341
     *   meaning an insert and false an update.
342
     *
343
     * @param \Cake\Event\Event $event The event that was triggered
344
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
345
     * @param \ArrayObject $options Additional options given as an array
346
     * @return bool True always
347
     */
348
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
349
    {
350
        if (!$this->config('enabled')) {
351
            return true;
352
        }
353
354
        // as we don't know entity's ID on beforeSave, we must delay values storage;
355
        // all this occurs inside a transaction so we are safe
356
        if (!empty($this->_cache['createValues'])) {
357
            foreach ($this->_cache['createValues'] as $valueEntity) {
358
                $valueEntity->set('entity_id', $this->_toolbox->getEntityId($entity));
359
                $valueEntity->unsetProperty('id');
360
                TableRegistry::get('Eav.EavValues')->save($valueEntity);
361
            }
362
            $this->_cache['createValues'] = [];
363
        }
364
365
        foreach ($this->_attributesForEntity($entity) as $attr) {
366
            $field = $this->_prepareMockField($entity, $attr);
367
            $field->afterSave();
368
        }
369
370
        if ($this->config('cacheMap')) {
371
            $this->updateEavCache($entity);
372
        }
373
374
        return true;
375
    }
376
377
    /**
378
     * Deletes an entity from a fieldable table.
379
     *
380
     * ### Events Triggered:
381
     *
382
     * - `Field.<FieldHandler>.Entity.beforeDelete`: Fired before the delete occurs.
383
     *    If stopped the delete will be aborted. Receives as arguments the field
384
     *    entity and options array.
385
     *
386
     * @param \Cake\Event\Event $event The event that was triggered
387
     * @param \Cake\Datasource\EntityInterface $entity The entity being deleted
388
     * @param \ArrayObject $options Additional options given as an array
389
     * @return bool
390
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
391
     */
392
    public function beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)
393
    {
394
        if (!$this->config('enabled')) {
395
            return true;
396
        }
397
398
        if (!$options['atomic']) {
399
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transaction. Set [atomic = true]'));
400
        }
401
402
        foreach ($this->_attributesForEntity($entity) as $attr) {
403
            $field = $this->_prepareMockField($entity, $attr);
404
            $result = $field->beforeDelete();
405
406
            if ($result === false) {
407
                $event->stopPropagation();
408
409
                return false;
410
            }
411
412
            // holds in cache field mocks, so we can catch them on afterDelete
413
            $this->_cache['afterDelete'][] = $field;
414
        }
415
416
        return true;
417
    }
418
419
    /**
420
     * After an entity was removed from database.
421
     *
422
     * ### Events Triggered:
423
     *
424
     * - `Field.<FieldHandler>.Entity.afterDelete`: Fired after the delete has been
425
     *    successful. Receives as arguments the field entity and options array.
426
     *
427
     * **NOTE:** This method automatically removes all field values from
428
     * `eav_values` database table for each entity.
429
     *
430
     * @param \Cake\Event\Event $event The event that was triggered
431
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
432
     * @param \ArrayObject $options Additional options given as an array
433
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
434
     * @return void
435
     */
436
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
437
    {
438
        if (!$this->config('enabled')) {
439
            return;
440
        }
441
442
        if (!$options['atomic']) {
443
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
444
        }
445
446
        if (!empty($this->_cache['afterDelete'])) {
447
            foreach ((array)$this->_cache['afterDelete'] as $field) {
448
                $field->afterDelete();
449
            }
450
            $this->_cache['afterDelete'] = [];
451
        }
452
453
        parent::afterDelete($event, $entity, $options);
454
    }
455
456
    /**
457
     * Changes behavior's configuration parameters on the fly.
458
     *
459
     * @param array $config Configuration parameters as `key` => `value`
460
     * @return void
461
     */
462
    public function configureFieldable($config)
463
    {
464
        $this->config($config);
465
    }
466
467
    /**
468
     * Enables this behavior.
469
     *
470
     * @return void
471
     */
472
    public function bindFieldable()
473
    {
474
        $this->config('enabled', true);
475
    }
476
477
    /**
478
     * Disables this behavior.
479
     *
480
     * @return void
481
     */
482
    public function unbindFieldable()
483
    {
484
        $this->config('enabled', false);
485
    }
486
487
    /**
488
     * The method which actually fetches custom fields.
489
     *
490
     * Fetches all Entity's fields under the `_fields` property.
491
     *
492
     * @param \Cake\Datasource\EntityInterface $entity The entity where to fetch fields
493
     * @return \Cake\Datasource\EntityInterface
494
     */
495
    public function attachEntityFields(EntityInterface $entity)
496
    {
497
        $_fields = [];
498
        foreach ($this->_attributesForEntity($entity) as $attr) {
499
            $field = $this->_prepareMockField($entity, $attr);
500
            if ($entity->has($field->get('name'))) {
501
                $this->_fetchPost($field);
502
            }
503
504
            $field->fieldAttached();
505
            $_fields[] = $field;
506
        }
507
508
        $entity->set('_fields', new FieldCollection($_fields));
509
510
        return $entity;
511
    }
512
513
    /**
514
     * Triggers before/after validate events.
515
     *
516
     * @param \Cake\Datasource\EntityInterface $entity The entity being validated
517
     * @return bool True if save operation should continue, false otherwise
518
     */
519
    protected function _validation(EntityInterface $entity)
520
    {
521
        $validator = new Validator();
522
        $hasErrors = false;
523
524
        foreach ($this->_attributesForEntity($entity) as $attr) {
525
            $field = $this->_prepareMockField($entity, $attr);
526
            $result = $field->validate($validator);
527
528
            if ($result === false) {
529
                $this->attachEntityFields($entity);
530
531
                return false;
532
            }
533
534
            $errors = $validator->errors($entity->toArray(), $entity->isNew());
535
            $entity->errors($errors);
536
537
            if (!empty($errors)) {
538
                $hasErrors = true;
539
                if ($entity->has('_fields')) {
540
                    $entityErrors = $entity->errors();
541
                    foreach ($entity->get('_fields') as $field) {
542
                        $postData = $entity->get($field->name);
543
                        if (!empty($entityErrors[$field->name])) {
544
                            $field->set('value', $postData);
545
                            $field->metadata->set('errors', (array)$entityErrors[$field->name]);
546
                        }
547
                    }
548
                }
549
            }
550
        }
551
552
        return !$hasErrors;
553
    }
554
555
    /**
556
     * Alters the given $field and fetches incoming POST data, both "value" and
557
     * "extra" property will be automatically filled for the given $field entity.
558
     *
559
     * @param \Field\Model\Entity\Field $field The field entity for which
560
     *  fetch POST information
561
     * @return mixed Raw POST information
562
     */
563
    protected function _fetchPost(Field $field)
564
    {
565
        $post = $field
566
            ->get('metadata')
567
            ->get('entity')
568
            ->get($field->get('name'));
569
570
        // auto-magic
571
        if (is_array($post)) {
572
            $field->set('extra', $post);
573
            $field->set('value', null);
574
        } else {
575
            $field->set('extra', null);
576
            $field->set('value', $post);
577
        }
578
579
        return $post;
580
    }
581
582
    /**
583
     * Gets all attributes that should be attached to the given entity, this entity
584
     * will be used as context to calculate the proper bundle.
585
     *
586
     * @param \Cake\Datasource\EntityInterface $entity Entity context
587
     * @return array
588
     */
589
    protected function _attributesForEntity(EntityInterface $entity)
590
    {
591
        $bundle = $this->_resolveBundle($entity);
592
        $attrs = $this->_toolbox->attributes($bundle);
593
        $attrByIds = []; // attrs indexed by id
594
        $attrByNames = []; // attrs indexed by name
595
596
        foreach ($attrs as $name => $attr) {
597
            $attrByNames[$name] = $attr;
598
            $attrByIds[$attr->get('id')] = $attr;
599
            $attr->set(':value', null);
600
        }
601
602
        if (!empty($attrByIds)) {
603
            $instances = $this->Attributes->Instance
604
                ->find()
605
                ->where(['eav_attribute_id IN' => array_keys($attrByIds)])
606
                ->all();
607
            foreach ($instances as $instance) {
608
                if (!empty($attrByIds[$instance->get('eav_attribute_id')])) {
609
                    $attr = $attrByIds[$instance->get('eav_attribute_id')];
610
                    if (!$attr->has('instance')) {
611
                        $attr->set('instance', $instance);
612
                    }
613
                }
614
            }
615
        }
616
617
        $values = $this->_fetchValues($entity, array_keys($attrByNames));
618
        foreach ($values as $value) {
619
            if (!empty($attrByNames[$value->get('eav_attribute')->get('name')])) {
620
                $attrByNames[$value->get('eav_attribute')->get('name')]->set(':value', $value);
621
            }
622
        }
623
624
        return $this->_toolbox->attributes($bundle);
625
    }
626
627
    /**
628
     * Retrives stored values for all virtual properties by name. This gets all
629
     * values at once.
630
     *
631
     * This method is used to reduce the number of SQl queries, so we get all
632
     * values at once in a single Select instead of creating a select for every
633
     * field attached to the given entity.
634
     *
635
     * @param \Cake\Datasource\EntityInterface $entity The entuity for which
636
     *  get related values
637
     * @param array $attrNames List of attribute names for which get their
638
     *  values
639
     * @return \Cake\Datasource\ResultSetInterface
640
     */
641
    protected function _fetchValues(EntityInterface $entity, array $attrNames = [])
642
    {
643
        $bundle = $this->_resolveBundle($entity);
644
        $conditions = [
645
            'EavAttribute.table_alias' => $this->_table->table(),
646
            'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
647
        ];
648
649
        if ($bundle) {
650
            $conditions['EavAttribute.bundle'] = $bundle;
651
        }
652
653
        if (!empty($attrNames)) {
654
            $conditions['EavAttribute.name IN'] = $attrNames;
655
        }
656
657
        $storedValues = TableRegistry::get('Eav.EavValues')
658
            ->find()
659
            ->contain(['EavAttribute'])
660
            ->where($conditions)
661
            ->all();
662
663
        return $storedValues;
664
    }
665
666
    /**
667
     * Creates a new Virtual "Field" to be attached to the given entity.
668
     *
669
     * This mock Field represents a new property (table column) of the entity.
670
     *
671
     * @param \Cake\Datasource\EntityInterface $entity The entity where the
672
     *  generated virtual field will be attached
673
     * @param \Cake\Datasource\EntityInterface $attribute The attribute where to get
674
     *  the information when creating the mock field.
675
     * @return \Field\Model\Entity\Field
676
     */
677
    protected function _prepareMockField(EntityInterface $entity, EntityInterface $attribute)
678
    {
679
        $type = $this->_toolbox->mapType($attribute->get('type'));
680
        if (!$attribute->has(':value')) {
681
            $bundle = $this->_resolveBundle($entity);
682
            $conditions = [
683
                'EavAttribute.table_alias' => $this->_table->table(),
684
                'EavAttribute.name' => $attribute->get('name'),
685
                'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
686
            ];
687
688
            if ($bundle) {
689
                $conditions['EavAttribute.bundle'] = $bundle;
690
            }
691
692
            $storedValue = TableRegistry::get('Eav.EavValues')
693
                ->find()
694
                ->contain(['EavAttribute'])
695
                ->select(['id', "value_{$type}", 'extra'])
696
                ->where($conditions)
697
                ->limit(1)
698
                ->first();
699
        } else {
700
            $storedValue = $attribute->get(':value');
701
        }
702
703
        $mockField = new Field([
704
            'name' => $attribute->get('name'),
705
            'label' => $attribute->get('instance')->get('label'),
706
            'value' => null,
707
            'extra' => null,
708
            'metadata' => new Entity([
709
                'value_id' => null,
710
                'instance_id' => $attribute->get('instance')->get('id'),
711
                'attribute_id' => $attribute->get('id'),
712
                'entity_id' => $this->_toolbox->getEntityId($entity),
713
                'table_alias' => $attribute->get('table_alias'),
714
                'type' => $type,
715
                'bundle' => $attribute->get('bundle'),
716
                'handler' => $attribute->get('instance')->get('handler'),
717
                'required' => $attribute->get('instance')->required,
718
                'description' => $attribute->get('instance')->description,
719
                'settings' => $attribute->get('instance')->settings,
720
                'view_modes' => $attribute->get('instance')->view_modes,
721
                'entity' => $entity,
722
                'errors' => [],
723
            ]),
724
        ]);
725
726
        if ($storedValue) {
727
            $mockField->set('value', $this->_toolbox->marshal($storedValue->get("value_{$type}"), $type));
728
            $mockField->set('extra', $storedValue->get('extra'));
729
            $mockField->metadata->set('value_id', $storedValue->id);
730
        }
731
732
        $mockField->isNew($entity->isNew());
733
734
        return $mockField;
735
    }
736
737
    /**
738
     * Resolves `bundle` name using $entity as context.
739
     *
740
     * @param \Cake\Datasource\EntityInterface $entity Entity to use as context when
741
     *  resolving bundle
742
     * @return string Bundle name as string value, it may be an empty string if no
743
     *  bundle should be applied
744
     */
745
    protected function _resolveBundle(EntityInterface $entity)
746
    {
747
        $bundle = $this->config('bundle');
748
        if (is_callable($bundle)) {
749
            $callable = $this->config('bundle');
750
            $bundle = $callable($entity);
751
        }
752
753
        return (string)$bundle;
754
    }
755
}
756