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

FieldableBehavior::_prepareMockField()   A

Complexity

Conditions 4
Paths 6

Size

Total Lines 58
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 58
rs 9.0077

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\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
                return;
210
            }
211
212
            if ($entity === null) {
213
                return false;
214
            }
215
216
            return $entity;
217
        });
218
    }
219
220
    /**
221
     * {@inheritDoc}
222
     *
223
     * Attaches entity's field under the `_fields` property, this method is invoked
224
     * by `beforeFind()` when iterating results sets.
225
     */
226
    public function attachEntityAttributes(EntityInterface $entity, array $options)
227
    {
228
        $entity = $this->attachEntityFields($entity);
229
        foreach ($entity->get('_fields') as $field) {
230
            $result = $field->beforeFind((array)$options['options'], $options['primary']);
231
            if ($result === null) {
232
                return null; // remove entity from collection
233
            } elseif ($result === false) {
234
                return false; // abort find() operation
235
            }
236
        }
237
238
        return $entity;
239
    }
240
241
    /**
242
     * Before an entity is saved.
243
     *
244
     * Here is where we dispatch each custom field's `$_POST` information to its
245
     * corresponding Field Handler, so they can operate over their values.
246
     *
247
     * Fields Handler's `beforeSave()` method is automatically invoked for each
248
     * attached field for the entity being processed, your field handler should look
249
     * as follow:
250
     *
251
     * ```php
252
     * use Field\Handler;
253
     *
254
     * class TextField extends Handler
255
     * {
256
     *     public function beforeSave(Field $field, $post)
257
     *     {
258
     *          // alter $field, and do nifty things with $post
259
     *          // return FALSE; will halt the operation
260
     *     }
261
     * }
262
     * ```
263
     *
264
     * Field Handlers should **alter** `$field->value` and `$field->extra` according
265
     * to its needs using the provided **$post** argument.
266
     *
267
     * **NOTE:** Returning boolean FALSE will halt the whole Entity's save operation.
268
     *
269
     * @param \Cake\Event\Event $event The event that was triggered
270
     * @param \Cake\Datasource\EntityInterface $entity The entity being saved
271
     * @param \ArrayObject $options Additional options given as an array
272
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
273
     * @return bool True if save operation should continue
274
     */
275
    public function beforeSave(Event $event, EntityInterface $entity, ArrayObject $options)
276
    {
277
        if (!$this->config('enabled')) {
278
            return true;
279
        }
280
281
        if (!$options['atomic']) {
282
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be saved using transaction. Set [atomic = true]'));
283
        }
284
285
        if (!$this->_validation($entity)) {
286
            return false;
287
        }
288
289
        $this->_cache['createValues'] = [];
290
        foreach ($this->_attributesForEntity($entity) as $attr) {
291
            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...
292
                continue;
293
            }
294
295
            $field = $this->_prepareMockField($entity, $attr);
296
            $result = $field->beforeSave($this->_fetchPost($field));
297
298
            if ($result === false) {
299
                $this->attachEntityFields($entity);
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
                return false;
323
            }
324
        }
325
326
        $this->attachEntityFields($entity);
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('enabled')) {
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('enabled')) {
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
                return false;
407
            }
408
409
            // holds in cache field mocks, so we can catch them on afterDelete
410
            $this->_cache['afterDelete'][] = $field;
411
        }
412
413
        return true;
414
    }
415
416
    /**
417
     * After an entity was removed from database.
418
     *
419
     * ### Events Triggered:
420
     *
421
     * - `Field.<FieldHandler>.Entity.afterDelete`: Fired after the delete has been
422
     *    successful. Receives as arguments the field entity and options array.
423
     *
424
     * **NOTE:** This method automatically removes all field values from
425
     * `eav_values` database table for each entity.
426
     *
427
     * @param \Cake\Event\Event $event The event that was triggered
428
     * @param \Cake\Datasource\EntityInterface $entity The entity that was deleted
429
     * @param \ArrayObject $options Additional options given as an array
430
     * @throws \Cake\Error\FatalErrorException When using this behavior in non-atomic mode
431
     * @return void
432
     */
433
    public function afterDelete(Event $event, EntityInterface $entity, ArrayObject $options)
434
    {
435
        if (!$this->config('enabled')) {
436
            return;
437
        }
438
439
        if (!$options['atomic']) {
440
            throw new FatalErrorException(__d('field', 'Entities in fieldable tables can only be deleted using transactions. Set [atomic = true]'));
441
        }
442
443
        if (!empty($this->_cache['afterDelete'])) {
444
            foreach ((array)$this->_cache['afterDelete'] as $field) {
445
                $field->afterDelete();
446
            }
447
            $this->_cache['afterDelete'] = [];
448
        }
449
450
        parent::afterDelete($event, $entity, $options);
451
    }
452
453
    /**
454
     * Changes behavior's configuration parameters on the fly.
455
     *
456
     * @param array $config Configuration parameters as `key` => `value`
457
     * @return void
458
     */
459
    public function configureFieldable($config)
