Completed
Push — 2.0 ( a506c7...300469 )
by Christopher
04:09
created

FieldableBehavior::_prepareMockField()   B

Complexity

Conditions 4
Paths 6

Size

Total Lines 59
Code Lines 45

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 4
eloc 45
c 3
b 1
f 0
nc 6
nop 2
dl 0
loc 59
rs 8.9846

How to fix   Long Method   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * Licensed under The GPL-3.0 License
4
 * For full copyright and license information, please see the LICENSE.txt
5
 * Redistributions of files must retain the above copyright notice.
6
 *
7
 * @since    2.0.0
8
 * @author   Christopher Castro <[email protected]>
9
 * @link     http://www.quickappscms.org
10
 * @license  http://opensource.org/licenses/gpl-3.0.html GPL-3.0 License
11
 */
12
namespace Field\Model\Behavior;
13
14
use Cake\Datasource\EntityInterface;
15
use Cake\Error\FatalErrorException;
16
use Cake\Event\Event;
17
use Cake\ORM\Behavior;
18
use Cake\ORM\Entity;
19
use Cake\ORM\Query;
20
use Cake\ORM\Table;
21
use Cake\ORM\TableRegistry;
22
use Cake\Validation\Validator;
23
use Eav\Model\Behavior\EavBehavior;
24
use Field\Collection\FieldCollection;
25
use Field\Model\Entity\Field;
26
use \ArrayObject;
27
28
/**
29
 * Fieldable Behavior.
30
 *
31
 * A more flexible EAV approach. Allows additional fields to be attached to Tables.
32
 * Any Table (Contents, Users, etc.) can use this behavior to make itself `fieldable`
33
 * and thus allow fields to be attached to it.
34
 *
35
 * The Field API defines two primary data structures, FieldInstance and FieldValue:
36
 *
37
 * - FieldInstance: is a Field attached to a single Table. (Schema equivalent: column)
38
 * - FieldValue: is the stored data for a particular [FieldInstance, Entity]
39
 *   tuple of your Table. (Schema equivalent: cell value)
40
 *
41
 * **This behavior allows you to add _virtual columns_ to your table schema.**
42
 *
43
 * @link https://github.com/quickapps/docs/blob/2.x/en/developers/field-api.rst
44
 */
