Completed
Push — 2.0 ( 321c43...0b0e6b )
by Christopher
03:34
created

FieldableBehavior::_resolveBundle()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 1
dl 0
loc 10
rs 9.4285
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
            'attachFields' => 'attachEntityFields',
85
            'fieldable' => 'fieldable',
86
        ],
87
    ];
88
89
    /**
90
     * Instance of EavAttributes table.
91
     *
92
     * @var \Eav\Model\Table\EavAttributesTable
93
     */
94
    public $Attributes = null;
95
96
    /**
97
     * Constructor.
98
     *
99
     * @param \Cake\ORM\Table $table The table this behavior is attached to
100
     * @param array $config Configuration array for this behavior
101
     */
102
    public function __construct(Table $table, array $config = [])
103
    {
104
        $this->_defaultConfig = array_merge($this->_defaultConfig, $this->_fieldableDefaultConfig);
105
        $this->Attributes = TableRegistry::get('Eav.EavAttributes');
106
        $this->Attributes->hasOne('Instance', [
107
            'className' => 'Field.FieldInstances',
108
            'foreignKey' => 'eav_attribute_id',
109
            'propertyName' => 'instance',
110
        ]);
111
        parent::__construct($table, $config);
112
    }
113
114
    /**
115
     * Returns a list of events this class is implementing. When the class is
116
     * registered in an event manager, each individual method will be associated
117
     * with the respective event.
118
     *
119
     * @return void
120
     */
121
    public function implementedEvents()
122
    {
123
        $events = [
124
            'Model.beforeFind' => ['callable' => 'beforeFind', 'priority' => 15],
125
            'Model.beforeSave' => ['callable' => 'beforeSave', 'priority' => 15],
126
            'Model.afterSave' => ['callable' => 'afterSave', 'priority' => 15],
127
            'Model.beforeDelete' => ['callable' => 'beforeDelete', 'priority' => 15],
128
            'Model.afterDelete' => ['callable' => 'afterDelete', 'priority' => 15],
129
        ];
130
131
        return $events;
132
    }
133
134
    /**
135
     * Modifies the query object in order to merge custom fields records
136
     * into each entity under the `_fields` property.
137
     *
138
     * ### Events Triggered:
139
     *
140
     * - `Field.<FieldHandler>.Entity.beforeFind`: This event is triggered for each
141
     *    entity in the resulting collection and for each field attached to these
142
     *    entities. It receives three arguments, a field entity representing the
143
     *    field being processed, an options array and boolean value indicating
144
     *    whether the query that initialized the event is part of a primary find
145
     *    operation or not. Returning false will cause the entity to be removed from
146
     *    the resulting collection, also will stop event propagation, so other
147
     *    fields won't be able to listen this event. If the event is stopped using
148
     *    the event API, will halt the entire find operation.
149
     *
150
     * You can enable or disable this behavior for a single `find()` or `get()`
151
     * operation by setting `fieldable` or `eav` to false in the options array for
152
     * find method. e.g.:
153
     *
154
     * ```php
155
     * $contents = $this->Contents->find('all', ['fieldable' => false]);
156
     * $content = $this->Contents->get($id, ['fieldable' => false]);
157
     * ```
158
     *
159
     * It also looks for custom fields in WHERE clause. This will search entities in
160
     * all bundles this table may have, if you need to restrict the search to an
161
     * specific bundle you must use the `bundle` key in find()'s options:
162
     *
163
     * ```php
164
     * $this->Contents
165
     *     ->find('all', ['bundle' => 'articles'])
166
     *     ->where(['article-title' => 'My first article!']);
167
     * ```
168
     *
169
     * The `bundle` option has no effects if no custom fields are given in the
170
     * WHERE clause.
171
     *
172
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
173
     * @param \Cake\ORM\Query $query The original query to modify
174
     * @param \ArrayObject $options Additional options given as an array
175
     * @param bool $primary Whether this find is a primary query or not
176
     * @return void
177
     */
178
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
179
    {
180 View Code Duplication
        if ((isset($options['fieldable']) && $options['fieldable'] === false) ||
181
            !$this->config('status')
182
        ) {
183
            return true;
184
        }
185
186
        if (array_key_exists('eav', $options)) {
187
            unset($options['eav']);
188
        }
189
190
        return parent::beforeFind($event, $query, $options, $primary);
191
    }
192
193
194
    /**
195
     * {@inheritDoc}
196
     */
197
    protected function _hydrateEntities(CollectionInterface $entities, array $args)