460
    {
461
        $this->config($config);
462
    }
463
464
    /**
465
     * Enables this behavior.
466
     *
467
     * @return void
468
     */
469
    public function bindFieldable()
470
    {
471
        $this->config('enabled', true);
472
    }
473
474
    /**
475
     * Disables this behavior.
476
     *
477
     * @return void
478
     */
479
    public function unbindFieldable()
480
    {
481
        $this->config('enabled', false);
482
    }
483
484
    /**
485
     * The method which actually fetches custom fields.
486
     *
487
     * Fetches all Entity's fields under the `_fields` property.
488
     *
489
     * @param \Cake\Datasource\EntityInterface $entity The entity where to fetch fields
490
     * @return \Cake\Datasource\EntityInterface
491
     */
492
    public function attachEntityFields(EntityInterface $entity)
493
    {
494
        $_fields = [];
495
        foreach ($this->_attributesForEntity($entity) as $attr) {
496
            $field = $this->_prepareMockField($entity, $attr);
497
            if ($entity->has($field->get('name'))) {
498
                $this->_fetchPost($field);
499
            }
500
501
            $field->fieldAttached();
502
            $_fields[] = $field;
503
        }
504
505
        $entity->set('_fields', new FieldCollection($_fields));
506
        return $entity;
507
    }
508
509
    /**
510
     * Triggers before/after validate events.
511
     *
512
     * @param \Cake\Datasource\EntityInterface $entity The entity being validated
513
     * @return bool True if save operation should continue, false otherwise
514
     */
515
    protected function _validation(EntityInterface $entity)
516
    {
517
        $validator = new Validator();
518
        $hasErrors = false;
519
520
        foreach ($this->_attributesForEntity($entity) as $attr) {
521
            $field = $this->_prepareMockField($entity, $attr);
522
            $result = $field->validate($validator);
523
524
            if ($result === false) {
525
                $this->attachEntityFields($entity);
526
                return false;
527
            }
528
529
            $errors = $validator->errors($entity->toArray(), $entity->isNew());
530
            $entity->errors($errors);
531
532
            if (!empty($errors)) {
533
                $hasErrors = true;
534
                if ($entity->has('_fields')) {
535
                    $entityErrors = $entity->errors();
536
                    foreach ($entity->get('_fields') as $field) {
537
                        $postData = $entity->get($field->name);
538
                        if (!empty($entityErrors[$field->name])) {
539
                            $field->set('value', $postData);
540
                            $field->metadata->set('errors', (array)$entityErrors[$field->name]);
541
                        }
542
                    }
543
                }
544
            }
545
        }
546
547
        return !$hasErrors;
548
    }
549
550
    /**
551
     * Alters the given $field and fetches incoming POST data, both "value" and
552
     * "extra" property will be automatically filled for the given $field entity.
553
     *
554
     * @param \Field\Model\Entity\Field $field The field entity for which
555
     *  fetch POST information
556
     * @return mixed Raw POST information
557
     */
558
    protected function _fetchPost(Field $field)
559
    {
560
        $post = $field
561
            ->get('metadata')
562
            ->get('entity')
563
            ->get($field->get('name'));
564
565
        // auto-magic
566
        if (is_array($post)) {
567
            $field->set('extra', $post);
568
            $field->set('value', null);
569
        } else {
570
            $field->set('extra', null);
571
            $field->set('value', $post);
572
        }
573
574
        return $post;
575
    }
576
577
    /**
578
     * Gets all attributes that should be attached to the given entity, this entity
579
     * will be used as context to calculate the proper bundle.
580
     *
581
     * @param \Cake\Datasource\EntityInterface $entity Entity context
582
     * @return array
583
     */
584
    protected function _attributesForEntity(EntityInterface $entity)
585
    {
586
        $bundle = $this->_resolveBundle($entity);
587
        $attrs = $this->_toolbox->attributes($bundle);
588
        $attrByIds = []; // attrs indexed by id
589
        $attrByNames = []; // attrs indexed by name
590
591
        foreach ($attrs as $name => $attr) {
592
            $attrByNames[$name] = $attr;
593
            $attrByIds[$attr->get('id')] = $attr;
594
            $attr->set(':value', null);
595
        }
596
597
        if (!empty($attrByIds)) {
598
            $instances = $this->Attributes->Instance
599
                ->find()
600
                ->where(['eav_attribute_id IN' => array_keys($attrByIds)])
601
                ->all();
602
            foreach ($instances as $instance) {
603
                if (!empty($attrByIds[$instance->get('eav_attribute_id')])) {
604
                    $attr = $attrByIds[$instance->get('eav_attribute_id')];
605
                    if (!$attr->has('instance')) {
606
                        $attr->set('instance', $instance);
607
                    }
608
                }
609
            }
610
        }
611
612
        $values = $this->_fetchValues($entity, array_keys($attrByNames));
613
        foreach ($values as $value) {
614
            if (!empty($attrByNames[$value->get('eav_attribute')->get('name')])) {
615
                $attrByNames[$value->get('eav_attribute')->get('name')]->set(':value', $value);
616
            }
617
        }
618
        return $this->_toolbox->attributes($bundle);
619
    }