45
class FieldableBehavior extends EavBehavior
46
{
47
48
    /**
49
     * Used for reduce BD queries and allow inter-method communication.
50
     * Example, it allows to pass some information from beforeDelete() to
51
     * afterDelete().
52
     *
53
     * @var array
54
     */
55
    protected $_cache = [];
56
57
    /**
58
     * Default configuration.
59
     *
60
     * These are merged with user-provided configuration when the behavior is used.
61
     * Available options are:
62
     *
63
     * - `bundle`: Bundle within this the table. Can be a string or a callable
64
     *   method that must return a string to use as bundle. Default null. If set to
65
     *   a callable function, it will receive the entity being saved as first
66
     *   argument, so you can calculate a bundle name for each particular entity.
67
     *
68
     * - `enabled`: True enables this behavior or false for disable. Default to
69
     *   true.
70
     *
71
     * - `cache`: Column-based cache. See EAV plugin's documentation.
72
     *
73
     * Bundles are usually set to dynamic values. For example, for the "contents"
74
     * table we have "content" entities, but we may have "article contents", "page
75
     * contents", etc. depending on the "type of content" they are; is said that
76
     * "article" and "page" **are bundles** of "contents" table.
77
     *
78
     * @var array
79
     */
80
    protected $_fieldableDefaultConfig = [
81
        'bundle' => null,
82
        'implementedMethods' => [
83
            'configureFieldable' => 'configureFieldable',
84
            'attachFields' => 'attachEntityFields',
85
            'unbindFieldable' => 'unbindFieldable',
86
            'bindFieldable' => 'bindFieldable',
87
        ],
88
    ];
89
90
    /**
91
     * Instance of EavAttributes table.
92
     *
93
     * @var \Eav\Model\Table\EavAttributesTable
94
     */
95
    public $Attributes = null;
96
97
    /**
98
     * Constructor.
99
     *
100
     * @param \Cake\ORM\Table $table The table this behavior is attached to
101
     * @param array $config Configuration array for this behavior
102
     */
103
    public function __construct(Table $table, array $config = [])
104
    {
105
        $this->_defaultConfig = array_merge($this->_defaultConfig, $this->_fieldableDefaultConfig);
106
        $this->Attributes = TableRegistry::get('Eav.EavAttributes');
107
        $this->Attributes->hasOne('Instance', [
108
            'className' => 'Field.FieldInstances',
109
            'foreignKey' => 'eav_attribute_id',
110
            'propertyName' => 'instance',
111
        ]);
112
        parent::__construct($table, $config);
113
    }
114
115
    /**
116
     * Returns a list of events this class is implementing. When the class is
117
     * registered in an event manager, each individual method will be associated
118
     * with the respective event.
119
     *
120
     * @return void
121
     */
122
    public function implementedEvents()
123
    {
124
        $events = [
125
            'Model.beforeFind' => ['callable' => 'beforeFind', 'priority' => 15],
126
            'Model.beforeSave' => ['callable' => 'beforeSave', 'priority' => 15],
127
            'Model.afterSave' => ['callable' => 'afterSave', 'priority' => 15],
128
            'Model.beforeDelete' => ['callable' => 'beforeDelete', 'priority' => 15],
129
            'Model.afterDelete' => ['callable' => 'afterDelete', 'priority' => 15],
130
        ];
131
132
        return $events;
133
    }
134
135
    /**
136
     * Modifies the query object in order to merge custom fields records
137
     * into each entity under the `_fields` property.
138
     *
139
     * ### Events Triggered:
140
     *
141
     * - `Field.<FieldHandler>.Entity.beforeFind`: This event is triggered for each
142
     *    entity in the resulting collection and for each field attached to these
143
     *    entities. It receives three arguments, a field entity representing the
144
     *    field being processed, an options array and boolean value indicating
145
     *    whether the query that initialized the event is part of a primary find
146
     *    operation or not. Returning false will cause the entity to be removed from
147
     *    the resulting collection, also will stop event propagation, so other
148
     *    fields won't be able to listen this event. If the event is stopped using
149
     *    the event API, will halt the entire find operation.
150
     *
151
     * You can enable or disable this behavior for a single `find()` or `get()`
152
     * operation by setting `fieldable` or `eav` to false in the options array for
153
     * find method. e.g.:
154
     *
155
     * ```php
156
     * $contents = $this->Contents->find('all', ['fieldable' => false]);
157
     * $content = $this->Contents->get($id, ['fieldable' => false]);
158
     * ```
159
     *
160
     * It also looks for custom fields in WHERE clause. This will search entities in
161
     * all bundles this table may have, if you need to restrict the search to an
162
     * specific bundle you must use the `bundle` key in find()'s options:
163
     *
164
     * ```php
165
     * $this->Contents
166
     *     ->find('all', ['bundle' => 'articles'])
167
     *     ->where(['article-title' => 'My first article!']);
168
     * ```
169
     *
170
     * The `bundle` option has no effects if no custom fields are given in the
171
     * WHERE clause.
172
     *
173
     * @param \Cake\Event\Event $event The beforeFind event that was triggered
174
     * @param \Cake\ORM\Query $query The original query to modify
175
     * @param \ArrayObject $options Additional options given as an array
176
     * @param bool $primary Whether this find is a primary query or not
177
     * @return void
178
     */
179
    public function beforeFind(Event $event, Query $query, ArrayObject $options, $primary)
180
    {
181 View Code Duplication
        if ((isset($options['fieldable']) && $options['fieldable'] === false) ||
182
            !$this->config('enabled')
183
        ) {
184
            return true;
185
        }
186
187
        return parent::beforeFind($event, $query, $options, $primary);
188
    }
189
190
    /**
191
     * {@inheritDoc}
192
     *
193
     * Attaches entity's field under the `_fields` property, this method is invoked
194
     * by `beforeFind()` when iterating results sets.
195
     *
196
     * When `bundle` option is used the Entity will be removed from the collection
197
     * if it does not belongs to that bundle.
198
     */
199
    public function attachEntityAttributes(EntityInterface $entity, array $options = [])
200
    {
201
        $entityBundle = $this->_resolveBundle($entity);
202
        if (!empty($options['bundle']) && $options['bundle'] !== $entityBundle) {
203
            return false;
204
        }
205
206
        $entity = $this->attachEntityFields($entity);
207
        foreach ($entity->get('_fields') as $field) {
208
            $result = $field->beforeFind((array)$options['options'], $options['primary']);
209
            if ($result === null) {
210
                return null; // remove entity from collection
211
            } elseif ($result === false) {
212
                return false; // abort find() operation
213
            }
214
        }
215
216
        return $entity;
217
    }
218
219
    /**
220
     * Before an entity is saved.
221
     *
222
     * Here is where we dispatch each custom field's `$_POST` information to its
223
     * corresponding Field Handler, so they can operate over their values.
224
     *
225
     * Fields Handler's `beforeSave()` method is automatically invoked for each
226
     * attached field for the entity being processed, your field handler should look
227
     * as follow:
228
     *
229
     * ```php
230
     * use Field\Handler;
231
     *
232
     * class TextField extends Handler
233
     * {
234
     *     public function beforeSave(Field $field, $post)
235
     *     {
236
     *          // alter $field, and do nifty things with $post
237
     *          // return FALSE; will halt the operation
238
     *     }
239
     * }
240
     * ```
241
     *
242
     * Field Handlers should **alter** `$field->value` and `$field->extra` according
243
     * to its needs using the provided **$post** argument.
244
     *
245
     * **NOTE:** Returning boolean FALSE will halt the whole Entity's save operation.
246
     *
247
     * @param \Cake\Event\Event $event The event that was triggered
248
     * @param \Cake\Datasource\EntityInterface $entity The entity being saved
249
     * @param \ArrayObject $options Additional options given as an array
250
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
251
     * @return bool True if save operation should continue
252
     */
253
    public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
254
    {
255
        if (!$this->config('enabled')) {
256
            return true;
257
        }
258
259
        if (!$options['atomic']) {
260
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be saved using transaction. Set [atomic = true]'));
261
        }
262
263
        if (!$this->_validation($entity)) {
264
            return false;
265
        }
266
267
        $this->_cache['createValues'] = [];
268
        foreach ($this->_attributesForEntity($entity) as $attr) {
269
            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...
270
                continue;
271
            }
272
273
            $field = $this->_prepareMockField($entity, $attr);
274
            $result = $field->beforeSave($this->_fetchPost($field));
275
276
            if ($result === false) {
277
                $this->attachEntityFields($entity);
278
279
                return false;
280
            }
281
282
            $data = [
283
                'eav_attribute_id' => $field->get('metadata')->get('attribute_id'),
284
                'entity_id' => $this->_toolbox->getEntityId($entity),
285
                "value_{$field->metadata['type']}" => $field->get('value'),
286
                'extra' => $field->get('extra'),
287
            ];
288
289
            if ($field->get('metadata')->get('value_id')) {
290
                $valueEntity = TableRegistry::get('Eav.EavValues')->get($field->get('metadata')->get('value_id'));
291
                $valueEntity = TableRegistry::get('Eav.EavValues')->patchEntity($valueEntity, $data, ['validate' => false]);
292
            } else {
293
                $valueEntity = TableRegistry::get('Eav.EavValues')->newEntity($data, ['validate' => false]);
294
            }
295
296
            if ($entity->isNew() || $valueEntity->isNew()) {
297
                $this->_cache['createValues'][] = $valueEntity;
298
            } elseif (!TableRegistry::get('Eav.EavValues')->save($valueEntity)) {
299
                $this->attachEntityFields($entity);
300
                $event->stopPropagation();
301
302
                return false;
303
            }
304
        }
305
306
        $this->attachEntityFields($entity);
307
308
        return true;
309
    }