198
    {
199
        return $entities->map(function ($entity) use ($args) {
200
            if ($entity instanceof EntityInterface) {
201
                $entity = $this->_prepareCachedColumns($entity);
202
                $entity = $this->_attachEntityFields($entity, $args);
203
204
                if ($entity === null) {
205
                    return self::NULL_ENTITY;
206
                }
207
            }
208
209
            return $entity;
210
        })
211
        ->filter(function ($entity) {
212
            return $entity !== self::NULL_ENTITY;
213
        });
214
    }
215
216
    /**
217
     * Attaches entity's field under the `_fields` property, this method is invoked
218
     * by `beforeFind()` when iterating results sets.
219
     *
220
     * @param \Cake\Datasource\EntityInterface $entity The entity being altered
221
     * @param array $args Arguments given to the originating `beforeFind()`
222
     */
223
    protected function _attachEntityFields(EntityInterface $entity, array $args)
224
    {
225
        $entity = $this->attachEntityFields($entity);
226
        foreach ($entity->get('_fields') as $field) {
227
            $result = $field->beforeFind((array)$args['options'], $args['primary']);
228
            if ($result === null) {
229
                return null; // remove entity from collection
230
            } elseif ($result === false) {
231
                return false; // abort find() operation
232
            }
233
        }
234
235
        return $entity;
236
    }
237
238
    /**
239
     * Before an entity is saved.
240
     *
241
     * Here is where we dispatch each custom field's `$_POST` information to its
242
     * corresponding Field Handler, so they can operate over their values.
243
     *
244
     * Fields Handler's `beforeSave()` method is automatically invoked for each
245
     * attached field for the entity being processed, your field handler should look
246
     * as follow:
247
     *
248
     * ```php
249
     * use Field\Handler;
250
     *
251
     * class TextField extends Handler
252
     * {
253
     *     public function beforeSave(Field $field, $post)
254
     *     {
255
     *          // alter $field, and do nifty things with $post
256
     *          // return FALSE; will halt the operation
257
     *     }
258
     * }
259
     * ```
260
     *
261
     * Field Handlers should **alter** `$field->value` and `$field->extra` according
262
     * to its needs using the provided **$post** argument.
263
     *
264
     * **NOTE:** Returning boolean FALSE will halt the whole Entity's save operation.
265
     *
266
     * @param \Cake\Event\Event $event The event that was triggered
267
     * @param \Cake\Datasource\EntityInterface $entity The entity being saved
268
     * @param \ArrayObject $options Additional options given as an array
269
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
270
     * @return bool True if save operation should continue
271
     */
272
    public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
273
    {
274
        if (!$this->config('status')) {
275
            return true;
276
        }
277
278
        if (!$options['atomic']) {
279
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be saved using transaction. Set [atomic = true]'));
280
        }
281
282
        if (!$this->_validation($entity)) {
283
            return false;
284
        }
285
286
        $this->_cache['createValues'] = [];
287
        foreach ($this->_attributesForEntity($entity) as $attr) {
288
            if (!$this->_toolbox->propertyExists($entity, $attr->get('name'))) {
289
                continue;
290
            }
291
292
            $field = $this->_prepareMockField($entity, $attr);
293
            $result = $field->beforeSave($this->_fetchPost($field));
294
295
            if ($result === false) {
296
                $this->attachEntityFields($entity);
297
298
                return false;
299
            }
300
301
            $data = [
302
                'eav_attribute_id' => $field->get('metadata')->get('attribute_id'),
303
                'entity_id' => $this->_toolbox->getEntityId($entity),
304
                "value_{$field->metadata['type']}" => $field->get('value'),
305
                'extra' => $field->get('extra'),
306
            ];
307
308
            if ($field->get('metadata')->get('value_id')) {
309
                $valueEntity = TableRegistry::get('Eav.EavValues')->get($field->get('metadata')->get('value_id'));
310
                $valueEntity = TableRegistry::get('Eav.EavValues')->patchEntity($valueEntity, $data, ['validate' => false]);
311
            } else {
312
                $valueEntity = TableRegistry::get('Eav.EavValues')->newEntity($data, ['validate' => false]);
313
            }
314
315
            if ($entity->isNew() || $valueEntity->isNew()) {
316
                $this->_cache['createValues'][] = $valueEntity;
317
            } elseif (!TableRegistry::get('Eav.EavValues')->save($valueEntity)) {
318
                $this->attachEntityFields($entity);
319
                $event->stopPropagation();
320
321
                return false;
322
            }
323
        }
324
325
        $this->attachEntityFields($entity);
326
327
        return true;
328
    }