620
621
    /**
622
     * Retrives stored values for all virtual properties by name. This gets all
623
     * values at once.
624
     *
625
     * This method is used to reduce the number of SQl queries, so we get all
626
     * values at once in a single Select instead of creating a select for every
627
     * field attached to the given entity.
628
     *
629
     * @param \Cake\Datasource\EntityInterface $entity The entuity for which
630
     *  get related values
631
     * @param array $attrNames List of attribute names for which get their
632
     *  values
633
     * @return \Cake\Datasource\ResultSetInterface
634
     */
635
    protected function _fetchValues(EntityInterface $entity, array $attrNames = [])
636
    {
637
        $bundle = $this->_resolveBundle($entity);
638
        $conditions = [
639
            'EavAttribute.table_alias' => $this->_table->table(),
640
            'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
641
        ];
642
643
        if ($bundle) {
644
            $conditions['EavAttribute.bundle'] = $bundle;
645
        }
646
647
        if (!empty($attrNames)) {
648
            $conditions['EavAttribute.name IN'] = $attrNames;
649
        }
650
651
        $storedValues = TableRegistry::get('Eav.EavValues')
652
            ->find()
653
            ->contain(['EavAttribute'])
654
            ->where($conditions)
655
            ->all();
656
        return $storedValues;
657
    }
658
659
    /**
660
     * Creates a new Virtual "Field" to be attached to the given entity.
661
     *
662
     * This mock Field represents a new property (table column) of the entity.
663
     *
664
     * @param \Cake\Datasource\EntityInterface $entity The entity where the
665
     *  generated virtual field will be attached
666
     * @param \Cake\Datasource\EntityInterface $attribute The attribute where to get
667
     *  the information when creating the mock field.
668
     * @return \Field\Model\Entity\Field
669
     */
670
    protected function _prepareMockField(EntityInterface $entity, EntityInterface $attribute)
671
    {
672
        $type = $this->_toolbox->mapType($attribute->get('type'));
673
        if (!$attribute->has(':value')) {
674
            $bundle = $this->_resolveBundle($entity);
675
            $conditions = [
676
                'EavAttribute.table_alias' => $this->_table->table(),
677
                'EavAttribute.name' => $attribute->get('name'),
678
                'EavValues.entity_id' => $entity->get((string)$this->_table->primaryKey()),
679
            ];
680
681
            if ($bundle) {
682
                $conditions['EavAttribute.bundle'] = $bundle;
683
            }
684
685
            $storedValue = TableRegistry::get('Eav.EavValues')
686
                ->find()
687
                ->contain(['EavAttribute'])
688
                ->select(['id', "value_{$type}", 'extra'])
689
                ->where($conditions)
690
                ->limit(1)
691
                ->first();
692
        } else {
693
            $storedValue = $attribute->get(':value');
694
        }
695
696
        $mockField = new Field([
697
            'name' => $attribute->get('name'),
698
            'label' => $attribute->get('instance')->get('label'),
699
            'value' => null,
700
            'extra' => null,
701
            'metadata' => new Entity([
702
                'value_id' => null,
703
                'instance_id' => $attribute->get('instance')->get('id'),
704
                'attribute_id' => $attribute->get('id'),
705
                'entity_id' => $this->_toolbox->getEntityId($entity),
706
                'table_alias' => $attribute->get('table_alias'),
707
                'type' => $type,
708
                'bundle' => $attribute->get('bundle'),
709
                'handler' => $attribute->get('instance')->get('handler'),
710
                'required' => $attribute->get('instance')->required,
711
                'description' => $attribute->get('instance')->description,
712
                'settings' => $attribute->get('instance')->settings,
713
                'view_modes' => $attribute->get('instance')->view_modes,
714
                'entity' => $entity,
715
                'errors' => [],
716
            ]),
717
        ]);
718
719
        if ($storedValue) {
720
            $mockField->set('value', $this->_toolbox->marshal($storedValue->get("value_{$type}"), $type));
721
            $mockField->set('extra', $storedValue->get('extra'));
722
            $mockField->metadata->set('value_id', $storedValue->id);
723
        }
724
725
        $mockField->isNew($entity->isNew());
726
        return $mockField;
727
    }
728
729
    /**
730
     * Resolves `bundle` name using $entity as context.
731
     *
732
     * @param \Cake\Datasource\EntityInterface $entity Entity to use as context when
733
     *  resolving bundle
734
     * @return string Bundle name as string value, it may be an empty string if no
735
     *  bundle should be applied
736
     */
737
    protected function _resolveBundle(EntityInterface $entity)
738
    {
739
        $bundle = $this->config('bundle');
740
        if (is_callable($bundle)) {
741
            $callable = $this->config('bundle');
742
            $bundle = $callable($entity);
743
        }
744
        return (string)$bundle;
745
    }
746
}
747