Seditio Source
Root |
./othercms/croogo-4.0.7/vendor/cakephp/cakephp/src/ORM/Marshaller.php
<?php
/**
 * CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
 * @link          https://cakephp.org CakePHP(tm) Project
 * @since         3.0.0
 * @license       https://opensource.org/licenses/mit-license.php MIT License
 */
namespace Cake\ORM;

use
ArrayObject;
use
Cake\Collection\Collection;
use
Cake\Database\Expression\TupleComparison;
use
Cake\Database\Type;
use
Cake\Datasource\EntityInterface;
use
Cake\Datasource\InvalidPropertyInterface;
use
Cake\ORM\Association\BelongsToMany;
use
RuntimeException;

/**
 * Contains logic to convert array data into entities.
 *
 * Useful when converting request data into entities.
 *
 * @see \Cake\ORM\Table::newEntity()
 * @see \Cake\ORM\Table::newEntities()
 * @see \Cake\ORM\Table::patchEntity()
 * @see \Cake\ORM\Table::patchEntities()
 */
class Marshaller
{
    use
AssociationsNormalizerTrait;

   
/**
     * The table instance this marshaller is for.
     *
     * @var \Cake\ORM\Table
     */
   
protected $_table;

   
/**
     * Constructor.
     *
     * @param \Cake\ORM\Table $table The table this marshaller is for.
     */
   
public function __construct(Table $table)
    {
       
$this->_table = $table;
    }

   
/**
     * Build the map of property => marshalling callable.
     *
     * @param array $data The data being marshalled.
     * @param array $options List of options containing the 'associated' key.
     * @throws \InvalidArgumentException When associations do not exist.
     * @return array
     */
   
protected function _buildPropertyMap($data, $options)
    {
       
$map = [];
       
$schema = $this->_table->getSchema();

       
// Is a concrete column?
       
foreach (array_keys($data) as $prop) {
           
$columnType = $schema->getColumnType($prop);
            if (
$columnType) {
               
$map[$prop] = function ($value, $entity) use ($columnType) {
                    return
Type::build($columnType)->marshal($value);
                };
            }
        }

       
// Map associations
       
if (!isset($options['associated'])) {
           
$options['associated'] = [];
        }
       
$include = $this->_normalizeAssociations($options['associated']);
        foreach (
$include as $key => $nested) {
            if (
is_int($key) && is_scalar($nested)) {
               
$key = $nested;
               
$nested = [];
            }
           
// If the key is not a special field like _ids or _joinData
            // it is a missing association that we should error on.
           
if (!$this->_table->hasAssociation($key)) {
                if (
substr($key, 0, 1) !== '_') {
                    throw new \
InvalidArgumentException(sprintf(
                       
'Cannot marshal data for "%s" association. It is not associated with "%s".',
                       
$key,
                       
$this->_table->getAlias()
                    ));
                }
                continue;
            }
           
$assoc = $this->_table->getAssociation($key);

            if (isset(
$options['forceNew'])) {
               
$nested['forceNew'] = $options['forceNew'];
            }
            if (isset(
$options['isMerge'])) {
               
$callback = function ($value, $entity) use ($assoc, $nested) {
                   
/** @var \Cake\Datasource\EntityInterface $entity */
                   
$options = $nested + ['associated' => [], 'association' => $assoc];

                    return
$this->_mergeAssociation($entity->get($assoc->getProperty()), $assoc, $value, $options);
                };
            } else {
               
$callback = function ($value, $entity) use ($assoc, $nested) {
                   
$options = $nested + ['associated' => []];

                    return
$this->_marshalAssociation($assoc, $value, $options);
                };
            }
           
$map[$assoc->getProperty()] = $callback;
        }

       
$behaviors = $this->_table->behaviors();
        foreach (
$behaviors->loaded() as $name) {
           
$behavior = $behaviors->get($name);
            if (
$behavior instanceof PropertyMarshalInterface) {
               
$map += $behavior->buildMarshalMap($this, $map, $options);
            }
        }

        return
$map;
    }

   
/**
     * Hydrate one entity and its associated data.
     *
     * ### Options:
     *
     * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied.
     *   Defaults to true/default.
     * - associated: Associations listed here will be marshalled as well. Defaults to null.
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead.
     * - fields: A whitelist of fields to be assigned to the entity. If not present,
     *   the accessible fields list in the entity will be used. Defaults to null.
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null
     * - forceNew: When enabled, belongsToMany associations will have 'new' entities created
     *   when primary key values are set, and a record does not already exist. Normally primary key
     *   on missing entities would be ignored. Defaults to false.
     *
     * The above options can be used in each nested `associated` array. In addition to the above
     * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations.
     * When true this option restricts the request data to only be read from `_ids`.
     *
     * ```
     * $result = $marshaller->one($data, [
     *   'associated' => ['Tags' => ['onlyIds' => true]]
     * ]);
     * ```
     *
     * @param array $data The data to hydrate.
     * @param array $options List of options
     * @return \Cake\Datasource\EntityInterface
     * @see \Cake\ORM\Table::newEntity()
     * @see \Cake\ORM\Entity::$_accessible
     */
   
public function one(array $data, array $options = [])
    {
        list(
$data, $options) = $this->_prepareDataAndOptions($data, $options);

       
$primaryKey = (array)$this->_table->getPrimaryKey();
       
$entityClass = $this->_table->getEntityClass();
       
/** @var \Cake\Datasource\EntityInterface $entity */
       
$entity = new $entityClass();
       
$entity->setSource($this->_table->getRegistryAlias());

        if (isset(
$options['accessibleFields'])) {
            foreach ((array)
$options['accessibleFields'] as $key => $value) {
               
$entity->setAccess($key, $value);
            }
        }
       
$errors = $this->_validate($data, $options, true);

       
$options['isMerge'] = false;
       
$propertyMap = $this->_buildPropertyMap($data, $options);
       
$properties = [];
        foreach (
$data as $key => $value) {
            if (!empty(
$errors[$key])) {
                if (
$entity instanceof InvalidPropertyInterface) {
                   
$entity->setInvalidField($key, $value);
                }
                continue;
            }

            if (
$value === '' && in_array($key, $primaryKey, true)) {
               
// Skip marshalling '' for pk fields.
               
continue;
            }
            if (isset(
$propertyMap[$key])) {
               
$properties[$key] = $propertyMap[$key]($value, $entity);
            } else {
               
$properties[$key] = $value;
            }
        }

        if (isset(
$options['fields'])) {
            foreach ((array)
$options['fields'] as $field) {
                if (
array_key_exists($field, $properties)) {
                   
$entity->set($field, $properties[$field]);
                }
            }
        } else {
           
$entity->set($properties);
        }

       
// Don't flag clean association entities as
        // dirty so we don't persist empty records.
       
foreach ($properties as $field => $value) {
            if (
$value instanceof EntityInterface) {
               
$entity->setDirty($field, $value->isDirty());
            }
        }

       
$entity->setErrors($errors);

        return
$entity;
    }

   
/**
     * Returns the validation errors for a data set based on the passed options
     *
     * @param array $data The data to validate.
     * @param array $options The options passed to this marshaller.
     * @param bool $isNew Whether it is a new entity or one to be updated.
     * @return array The list of validation errors.
     * @throws \RuntimeException If no validator can be created.
     */
   
protected function _validate($data, $options, $isNew)
    {
        if (!
$options['validate']) {
            return [];
        }

       
$validator = null;
        if (
$options['validate'] === true) {
           
$validator = $this->_table->getValidator();
        } elseif (
is_string($options['validate'])) {
           
$validator = $this->_table->getValidator($options['validate']);
        } elseif (
is_object($options['validate'])) {
           
/** @var \Cake\Validation\Validator $validator */
           
$validator = $options['validate'];
        }

        if (
$validator === null) {
            throw new
RuntimeException(
               
sprintf('validate must be a boolean, a string or an object. Got %s.', getTypeName($options['validate']))
            );
        }

        return
$validator->errors($data, $isNew);
    }

   
/**
     * Returns data and options prepared to validate and marshall.
     *
     * @param array $data The data to prepare.
     * @param array $options The options passed to this marshaller.
     * @return array An array containing prepared data and options.
     */
   
protected function _prepareDataAndOptions($data, $options)
    {
       
$options += ['validate' => true];

        if (!isset(
$options['fields']) && isset($options['fieldList'])) {
           
deprecationWarning(
               
'The `fieldList` option for marshalling is deprecated. Use the `fields` option instead.'
           
);
           
$options['fields'] = $options['fieldList'];
            unset(
$options['fieldList']);
        }

       
$tableName = $this->_table->getAlias();
        if (isset(
$data[$tableName])) {
           
$data += $data[$tableName];
            unset(
$data[$tableName]);
        }

       
$data = new ArrayObject($data);
       
$options = new ArrayObject($options);
       
$this->_table->dispatchEvent('Model.beforeMarshal', compact('data', 'options'));

        return [(array)
$data, (array)$options];
    }

   
/**
     * Create a new sub-marshaller and marshal the associated data.
     *
     * @param \Cake\ORM\Association $assoc The association to marshall
     * @param array $value The data to hydrate
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
     */
   
protected function _marshalAssociation($assoc, $value, $options)
    {
        if (!
is_array($value)) {
            return
null;
        }
       
$targetTable = $assoc->getTarget();
       
$marshaller = $targetTable->marshaller();
       
$types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
        if (
in_array($assoc->type(), $types, true)) {
            return
$marshaller->one($value, (array)$options);
        }
        if (
$assoc->type() === Association::ONE_TO_MANY || $assoc->type() === Association::MANY_TO_MANY) {
           
$hasIds = array_key_exists('_ids', $value);
           
$onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];

            if (
$hasIds && is_array($value['_ids'])) {
                return
$this->_loadAssociatedByIds($assoc, $value['_ids']);
            }
            if (
$hasIds || $onlyIds) {
                return [];
            }
        }
        if (
$assoc->type() === Association::MANY_TO_MANY) {
            return
$marshaller->_belongsToMany($assoc, $value, (array)$options);
        }