329
330
    /**
331
     * After an entity is saved.
332
     *
333
     * ### Events Triggered:
334
     *
335
     * - `Field.<FieldHandler>.Entity.afterSave`: Will be triggered after a
336
     *   successful insert or save, listeners will receive two arguments, the field
337
     *   entity and the options array. The type of operation performed (insert or
338
     *   update) can be infer by checking the field entity's method `isNew`, true
339
     *   meaning an insert and false an update.
340
     *
341
     * @param \Cake\Event\Event $event The event that was triggered
342
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
343
     * @param \ArrayObject $options Additional options given as an array
344
     * @return bool True always
345
     */
346
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
347
    {
348
        if (!$this->config('status')) {
349
            return true;
350
        }
351
352
        // as we don't know entity's ID on beforeSave, we must delay values storage;
353
        // all this occurs inside a transaction so we are safe
354
        if (!empty($this->_cache['createValues'])) {
355
            foreach ($this->_cache['createValues'] as $valueEntity) {
356
                $valueEntity->set('entity_id', $this->_toolbox->getEntityId($entity));
357
                $valueEntity->unsetProperty('id');
358
                TableRegistry::get('Eav.EavValues')->save($valueEntity);
359
            }
360
            $this->_cache['createValues'] = [];
361
        }
362
363
        foreach ($this->_attributesForEntity($entity) as $attr) {
364
            $field = $this->_prepareMockField($entity, $attr);
365
            $field->afterSave();
366
        }
367
368
        if ($this->config('cacheMap')) {
369
            $this->updateEavCache($entity);
370
        }
371
372
        return true;
373
    }
374
375
    /**
376
     * Deletes an entity from a fieldable table.
377
     *
378
     * ### Events Triggered:
379
     *
380
     * - `Field.<FieldHandler>.Entity.beforeDelete`: Fired before the delete occurs.
381
     *    If stopped the delete will be aborted. Receives as arguments the field
382
     *    entity and options array.
383
     *
384
     * @param \Cake\Event\Event $event The event that was triggered
385
     * @param \Cake\Datasource\EntityInterface $entity The entity being deleted
386
     * @param \ArrayObject $options Additional options given as an array
387
     * @return bool
388
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
389
     */
390
    public function beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)
391
    {
392
        if (!$this->config('status')) {
393
            return true;
394
        }
395
396
        if (!$options['atomic']) {
397
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transaction. Set [atomic = true]'));
398
        }
399
400
        foreach ($this->_attributesForEntity($entity) as $attr) {
401
            $field = $this->_prepareMockField($entity, $attr);
402
            $result = $field->beforeDelete();
403
404
            if ($result === false) {
405
                $event->stopPropagation();
406
407
                return false;
408
            }
409
410
            // holds in cache field mocks, so we can catch them on afterDelete
411
            $this->_cache['afterDelete'][] = $field;
412
        }
413
414
        return true;
415
    }
416
417
    /**
418
     * After an entity was removed from database.
419
     *
420
     * ### Events Triggered:
421
     *
422
     * - `Field.<FieldHandler>.Entity.afterDelete`: Fired after the delete has been
423
     *    successful. Receives as arguments the field entity and options array.
424
     *
425
     * **NOTE:** This method automatically removes all field values from
426
     * `eav_values` database table for each entity.
427
     *
428
     * @param \Cake\Event\Event $event The event that was triggered
429
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
430
     * @param \ArrayObject $options Additional options given as an array
431
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
432
     * @return void
433
     */
434
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
435
    {
436
        if (!$this->config('status')) {
437
            return;
438
        }
439
440
        if (!$options['atomic']) {
441
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
442
        }
443
444
        if (!empty($this->_cache['afterDelete'])) {
445
            foreach ((array)$this->_cache['afterDelete'] as $field) {
446
                $field->afterDelete();
447
            }
448
            $this->_cache['afterDelete'] = [];
449
        }
450
451
        parent::afterDelete($event, $entity, $options);
452
    }
453
454
    /**
455
     * Gets/sets fieldable behavior status.
456
     *
457
     * @param array|bool|null $status If set to a boolean value then turns on/off
458
     *  this behavior
459
     * @return bool|void
460
     */
461
    public function fieldable($status = null)
462
    {
463
        return $this->eav($status);
0 ignored issues
show
Bug introduced by
It seems like $status defined by parameter $status on line 461 can also be of type array; however, Eav\Model\Behavior\EavBehavior::eav() does only seem to accept boolean|null, maybe add an additional type check?

This check looks at variables that have been passed in as parameters and are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
464
    }
465
466
    /**
467
     * The method which actually fetches custom fields.
468
     *
469
     * Fetches all Entity's fields under the `_fields` property.
470
     *
471
     * @param \Cake\Datasource\EntityInterface $entity The entity where to fetch fields
472
     * @return \Cake\Datasource\EntityInterface
473
     */