310
311
    /**
312
     * After an entity is saved.
313
     *
314
     * ### Events Triggered:
315
     *
316
     * - `Field.<FieldHandler>.Entity.afterSave`: Will be triggered after a
317
     *   successful insert or save, listeners will receive two arguments, the field
318
     *   entity and the options array. The type of operation performed (insert or
319
     *   update) can be infer by checking the field entity's method `isNew`, true
320
     *   meaning an insert and false an update.
321
     *
322
     * @param \Cake\Event\Event $event The event that was triggered
323
     * @param \Cake\Datasource\EntityInterface $entity The entity that was saved
324
     * @param \ArrayObject $options Additional options given as an array
325
     * @return bool True always
326
     */
327
    public function afterSave(Event $event, EntityInterface $entity, ArrayObject $options)
328
    {
329
        if (!$this->config('enabled')) {
330
            return true;
331
        }
332
333
        // as we don't know entity's ID on beforeSave, we must delay values storage;
334
        // all this occurs inside a transaction so we are safe
335
        if (!empty($this->_cache['createValues'])) {
336
            foreach ($this->_cache['createValues'] as $valueEntity) {
337
                $valueEntity->set('entity_id', $this->_toolbox->getEntityId($entity));
338
                $valueEntity->unsetProperty('id');
339
                TableRegistry::get('Eav.EavValues')->save($valueEntity);
340
            }
341
            $this->_cache['createValues'] = [];
342
        }
343
344
        foreach ($this->_attributesForEntity($entity) as $attr) {
345
            $field = $this->_prepareMockField($entity, $attr);
346
            $field->afterSave();
347
        }
348
349
        if ($this->config('cacheMap')) {
350
            $this->updateEavCache($entity);
351
        }
352
353
        return true;
354
    }