        return
$marshaller->many($value, (array)$options);
    }

   
/**
     * Hydrate many entities and their associated data.
     *
     * ### Options:
     *
     * - validate: Set to false to disable validation. Can also be a string of the validator ruleset to be applied.
     *   Defaults to true/default.
     * - associated: Associations listed here will be marshalled as well. Defaults to null.
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead
     * - fields: A whitelist of fields to be assigned to the entity. If not present,
     *   the accessible fields list in the entity will be used. Defaults to null.
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields. Defaults to null
     * - forceNew: When enabled, belongsToMany associations will have 'new' entities created
     *   when primary key values are set, and a record does not already exist. Normally primary key
     *   on missing entities would be ignored. Defaults to false.
     *
     * @param array $data The data to hydrate.
     * @param array $options List of options
     * @return \Cake\Datasource\EntityInterface[] An array of hydrated records.
     * @see \Cake\ORM\Table::newEntities()
     * @see \Cake\ORM\Entity::$_accessible
     */
   
public function many(array $data, array $options = [])
    {
       
$output = [];
        foreach (
$data as $record) {
            if (!
is_array($record)) {
                continue;
            }
           
$output[] = $this->one($record, $options);
        }

        return
$output;
    }

   
/**
     * Marshals data for belongsToMany associations.
     *
     * Builds the related entities and handles the special casing
     * for junction table entities.
     *
     * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshal.
     * @param array $data The data to convert into entities.
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface[] An array of built entities.
     * @throws \BadMethodCallException
     * @throws \InvalidArgumentException
     * @throws \RuntimeException
     */
   