474
    public function attachEntityFields(EntityInterface $entity)
475
    {
476
        $_fields = [];
477
        foreach ($this->_attributesForEntity($entity) as $attr) {
478
            $field = $this->_prepareMockField($entity, $attr);
479
            if ($entity->has($field->get('name'))) {
480
                $this->_fetchPost($field);
481
            }
482
483
            $field->fieldAttached();
484
            $_fields[] = $field;
485
        }
486
487
        $entity->set('_fields', new FieldCollection($_fields));
488
489
        return $entity;
490
    }
491
492
    /**
493
     * Triggers before/after validate events.
494
     *
495
     * @param \Cake\Datasource\EntityInterface $entity The entity being validated
496
     * @return bool True if save operation should continue, false otherwise
497
     */
498
    protected function _validation(EntityInterface $entity)
499
    {
500
        $validator = new Validator();
501
        $hasErrors = false;
502
503
        foreach ($this->_attributesForEntity($entity) as $attr) {
504
            $field = $this->_prepareMockField($entity, $attr);
505
            $result = $field->validate($validator);
506
507
            if ($result === false) {
508
                $this->attachEntityFields($entity);
509
510
                return false;
511
            }
512
513
            $errors = $validator->errors($entity->toArray(), $entity->isNew());
514
            $entity->errors($errors);
515
516
            if (!empty($errors)) {
517
                $hasErrors = true;
518
                if ($entity->has('_fields')) {
519
                    $entityErrors = $entity->errors();
520
                    foreach ($entity->get('_fields') as $field) {
521
                        $postData = $entity->get($field->name);
522
                        if (!empty($entityErrors[$field->name])) {
523
                            $field->set('value', $postData);
524
                            $field->metadata->set('errors', (array)$entityErrors[$field->name]);
525
                        }
526
                    }
527
                }
528
            }
529
        }
530
531
        return !$hasErrors;
532
    }
533
534
    /**
535
     * Alters the given $field and fetches incoming POST data, both "value" and
536
     * "extra" property will be automatically filled for the given $field entity.
537
     *
538
     * @param \Field\Model\Entity\Field $field The field entity for which
539
     *  fetch POST information
540
     * @return mixed Raw POST information
541
     */
542
    protected function _fetchPost(Field $field)
543
    {
544
        $post = $field
545
            ->get('metadata')
546
            ->get('entity')
547
            ->get($field->get('name'));
548
549
        // auto-magic
550
        if (is_array($post)) {
551
            $field->set('extra', $post);
552
            $field->set('value', null);
553
        } else {
554
            $field->set('extra', null);
555
            $field->set('value', $post);
556
        }
557
558
        return $post;
559
    }
560
561
    /**
562
     * Gets all attributes that should be attached to the given entity, this entity
563
     * will be used as context to calculate the proper bundle.
564
     *
565
     * @param \Cake\Datasource\EntityInterface $entity Entity context
566
     * @return array
567
     */
568
    protected function _attributesForEntity(EntityInterface $entity)
569
    {
570
        $bundle = $this->_resolveBundle($entity);
571
        $attrs = $this->_toolbox->attributes($bundle);
572
        $attrByIds = []; // attrs indexed by id
573
        $attrByNames = []; // attrs indexed by name
574
575
        foreach ($attrs as $name => $attr) {
576
            $attrByNames[$name] = $attr;
577
            $attrByIds[$attr->get('id')] = $attr;
578
            $attr->set(':value', null);
579
        }
580
581
        if (!empty($attrByIds)) {
582
            $instances = $this->Attributes->Instance
583
                ->find()
584
                ->where(['eav_attribute_id IN' => array_keys($attrByIds)])
585
                ->all();
586
            foreach ($instances as $instance) {
587
                if (!empty($attrByIds[$instance->get('eav_attribute_id')])) {
588
                    $attr = $attrByIds[$instance->get('eav_attribute_id')];
589
                    if (!$attr->has('instance')) {
590
                        $attr->set('instance', $instance);
591
                    }
592
                }
593
            }
594
        }
595
596
        $values = $this->_fetchValues($entity, array_keys($attrByNames));
597
        foreach ($values as $value) {
598
            if (!empty($attrByNames[$value->get('eav_attribute')->get('name')])) {
599
                $attrByNames[$value->get('eav_attribute')->get('name')]->set(':value', $value);
600
            }
601
        }
602
603
        return $this->_toolbox->attributes($bundle);
604
    }