355
356
    /**
357
     * Deletes an entity from a fieldable table.
358
     *
359
     * ### Events Triggered:
360
     *
361
     * - `Field.<FieldHandler>.Entity.beforeDelete`: Fired before the delete occurs.
362
     *    If stopped the delete will be aborted. Receives as arguments the field
363
     *    entity and options array.
364
     *
365
     * @param \Cake\Event\Event $event The event that was triggered
366
     * @param \Cake\Datasource\EntityInterface $entity The entity being deleted
367
     * @param \ArrayObject $options Additional options given as an array
368
     * @return bool
369
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
370
     */
371
    public function beforeDelete(Event $event, EntityInterface $entity, ArrayObject $options)
372
    {
373
        if (!$this->config('enabled')) {
374
            return true;
375
        }
376
377
        if (!$options['atomic']) {
378
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transaction. Set [atomic = true]'));
379
        }
380
381
        foreach ($this->_attributesForEntity($entity) as $attr) {
382
            $field = $this->_prepareMockField($entity, $attr);
383
            $result = $field->beforeDelete();
384
385
            if ($result === false) {
386
                $event->stopPropagation();
387
388
                return false;
389
            }
390
391
            // holds in cache field mocks, so we can catch them on afterDelete
392
            $this->_cache['afterDelete'][] = $field;
393
        }
394
395
        return true;
396
    }
397
398
    /**
399
     * After an entity was removed from database.
400
     *
401
     * ### Events Triggered:
402
     *
403
     * - `Field.<FieldHandler>.Entity.afterDelete`: Fired after the delete has been
404
     *    successful. Receives as arguments the field entity and options array.
405
     *
406
     * **NOTE:** This method automatically removes all field values from
407
     * `eav_values` database table for each entity.
408
     *
409
     * @param \Cake\Event\Event $event The event that was triggered
410
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
411
     * @param \ArrayObject $options Additional options given as an array
412
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
413
     * @return void
414
     */
415
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
416
    {
417
        if (!$this->config('enabled')) {
418
            return;
419
        }
420
421
        if (!$options['atomic']) {
422
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
423
        }
424
425
        if (!empty($this->_cache['afterDelete'])) {
426
            foreach ((array)$this->_cache['afterDelete'] as $field) {
427
                $field->afterDelete();
428
            }
429
            $this->_cache['afterDelete'] = [];
430
        }
431
432
        parent::afterDelete($event, $entity, $options);
433
    }
434
435
    /**
436
     * Changes behavior's configuration parameters on the fly.
437
     *
438
     * @param array $config Configuration parameters as `key` => `value`
439
     * @return void
440
     */
441
    public function configureFieldable($config)
442
    {
443
        $this->config($config);
444
    }
445
446
    /**
447
     * Enables this behavior.
448
     *
449
     * @return void
450
     */
451
    public function bindFieldable()
452
    {
453
        $this->config('enabled', true);
454
    }
455
456
    /**
457
     * Disables this behavior.
458
     *
459
     * @return void
460
     */
461
    public function unbindFieldable()
462
    {
463
        $this->config('enabled', false);
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