protected function _belongsToMany(BelongsToMany $assoc, array $data, $options = [])
    {
       
$associated = isset($options['associated']) ? $options['associated'] : [];
       
$forceNew = isset($options['forceNew']) ? $options['forceNew'] : false;

       
$data = array_values($data);

       
$target = $assoc->getTarget();
       
$primaryKey = array_flip((array)$target->getPrimaryKey());
       
$records = $conditions = [];
       
$primaryCount = count($primaryKey);
       
$conditions = [];

        foreach (
$data as $i => $row) {
            if (!
is_array($row)) {
                continue;
            }
            if (
array_intersect_key($primaryKey, $row) === $primaryKey) {
               
$keys = array_intersect_key($row, $primaryKey);
                if (
count($keys) === $primaryCount) {
                   
$rowConditions = [];
                    foreach (
$keys as $key => $value) {
                       
$rowConditions[][$target->aliasField($key)] = $value;
                    }

                    if (
$forceNew && !$target->exists($rowConditions)) {
                       
$records[$i] = $this->one($row, $options);
                    }

                   
$conditions = array_merge($conditions, $rowConditions);
                }
            } else {
               
$records[$i] = $this->one($row, $options);
            }
        }

        if (!empty(
$conditions)) {
           
$query = $target->find();
           
$query->andWhere(function ($exp) use ($conditions) {
               
/** @var \Cake\Database\Expression\QueryExpression $exp */
               
return $exp->or($conditions);
            });

           
$keyFields = array_keys($primaryKey);

           
$existing = [];
            foreach (
$query as $row) {
               
$k = implode(';', $row->extract($keyFields));
               
$existing[$k] = $row;
            }

            foreach (
$data as $i => $row) {
               
$key = [];
                foreach (
$keyFields as $k) {
                    if (isset(
$row[$k])) {
                       
$key[] = $row[$k];
                    }
                }
               
$key = implode(';', $key);

               
// Update existing record and child associations
               
if (isset($existing[$key])) {
                   
$records[$i] = $this->merge($existing[$key], $data[$i], $options);
                }
            }
        }

       
$jointMarshaller = $assoc->junction()->marshaller();

       
$nested = [];
        if (isset(
$associated['_joinData'])) {
           
$nested = (array)$associated['_joinData'];
        }

        foreach (
$records as $i => $record) {
           
// Update junction table data in _joinData.
           
if (isset($data[$i]['_joinData'])) {
               
$joinData = $jointMarshaller->one($data[$i]['_joinData'], $nested);
               
$record->set('_joinData', $joinData);
            }
        }

        return
$records;
    }

   