605
606
    /**
607
     * Retrives stored values for all virtual properties by name. This gets all
608
     * values at once.
609
     *
610
     * This method is used to reduce the number of SQl queries, so we get all
611
     * values at once in a single Select instead of creating a select for every
612
     * field attached to the given entity.
613
     *
614
     * @param \Cake\Datasource\EntityInterface $entity The entuity for which
615
     *  get related values
616
     * @param array $attrNames List of attribute names for which get their
617
     *  values
618
     * @return \Cake\Datasource\ResultSetInterface
619
     */
620
    protected function _fetchValues(EntityInterface $entity, array $attrNames = [])
621
    {
622
        $bundle = $this->_resolveBundle($entity);
623
        $conditions = [
624
            'EavAttribute.table_alias' => $this->_table->table(),
625
            'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
626
        ];
627
628
        if ($bundle) {
629
            $conditions['EavAttribute.bundle'] = $bundle;
630
        }
631
632
        if (!empty($attrNames)) {
633
            $conditions['EavAttribute.name IN'] = $attrNames;
634
        }
635
636
        $storedValues = TableRegistry::get('Eav.EavValues')
637
            ->find()
638
            ->contain(['EavAttribute'])
639
            ->where($conditions)
640
            ->all();
641
642
        return $storedValues;
643
    }
644
645
    /**
646
     * Creates a new Virtual "Field" to be attached to the given entity.
647
     *
648
     * This mock Field represents a new property (table column) of the entity.
649
     *
650
     * @param \Cake\Datasource\EntityInterface $entity The entity where the
651
     *  generated virtual field will be attached
652
     * @param \Cake\Datasource\EntityInterface $attribute The attribute where to get
653
     *  the information when creating the mock field.
654
     * @return \Field\Model\Entity\Field
655
     */
656
    protected function _prepareMockField(EntityInterface $entity, EntityInterface $attribute)
657
    {
658
        $type = $this->_toolbox->mapType($attribute->get('type'));
659
        if (!$attribute->has(':value')) {
660
            $bundle = $this->_resolveBundle($entity);
661
            $conditions = [
662
                'EavAttribute.table_alias' => $this->_table->table(),
663
                'EavAttribute.name' => $attribute->get('name'),
664
                'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
665
            ];
666
667
            if ($bundle) {
668
                $conditions['EavAttribute.bundle'] = $bundle;
669
            }
670
671
            $storedValue = TableRegistry::get('Eav.EavValues')
672
                ->find()
673
                ->contain(['EavAttribute'])
674
                ->select(['id', "value_{$type}", 'extra'])
675
                ->where($conditions)
676
                ->limit(1)
677
                ->first();
678
        } else {
679
            $storedValue = $attribute->get(':value');
680
        }
681
682
        $mockField = new Field([
683
            'name' => $attribute->get('name'),
684
            'label' => $attribute->get('instance')->get('label'),
685
            'value' => null,
686
            'extra' => null,
687
            'metadata' => new Entity([
688
                'value_id' => null,
689
                'instance_id' => $attribute->get('instance')->get('id'),
690
                'attribute_id' => $attribute->get('id'),
691
                'entity_id' => $this->_toolbox->getEntityId($entity),
692
                'table_alias' => $attribute->get('table_alias'),
693
                'type' => $type,
694
                'bundle' => $attribute->get('bundle'),
695
                'handler' => $attribute->get('instance')->get('handler'),
696
                'required' => $attribute->get('instance')->required,
697
                'description' => $attribute->get('instance')->description,
698
                'settings' => $attribute->get('instance')->settings,
699
                'view_modes' => $attribute->get('instance')->view_modes,
700
                'entity' => $entity,
701
                'errors' => [],
702
            ]),
703
        ]);
704
705
        if ($storedValue) {
706
            $mockField->set('value', $this->_toolbox->marshal($storedValue->get("value_{$type}"), $type));
707
            $mockField->set('extra', $storedValue->get('extra'));
708
            $mockField->metadata->set('value_id', $storedValue->id);
709
        }
710
711
        $mockField->isNew($entity->isNew());
712
713
        return $mockField;
714
    }
715
716
    /**
717
     * Resolves `bundle` name using $entity as context.
718
     *
719
     * @param \Cake\Datasource\EntityInterface $entity Entity to use as context when
720
     *  resolving bundle
721
     * @return string Bundle name as string value, it may be an empty string if no
722
     *  bundle should be applied
723
     */
724
    protected function _resolveBundle(EntityInterface $entity)
725
    {
726
        $bundle = $this->config('bundle');
727
        if (is_callable($bundle)) {
728
            $callable = $this->config('bundle');
729
            $bundle = $callable($entity);
730
        }
731
732
        return (string)$bundle;
733
    }
734
}
735