/**
     * Loads a list of belongs to many from ids.
     *
     * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association.
     * @param array $ids The list of ids to load.
     * @return \Cake\Datasource\EntityInterface[] An array of entities.
     */
   
protected function _loadAssociatedByIds($assoc, $ids)
    {
        if (empty(
$ids)) {
            return [];
        }

       
$target = $assoc->getTarget();
       
$primaryKey = (array)$target->getPrimaryKey();
       
$multi = count($primaryKey) > 1;
       
$primaryKey = array_map([$target, 'aliasField'], $primaryKey);

        if (
$multi) {
           
$first = current($ids);
            if (!
is_array($first) || count($first) !== count($primaryKey)) {
                return [];
            }
           
$filter = new TupleComparison($primaryKey, $ids, [], 'IN');
        } else {
           
$filter = [$primaryKey[0] . ' IN' => $ids];
        }

        return
$target->find()->where($filter)->toArray();
    }

   
/**
     * Loads a list of belongs to many from ids.
     *
     * @param \Cake\ORM\Association $assoc The association class for the belongsToMany association.
     * @param array $ids The list of ids to load.
     * @return \Cake\Datasource\EntityInterface[] An array of entities.
     * @deprecated Use _loadAssociatedByIds()
     */
   
protected function _loadBelongsToMany($assoc, $ids)
    {
       
deprecationWarning(
           
'Marshaller::_loadBelongsToMany() is deprecated. Use _loadAssociatedByIds() instead.'
       
);

        return
$this->_loadAssociatedByIds($assoc, $ids);
    }

   
/**
     * Merges `$data` into `$entity` and recursively does the same for each one of
     * the association names passed in `$options`. When merging associations, if an
     * entity is not present in the parent entity for a given association, a new one
     * will be created.
     *
     * When merging HasMany or BelongsToMany associations, all the entities in the
     * `$data` array will appear, those that can be matched by primary key will get
     * the data merged, but those that cannot, will be discarded. `ids` option can be used
     * to determine whether the association must use the `_ids` format.
     *
     * ### Options:
     *
     * - associated: Associations listed here will be marshalled as well.
     * - validate: Whether or not to validate data before hydrating the entities. Can
     *   also be set to a string to use a specific validator. Defaults to true/default.
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead
     * - fields: A whitelist of fields to be assigned to the entity. If not present
     *   the accessible fields list in the entity will be used.
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
     *
     * The above options can be used in each nested `associated` array. In addition to the above
     * options you can also use the `onlyIds` option for HasMany and BelongsToMany associations.
     * When true this option restricts the request data to only be read from `_ids`.
     *
     * ```
     * $result = $marshaller->merge($entity, $data, [
     *   'associated' => ['Tags' => ['onlyIds' => true]]
     * ]);
     * ```
     *
     * @param \Cake\Datasource\EntityInterface $entity the entity that will get the
     * data merged in
     * @param array $data key value list of fields to be merged into the entity
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface
     * @see \Cake\ORM\Entity::$_accessible
     */
   
public function merge(EntityInterface $entity, array $data, array $options = [])
    {
        list(
$data, $options) = $this->_prepareDataAndOptions($data, $options);

       
$isNew = $entity->isNew();
       
$keys = [];

        if (!
$isNew) {
           
$keys = $entity->extract((array)$this->_table->getPrimaryKey());
        }

        if (isset(
$options['accessibleFields'])) {
            foreach ((array)
$options['accessibleFields'] as $key => $value) {
               
$entity->setAccess($key, $value);
            }
        }

       
$errors = $this->_validate($data + $keys, $options, $isNew);
       
$options['isMerge'] = true;
       
$propertyMap = $this->_buildPropertyMap($data, $options);
       
$properties = [];
        foreach (
$data as $key => $value) {
            if (!empty(
$errors[$key])) {
                if (
$entity instanceof InvalidPropertyInterface) {
                   
$entity->setInvalidField($key, $value);
                }
                continue;
            }
           
$original = $entity->get($key);

            if (isset(
$propertyMap[$key])) {
               
$value = $propertyMap[$key]($value, $entity);

               
// Don't dirty scalar values and objects that didn't
                // change. Arrays will always be marked as dirty because
                // the original/updated list could contain references to the
                // same objects, even though those objects may have changed internally.
               
if (
                    (
is_scalar($value) && $original === $value) ||
                    (
$value === null && $original === $value) ||
                    (
is_object($value) && !($value instanceof EntityInterface) && $original == $value)
                ) {
                    continue;
                }
            }
           
$properties[$key] = $value;
        }

       
$entity->setErrors($errors);
        if (!isset(
$options['fields'])) {
           
$entity->set($properties);

            foreach (
$properties as $field => $value) {
                if (
$value instanceof EntityInterface) {
                   
$entity->setDirty($field, $value->isDirty());
                }
            }

            return
$entity;
        }

        foreach ((array)
$options['fields'] as $field) {
            if (!
array_key_exists($field, $properties)) {
                continue;
            }
           
$entity->set($field, $properties[$field]);
            if (
$properties[$field] instanceof EntityInterface) {
               
$entity->setDirty($field, $properties[$field]->isDirty());
            }
        }

        return
$entity;
    }

   
/**
     * Merges each of the elements from `$data` into each of the entities in `$entities`
     * and recursively does the same for each of the association names passed in
     * `$options`. When merging associations, if an entity is not present in the parent
     * entity for a given association, a new one will be created.
     *
     * Records in `$data` are matched against the entities using the primary key
     * column. Entries in `$entities` that cannot be matched to any record in
     * `$data` will be discarded. Records in `$data` that could not be matched will
     * be marshalled as a new entity.
     *
     * When merging HasMany or BelongsToMany associations, all the entities in the
     * `$data` array will appear, those that can be matched by primary key will get
     * the data merged, but those that cannot, will be discarded.
     *
     * ### Options:
     *
     * - validate: Whether or not to validate data before hydrating the entities. Can
     *   also be set to a string to use a specific validator. Defaults to true/default.
     * - associated: Associations listed here will be marshalled as well.
     * - fieldList: (deprecated) Since 3.4.0. Use fields instead
     * - fields: A whitelist of fields to be assigned to the entity. If not present,
     *   the accessible fields list in the entity will be used.
     * - accessibleFields: A list of fields to allow or deny in entity accessible fields.
     *
     * @param \Cake\Datasource\EntityInterface[]|\Traversable $entities the entities that will get the
     *   data merged in
     * @param array $data list of arrays to be merged into the entities
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface[]
     * @see \Cake\ORM\Entity::$_accessible
     */
   
public function mergeMany($entities, array $data, array $options = [])
    {
       
$primary = (array)$this->_table->getPrimaryKey();

       
$indexed = (new Collection($data))
            ->
groupBy(function ($el) use ($primary) {
               
$keys = [];
                foreach (
$primary as $key) {
                   
$keys[] = isset($el[$key]) ? $el[$key] : '';
                }

                return
implode(';', $keys);
            })
            ->
map(function ($element, $key) {
                return
$key === '' ? $element : $element[0];
            })
            ->
toArray();

       
$new = isset($indexed[null]) ? $indexed[null] : [];
        unset(
$indexed[null]);
       
$output = [];

        foreach (
$entities as $entity) {
            if (!(
$entity instanceof EntityInterface)) {
                continue;
            }

           
$key = implode(';', $entity->extract($primary));
            if (
$key === null || !isset($indexed[$key])) {
                continue;
            }

           
$output[] = $this->merge($entity, $indexed[$key], $options);
            unset(
$indexed[$key]);
        }

       
$conditions = (new Collection($indexed))
            ->
map(function ($data, $key) {
                return
explode(';', $key);
            })
            ->
filter(function ($keys) use ($primary) {
                return
count(array_filter($keys, 'strlen')) === count($primary);
            })
            ->
reduce(function ($conditions, $keys) use ($primary) {
               
$fields = array_map([$this->_table, 'aliasField'], $primary);
               
$conditions['OR'][] = array_combine($fields, $keys);

                return
$conditions;
            }, [
'OR' => []]);
       
$maybeExistentQuery = $this->_table->find()->where($conditions);

        if (!empty(
$indexed) && count($maybeExistentQuery->clause('where'))) {
            foreach (
$maybeExistentQuery as $entity) {
               
$key = implode(';', $entity->extract($primary));
                if (isset(
$indexed[$key])) {
                   
$output[] = $this->merge($entity, $indexed[$key], $options);
                    unset(
$indexed[$key]);
                }
            }
        }

        foreach ((new
Collection($indexed))->append($new) as $value) {
            if (!
is_array($value)) {
                continue;
            }
           
$output[] = $this->one($value, $options);
        }

        return
$output;
    }

   
/**
     * Creates a new sub-marshaller and merges the associated data.
     *
     * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $original The original entity
     * @param \Cake\ORM\Association $assoc The association to merge
     * @param array $value The data to hydrate
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null
     */
   
protected function _mergeAssociation($original, $assoc, $value, $options)
    {
        if (!
$original) {
            return
$this->_marshalAssociation($assoc, $value, $options);
        }
        if (!
is_array($value)) {
            return
null;
        }

       
$targetTable = $assoc->getTarget();
       
$marshaller = $targetTable->marshaller();
       
$types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE];
        if (
in_array($assoc->type(), $types, true)) {
            return
$marshaller->merge($original, $value, (array)$options);
        }
        if (
$assoc->type() === Association::MANY_TO_MANY) {
            return
$marshaller->_mergeBelongsToMany($original, $assoc, $value, (array)$options);
        }

        if (
$assoc->type() === Association::ONE_TO_MANY) {
           
$hasIds = array_key_exists('_ids', $value);
           
$onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];
            if (
$hasIds && is_array($value['_ids'])) {
                return
$this->_loadAssociatedByIds($assoc, $value['_ids']);
            }
            if (
$hasIds || $onlyIds) {
                return [];
            }
        }

        return
$marshaller->mergeMany($original, $value, (array)$options);
    }

   
/**
     * Creates a new sub-marshaller and merges the associated data for a BelongstoMany
     * association.
     *
     * @param \Cake\Datasource\EntityInterface $original The original entity
     * @param \Cake\ORM\Association $assoc The association to marshall
     * @param array $value The data to hydrate
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface[]
     */
   
protected function _mergeBelongsToMany($original, $assoc, $value, $options)
    {
       
$associated = isset($options['associated']) ? $options['associated'] : [];

       
$hasIds = array_key_exists('_ids', $value);
       
$onlyIds = array_key_exists('onlyIds', $options) && $options['onlyIds'];

        if (
$hasIds && is_array($value['_ids'])) {
            return
$this->_loadAssociatedByIds($assoc, $value['_ids']);
        }
        if (
$hasIds || $onlyIds) {
            return [];
        }

        if (!empty(
$associated) && !in_array('_joinData', $associated, true) && !isset($associated['_joinData'])) {
            return
$this->mergeMany($original, $value, $options);
        }

        return
$this->_mergeJoinData($original, $assoc, $value, $options);
    }

   
/**
     * Merge the special _joinData property into the entity set.
     *
     * @param \Cake\Datasource\EntityInterface $original The original entity
     * @param \Cake\ORM\Association\BelongsToMany $assoc The association to marshall
     * @param array $value The data to hydrate
     * @param array $options List of options.
     * @return \Cake\Datasource\EntityInterface[] An array of entities
     */
   
protected function _mergeJoinData($original, $assoc, $value, $options)
    {
       
$associated = isset($options['associated']) ? $options['associated'] : [];
       
$extra = [];
        foreach (
$original as $entity) {
           
// Mark joinData as accessible so we can marshal it properly.
           
$entity->setAccess('_joinData', true);

           
$joinData = $entity->get('_joinData');
            if (
$joinData && $joinData instanceof EntityInterface) {
               
$extra[spl_object_hash($entity)] = $joinData;
            }
        }

       
$joint = $assoc->junction();
       
$marshaller = $joint->marshaller();

       
$nested = [];
        if (isset(
$associated['_joinData'])) {
           
$nested = (array)$associated['_joinData'];
        }

       
$options['accessibleFields'] = ['_joinData' => true];

       
$records = $this->mergeMany($original, $value, $options);
        foreach (
$records as $record) {
           
$hash = spl_object_hash($record);
           
$value = $record->get('_joinData');

           
// Already an entity, no further marshalling required.
           
if ($value instanceof EntityInterface) {
                continue;
            }

           
// Scalar data can't be handled
           
if (!is_array($value)) {
               
$record->unsetProperty('_joinData');
                continue;
            }

           
// Marshal data into the old object, or make a new joinData object.
           
if (isset($extra[$hash])) {
               
$record->set('_joinData', $marshaller->merge($extra[$hash], $value, $nested));
            } elseif (
is_array($value)) {
               
$joinData = $marshaller->one($value, $nested);
               
$record->set('_joinData', $joinData);
            }
        }

        return
$records;
    }
}