diff --git a/Association.php b/Association.php index 218cade8..8601283b 100644 --- a/Association.php +++ b/Association.php @@ -17,16 +17,20 @@ namespace Cake\ORM; use Cake\Collection\Collection; +use Cake\Collection\CollectionInterface; use Cake\Core\App; use Cake\Core\ConventionsTrait; use Cake\Database\Expression\IdentifierExpression; use Cake\Datasource\EntityInterface; use Cake\Datasource\ResultSetDecorator; +use Cake\Datasource\ResultSetInterface; use Cake\ORM\Locator\LocatorAwareTrait; use Cake\Utility\Inflector; use Closure; use InvalidArgumentException; use RuntimeException; +use function Cake\Core\deprecationWarning; +use function Cake\Core\pluginSplit; /** * An Association is a relationship established between two tables and is used @@ -106,14 +110,14 @@ abstract class Association /** * The field name in the owning side table that is used to match with the foreignKey * - * @var string|string[]|null + * @var array|string|null */ protected $_bindingKey; /** * The name of the field representing the foreign key to the table to load * - * @var string|string[] + * @var array|string */ protected $_foreignKey; @@ -121,7 +125,7 @@ abstract class Association * A list of conditions to be always included when fetching records from * the target association * - * @var array|\Closure + * @var \Closure|array */ protected $_conditions = []; @@ -135,7 +139,7 @@ abstract class Association protected $_dependent = false; /** - * Whether or not cascaded deletes should also fire callbacks. + * Whether cascaded deletes should also fire callbacks. * * @var bool */ @@ -182,14 +186,14 @@ abstract class Association * The default finder name to use for fetching rows from the target table * With array value, finder name and default options are allowed. * - * @var string|array + * @var array|string */ protected $_finder = 'all'; /** * Valid strategies for this association. Subclasses can narrow this down. * - * @var string[] + * @var array */ protected $_validStrategies = [ self::STRATEGY_JOIN, @@ -202,7 +206,7 @@ abstract class Association * list of passed options if expecting any other special key * * @param string $alias The name given to the association - * @param array $options A list of properties to be set on this object + * @param array $options A list of properties to be set on this object */ public function __construct(string $alias, array $options = []) { @@ -246,9 +250,16 @@ public function __construct(string $alias, array $options = []) * * @param string $name Name to be assigned * @return $this + * @deprecated 4.3.0 Changing the association name after object creation is + * no longer supported. The name should only be set through the constructor. */ public function setName(string $name) { + deprecationWarning( + 'Changing the association name after object creation is no longer supported.' + . ' The name should only be set through the constructor' + ); + if ($this->_targetTable !== null) { $alias = $this->_targetTable->getAlias(); if ($alias !== $name) { @@ -277,7 +288,7 @@ public function getName(): string } /** - * Sets whether or not cascaded deletes should also fire callbacks. + * Sets whether cascaded deletes should also fire callbacks. * * @param bool $cascadeCallbacks cascade callbacks switch value * @return $this @@ -290,7 +301,7 @@ public function setCascadeCallbacks(bool $cascadeCallbacks) } /** - * Gets whether or not cascaded deletes should also fire callbacks. + * Gets whether cascaded deletes should also fire callbacks. * * @return bool */ @@ -422,9 +433,9 @@ public function getTarget(): Table * Sets a list of conditions to be always included when fetching records from * the target association. * - * @param array|\Closure $conditions list of conditions to be used + * @param \Closure|array $conditions list of conditions to be used * @see \Cake\Database\Query::where() for examples on the format of the array - * @return \Cake\ORM\Association + * @return $this */ public function setConditions($conditions) { @@ -438,7 +449,7 @@ public function setConditions($conditions) * the target association. * * @see \Cake\Database\Query::where() for examples on the format of the array - * @return array|\Closure + * @return \Closure|array */ public function getConditions() { @@ -449,7 +460,7 @@ public function getConditions() * Sets the name of the field representing the binding field with the target table. * When not manually specified the primary key of the owning side table is used. * - * @param string|string[] $key the table field or fields to be used to link both tables together + * @param array|string $key the table field or fields to be used to link both tables together * @return $this */ public function setBindingKey($key) @@ -463,7 +474,7 @@ public function setBindingKey($key) * Gets the name of the field representing the binding field with the target table. * When not manually specified the primary key of the owning side table is used. * - * @return string|string[] + * @return array|string */ public function getBindingKey() { @@ -479,7 +490,7 @@ public function getBindingKey() /** * Gets the name of the field representing the foreign key to the target table. * - * @return string|string[] + * @return array|string */ public function getForeignKey() { @@ -489,7 +500,7 @@ public function getForeignKey() /** * Sets the name of the field representing the foreign key to the target table. * - * @param string|string[] $key the key or keys to be used to link both tables together + * @param array|string $key the key or keys to be used to link both tables together * @return $this */ public function setForeignKey($key) @@ -533,7 +544,7 @@ public function getDependent(): bool /** * Whether this association can be expressed directly in a query join * - * @param array $options custom options key that could alter the return value + * @param array $options custom options key that could alter the return value * @return bool */ public function canBeJoined(array $options = []): bool @@ -653,7 +664,7 @@ public function getStrategy(): string /** * Gets the default finder to use for fetching rows from the target table. * - * @return string|array + * @return array|string */ public function getFinder() { @@ -663,7 +674,7 @@ public function getFinder() /** * Sets the default finder to use for fetching rows from the target table. * - * @param string|array $finder the finder name to use or array of finder name and option. + * @param array|string $finder the finder name to use or array of finder name and option. * @return $this */ public function setFinder($finder) @@ -677,7 +688,7 @@ public function setFinder($finder) * Override this function to initialize any concrete association class, it will * get passed the original list of options used in the constructor * - * @param array $options List of options used for initialization + * @param array $options List of options used for initialization * @return void */ protected function _options(array $options): void @@ -696,8 +707,6 @@ protected function _options(array $options): void * - conditions: array with a list of conditions to filter the join with, this * will be merged with any conditions originally configured for this association * - fields: a list of fields in the target table to include in the result - * - type: The type of join to be used (e.g. INNER) - * the records found on this association * - aliasPath: A dot separated string representing the path of association names * followed from the passed query main table to this association. * - propertyPath: A dot separated string representing the path of association @@ -708,27 +717,31 @@ protected function _options(array $options): void * with this association. * * @param \Cake\ORM\Query $query the query to be altered to include the target table data - * @param array $options Any extra options or overrides to be taken in account + * @param array $options Any extra options or overrides to be taken in account * @return void - * @throws \RuntimeException if the query builder passed does not return a query - * object + * @throws \RuntimeException Unable to build the query or associations. */ public function attachTo(Query $query, array $options = []): void { $target = $this->getTarget(); - $joinType = empty($options['joinType']) ? $this->getJoinType() : $options['joinType']; $table = $target->getTable(); $options += [ 'includeFields' => true, 'foreignKey' => $this->getForeignKey(), 'conditions' => [], + 'joinType' => $this->getJoinType(), 'fields' => [], - 'type' => $joinType, 'table' => $table, 'finder' => $this->getFinder(), ]; + // This is set by joinWith to disable matching results + if ($options['fields'] === false) { + $options['fields'] = []; + $options['includeFields'] = false; + } + if (!empty($options['foreignKey'])) { $joinCondition = $this->_joinCondition($options); if ($joinCondition) { @@ -764,9 +777,11 @@ public function attachTo(Query $query, array $options = []): void $dummy->where($options['conditions']); $this->_dispatchBeforeFind($dummy); - $joinOptions = ['table' => 1, 'conditions' => 1, 'type' => 1]; - $options['conditions'] = $dummy->clause('where'); - $query->join([$this->_name => array_intersect_key($options, $joinOptions)]); + $query->join([$this->_name => [ + 'table' => $options['table'], + 'conditions' => $dummy->clause('where'), + 'type' => $options['joinType'], + ]]); $this->_appendFields($query, $dummy, $options); $this->_formatAssociationResults($query, $dummy, $options); @@ -779,7 +794,7 @@ public function attachTo(Query $query, array $options = []): void * records where there is no match with this association. * * @param \Cake\ORM\Query $query The query to modify - * @param array $options Options array containing the `negateMatch` key. + * @param array $options Options array containing the `negateMatch` key. * @return void */ protected function _appendNotMatching(Query $query, array $options): void @@ -802,7 +817,7 @@ protected function _appendNotMatching(Query $query, array $options): void * @param array $row The row to transform * @param string $nestKey The array key under which the results for this association * should be found - * @param bool $joined Whether or not the row is a result of a direct join + * @param bool $joined Whether the row is a result of a direct join * with this association * @param string|null $targetProperty The property name in the source results where the association * data shuld be nested in. Will use the default one if not provided. @@ -826,10 +841,10 @@ public function transformRow(array $row, string $nestKey, bool $joined, ?string * with the default empty value according to whether the association was * joined or fetched externally. * - * @param array $row The row to set a default on. - * @param bool $joined Whether or not the row is a result of a direct join + * @param array $row The row to set a default on. + * @param bool $joined Whether the row is a result of a direct join * with this association - * @return array + * @return array */ public function defaultRowValue(array $row, bool $joined): array { @@ -846,9 +861,9 @@ public function defaultRowValue(array $row, bool $joined): array * and modifies the query accordingly based of this association * configuration * - * @param string|array|null $type the type of query to perform, if an array is passed, + * @param array|string|null $type the type of query to perform, if an array is passed, * it will be interpreted as the `$options` parameter - * @param array $options The options to for the find + * @param array $options The options to for the find * @see \Cake\ORM\Table::find() * @return \Cake\ORM\Query */ @@ -866,7 +881,7 @@ public function find($type = null, array $options = []): Query * Proxies the operation to the target table's exists method after * appending the default conditions for this association * - * @param array|\Closure|\Cake\Database\ExpressionInterface $conditions The conditions to use + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions The conditions to use * for checking if any record matches. * @see \Cake\ORM\Table::exists() * @return bool @@ -884,7 +899,7 @@ public function exists($conditions): bool * Proxies the update operation to the target table's updateAll method * * @param array $fields A hash of field => new value. - * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions Conditions to be used, accepts anything Query::where() * can take. * @see \Cake\ORM\Table::updateAll() * @return int Count Returns the affected rows. @@ -901,7 +916,7 @@ public function updateAll(array $fields, $conditions): int /** * Proxies the delete operation to the target table's deleteAll method * - * @param mixed $conditions Conditions to be used, accepts anything Query::where() + * @param \Cake\Database\ExpressionInterface|\Closure|array|string|null $conditions Conditions to be used, accepts anything Query::where() * can take. * @return int Returns the number of affected rows. * @see \Cake\ORM\Table::deleteAll() @@ -919,7 +934,7 @@ public function deleteAll($conditions): int * Returns true if the eager loading process will require a set of the owning table's * binding keys in order to use them as a filter in the finder query. * - * @param array $options The options containing the strategy to be used. + * @param array $options The options containing the strategy to be used. * @return bool true if a list of keys will be required */ public function requiresKeys(array $options = []): bool @@ -947,7 +962,7 @@ protected function _dispatchBeforeFind(Query $query): void * * @param \Cake\ORM\Query $query the query that will get the fields appended to * @param \Cake\ORM\Query $surrogate the query having the fields to be copied from - * @param array $options options passed to the method `attachTo` + * @param array $options options passed to the method `attachTo` * @return void */ protected function _appendFields(Query $query, Query $surrogate, array $options): void @@ -956,25 +971,17 @@ protected function _appendFields(Query $query, Query $surrogate, array $options) return; } - $fields = $surrogate->clause('select') ?: $options['fields']; - $target = $this->_targetTable; - $autoFields = $surrogate->isAutoFieldsEnabled(); - - if (empty($fields) && !$autoFields) { - if ($options['includeFields'] && ($fields === null || $fields !== false)) { - $fields = $target->getSchema()->columns(); - } - } + $fields = array_merge($surrogate->clause('select'), $options['fields']); - if ($autoFields === true) { - $fields = array_filter((array)$fields); - $fields = array_merge($fields, $target->getSchema()->columns()); + if ( + (empty($fields) && $options['includeFields']) || + $surrogate->isAutoFieldsEnabled() + ) { + $fields = array_merge($fields, $this->_targetTable->getSchema()->columns()); } - if ($fields) { - $query->select($query->aliasFields($fields, $this->_name)); - } - $query->addDefaultTypes($target); + $query->select($query->aliasFields($fields, $this->_name)); + $query->addDefaultTypes($this->_targetTable); } /** @@ -987,7 +994,7 @@ protected function _appendFields(Query $query, Query $surrogate, array $options) * @param \Cake\ORM\Query $query the query that will get the formatter applied to * @param \Cake\ORM\Query $surrogate the query having formatters for the associated * target table. - * @param array $options options passed to the method `attachTo` + * @param array $options options passed to the method `attachTo` * @return void */ protected function _formatAssociationResults(Query $query, Query $surrogate, array $options): void @@ -1000,35 +1007,40 @@ protected function _formatAssociationResults(Query $query, Query $surrogate, arr $property = $options['propertyPath']; $propertyPath = explode('.', $property); - $query->formatResults(function ($results, $query) use ($formatters, $property, $propertyPath) { - $extracted = []; - foreach ($results as $result) { - foreach ($propertyPath as $propertyPathItem) { - if (!isset($result[$propertyPathItem])) { - $result = null; - break; + $query->formatResults( + function (CollectionInterface $results, $query) use ($formatters, $property, $propertyPath) { + $extracted = []; + foreach ($results as $result) { + foreach ($propertyPath as $propertyPathItem) { + if (!isset($result[$propertyPathItem])) { + $result = null; + break; + } + $result = $result[$propertyPathItem]; + } + $extracted[] = $result; + } + $extracted = new Collection($extracted); + foreach ($formatters as $callable) { + $extracted = $callable($extracted, $query); + if (!$extracted instanceof ResultSetInterface) { + $extracted = new ResultSetDecorator($extracted); } - $result = $result[$propertyPathItem]; } - $extracted[] = $result; - } - $extracted = new Collection($extracted); - foreach ($formatters as $callable) { - $extracted = new ResultSetDecorator($callable($extracted, $query)); - } - /** @var \Cake\Collection\CollectionInterface $results */ - $results = $results->insert($property, $extracted); - if ($query->isHydrationEnabled()) { - $results = $results->map(function ($result) { - $result->clean(); + $results = $results->insert($property, $extracted); + if ($query->isHydrationEnabled()) { + $results = $results->map(function ($result) { + $result->clean(); - return $result; - }); - } + return $result; + }); + } - return $results; - }, Query::PREPEND); + return $results; + }, + Query::PREPEND + ); } /** @@ -1041,7 +1053,7 @@ protected function _formatAssociationResults(Query $query, Query $surrogate, arr * * @param \Cake\ORM\Query $query the query that will get the associations attached to * @param \Cake\ORM\Query $surrogate the query having the containments to be attached - * @param array $options options passed to the method `attachTo` + * @param array $options options passed to the method `attachTo` * @return void */ protected function _bindNewAssociations(Query $query, Query $surrogate, array $options): void @@ -1077,7 +1089,7 @@ protected function _bindNewAssociations(Query $query, Query $surrogate, array $o * Returns a single or multiple conditions to be appended to the generated join * clause for getting the results on the target table. * - * @param array $options list of options passed to attachTo method + * @param array $options list of options passed to attachTo method * @return array * @throws \RuntimeException if the number of columns in the foreignKey do not * match the number of columns in the source table primaryKey @@ -1130,7 +1142,7 @@ protected function _joinCondition(array $options): array * $query->contain(['Comments' => ['finder' => ['translations' => []]]]); * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]); * - * @param string|array $finderData The finder name or an array having the name as key + * @param array|string $finderData The finder name or an array having the name as key * and options as value. * @return array */ @@ -1192,7 +1204,7 @@ abstract public function type(): string; /** * Eager loads a list of records in the target table that are related to another - * set of records in the source table. Source records can specified in two ways: + * set of records in the source table. Source records can be specified in two ways: * first one is by passing a Query object setup to find on the source table and * the other way is by explicitly passing an array of primary key values from * the source table. @@ -1217,7 +1229,7 @@ abstract public function type(): string; * - strategy: The name of strategy to use for finding target table records * - nestKey: The array key under which results will be found when transforming the row * - * @param array $options The options for eager loading. + * @param array $options The options for eager loading. * @return \Closure */ abstract public function eagerLoader(array $options): Closure; @@ -1229,13 +1241,13 @@ abstract public function eagerLoader(array $options): Closure; * required. * * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete. - * @param array $options The options for the original delete. + * @param array $options The options for the original delete. * @return bool Success */ abstract public function cascadeDelete(EntityInterface $entity, array $options = []): bool; /** - * Returns whether or not the passed table is the owning side for this + * Returns whether the passed table is the owning side for this * association. This means that rows in the 'target' table would miss important * or required information if the row in 'source' did not exist. * @@ -1249,7 +1261,7 @@ abstract public function isOwningSide(Table $side): bool; * the saving operation to the target table. * * @param \Cake\Datasource\EntityInterface $entity the data to be saved - * @param array $options The options for saving associated data. + * @param array $options The options for saving associated data. * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns * the saved entity * @see \Cake\ORM\Table::save() diff --git a/Association/BelongsTo.php b/Association/BelongsTo.php index fb354148..3072073a 100644 --- a/Association/BelongsTo.php +++ b/Association/BelongsTo.php @@ -24,19 +24,23 @@ use Cake\Utility\Inflector; use Closure; use RuntimeException; +use function Cake\Core\pluginSplit; /** * Represents an 1 - N relationship where the source side of the relation is * related to only one record in the target table. * * An example of a BelongsTo association would be Article belongs to Author. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class BelongsTo extends Association { /** * Valid strategies for this type of association * - * @var string[] + * @var array */ protected $_validStrategies = [ self::STRATEGY_JOIN, @@ -46,7 +50,7 @@ class BelongsTo extends Association /** * Gets the name of the field representing the foreign key to the target table. * - * @return string|string[] + * @return array|string */ public function getForeignKey() { @@ -63,7 +67,7 @@ public function getForeignKey() * BelongsTo associations are never cleared in a cascading delete scenario. * * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete. - * @param array $options The options for the original delete. + * @param array $options The options for the original delete. * @return bool Success. */ public function cascadeDelete(EntityInterface $entity, array $options = []): bool @@ -84,7 +88,7 @@ protected function _propertyName(): string } /** - * Returns whether or not the passed table is the owning side for this + * Returns whether the passed table is the owning side for this * association. This means that rows in the 'target' table would miss important * or required information if the row in 'source' did not exist. * @@ -113,7 +117,7 @@ public function type(): string * `$options` * * @param \Cake\Datasource\EntityInterface $entity an entity from the source table - * @param array $options options to be passed to the save method in the target table + * @param array $options options to be passed to the save method in the target table * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns * the saved entity * @see \Cake\ORM\Table::save() @@ -144,8 +148,8 @@ public function saveAssociated(EntityInterface $entity, array $options = []) * Returns a single or multiple conditions to be appended to the generated join * clause for getting the results on the target table. * - * @param array $options list of options passed to attachTo method - * @return \Cake\Database\Expression\IdentifierExpression[] + * @param array $options list of options passed to attachTo method + * @return array<\Cake\Database\Expression\IdentifierExpression> * @throws \RuntimeException if the number of columns in the foreignKey do not * match the number of columns in the target table primaryKey */ diff --git a/Association/BelongsToMany.php b/Association/BelongsToMany.php index 1ba41d9e..71b26e4a 100644 --- a/Association/BelongsToMany.php +++ b/Association/BelongsToMany.php @@ -25,6 +25,7 @@ use Cake\ORM\Association\Loader\SelectWithPivotLoader; use Cake\ORM\Query; use Cake\ORM\Table; +use Cake\Utility\Hash; use Cake\Utility\Inflector; use Closure; use InvalidArgumentException; @@ -35,6 +36,10 @@ * that contains the association fields between the source and the target table. * * An example of a BelongsToMany association would be Article belongs to many Tags. + * In this example 'Article' is the source table and 'Tags' is the target table. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class BelongsToMany extends Association { @@ -106,21 +111,21 @@ class BelongsToMany extends Association /** * The name of the field representing the foreign key to the target table * - * @var string|string[]|null + * @var array|string|null */ protected $_targetForeignKey; /** * The table instance for the junction relation. * - * @var string|\Cake\ORM\Table + * @var \Cake\ORM\Table|string */ protected $_through; /** * Valid strategies for this type of association * - * @var string[] + * @var array */ protected $_validStrategies = [ self::STRATEGY_SELECT, @@ -161,7 +166,7 @@ class BelongsToMany extends Association /** * Sets the name of the field representing the foreign key to the target table. * - * @param string|string[] $key the key to be used to link both tables together + * @param array|string $key the key to be used to link both tables together * @return $this */ public function setTargetForeignKey($key) @@ -174,7 +179,7 @@ public function setTargetForeignKey($key) /** * Gets the name of the field representing the foreign key to the target table. * - * @return string|string[] + * @return array|string */ public function getTargetForeignKey() { @@ -188,7 +193,7 @@ public function getTargetForeignKey() /** * Whether this association can be expressed directly in a query join * - * @param array $options custom options key that could alter the return value + * @param array $options custom options key that could alter the return value * @return bool if the 'matching' key in $option is true then this function * will return true, false otherwise */ @@ -200,7 +205,7 @@ public function canBeJoined(array $options = []): bool /** * Gets the name of the field representing the foreign key to the source table. * - * @return string|string[] + * @return array|string */ public function getForeignKey() { @@ -251,7 +256,7 @@ public function defaultRowValue(array $row, bool $joined): array * Sets the table instance for the junction relation. If no arguments * are passed, the current configured table instance is returned * - * @param string|\Cake\ORM\Table|null $table Name or instance for the join table + * @param \Cake\ORM\Table|string|null $table Name or instance for the join table * @return \Cake\ORM\Table * @throws \InvalidArgumentException If the expected associations are incompatible with existing associations. */ @@ -425,6 +430,7 @@ protected function _generateJunctionAssociations(Table $junction, Table $source, if (!$junction->hasAssociation($sAlias)) { $junction->belongsTo($sAlias, [ + 'bindingKey' => $this->getBindingKey(), 'foreignKey' => $this->getForeignKey(), 'targetTable' => $source, ]); @@ -445,7 +451,7 @@ protected function _generateJunctionAssociations(Table $junction, Table $source, * - type: The type of join to be used (e.g. INNER) * * @param \Cake\ORM\Query $query the query to be altered to include the target table data - * @param array $options Any extra options or overrides to be taken in account + * @param array $options Any extra options or overrides to be taken in account * @return void */ public function attachTo(Query $query, array $options = []): void @@ -461,10 +467,7 @@ public function attachTo(Query $query, array $options = []): void $cond = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]); $cond += $this->junctionConditions(); - $includeFields = null; - if (isset($options['includeFields'])) { - $includeFields = $options['includeFields']; - } + $includeFields = $options['includeFields'] ?? null; // Attach the junction table as well we need it to populate _joinData. $assoc = $this->_targetTable->getAssociation($junction->getAlias()); @@ -492,9 +495,7 @@ protected function _appendNotMatching(Query $query, array $options): void if (empty($options['negateMatch'])) { return; } - if (!isset($options['conditions'])) { - $options['conditions'] = []; - } + $options['conditions'] = $options['conditions'] ?? []; $junction = $this->junction(); $belongsTo = $junction->getAssociation($this->getSource()->getAlias()); $conds = $belongsTo->_joinCondition(['foreignKey' => $belongsTo->getForeignKey()]); @@ -539,7 +540,7 @@ public function type(): string /** * Return false as join conditions are defined in the junction table * - * @param array $options list of options passed to attachTo method + * @param array $options list of options passed to attachTo method * @return array */ protected function _joinCondition(array $options): array @@ -578,7 +579,7 @@ public function eagerLoader(array $options): Closure * Clear out the data in the junction table for a given entity. * * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascading delete. - * @param array $options The options for the original delete. + * @param array $options The options for the original delete. * @return bool Success. */ public function cascadeDelete(EntityInterface $entity, array $options = []): bool @@ -676,7 +677,7 @@ public function getSaveStrategy(): string * not deleted. * * @param \Cake\Datasource\EntityInterface $entity an entity from the source table - * @param array $options options to be passed to the save method in the target table + * @param array $options options to be passed to the save method in the target table * @throws \InvalidArgumentException if the property representing the association * in the parent entity cannot be traversed * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns @@ -716,7 +717,7 @@ public function saveAssociated(EntityInterface $entity, array $options = []) * entities to be saved. * @param array $entities list of entities to persist in target table and to * link to the parent entity - * @param array $options list of options accepted by `Table::save()` + * @param array $options list of options accepted by `Table::save()` * @throws \InvalidArgumentException if the property representing the association * in the parent entity cannot be traversed * @return \Cake\Datasource\EntityInterface|false The parent entity after all links have been @@ -725,10 +726,12 @@ public function saveAssociated(EntityInterface $entity, array $options = []) protected function _saveTarget(EntityInterface $parentEntity, array $entities, $options) { $joinAssociations = false; - if (!empty($options['associated'][$this->_junctionProperty]['associated'])) { - $joinAssociations = $options['associated'][$this->_junctionProperty]['associated']; + if (isset($options['associated']) && is_array($options['associated'])) { + if (!empty($options['associated'][$this->_junctionProperty]['associated'])) { + $joinAssociations = $options['associated'][$this->_junctionProperty]['associated']; + } + unset($options['associated'][$this->_junctionProperty]); } - unset($options['associated'][$this->_junctionProperty]); $table = $this->getTarget(); $original = $entities; @@ -778,9 +781,9 @@ protected function _saveTarget(EntityInterface $parentEntity, array $entities, $ * * @param \Cake\Datasource\EntityInterface $sourceEntity the entity from source table in this * association - * @param \Cake\Datasource\EntityInterface[] $targetEntities list of entities to link to link to the source entity using the + * @param array<\Cake\Datasource\EntityInterface> $targetEntities list of entities to link to link to the source entity using the * junction table - * @param array $options list of options accepted by `Table::save()` + * @param array $options list of options accepted by `Table::save()` * @return bool success */ protected function _saveLinks(EntityInterface $sourceEntity, array $targetEntities, array $options): bool @@ -810,7 +813,7 @@ protected function _saveLinks(EntityInterface $sourceEntity, array $targetEntiti ); // Keys were changed, the junction table record _could_ be // new. By clearing the primary key values, and marking the entity - // as new, we let save() sort out whether or not we have a new link + // as new, we let save() sort out whether we have a new link // or if we are updating an existing link. if ($changedKeys) { $joint->setNew(true); @@ -852,9 +855,9 @@ protected function _saveLinks(EntityInterface $sourceEntity, array $targetEntiti * * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side * of this association - * @param \Cake\Datasource\EntityInterface[] $targetEntities list of entities belonging to the `target` side + * @param array<\Cake\Datasource\EntityInterface> $targetEntities list of entities belonging to the `target` side * of this association - * @param array $options list of options to be passed to the internal `save` call + * @param array $options list of options to be passed to the internal `save` call * @throws \InvalidArgumentException when any of the values in $targetEntities is * detected to not be already persisted * @return bool true on success, false otherwise @@ -884,7 +887,7 @@ function () use ($sourceEntity, $targetEntities, $options) { * Additionally to the default options accepted by `Table::delete()`, the following * keys are supported: * - * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that + * - cleanProperty: Whether to remove all the objects in `$targetEntities` that * are stored in `$sourceEntity` (default: true) * * By default this method will unset each of the entity objects stored inside the @@ -902,9 +905,9 @@ function () use ($sourceEntity, $targetEntities, $options) { * * @param \Cake\Datasource\EntityInterface $sourceEntity An entity persisted in the source table for * this association. - * @param \Cake\Datasource\EntityInterface[] $targetEntities List of entities persisted in the target table for + * @param array<\Cake\Datasource\EntityInterface> $targetEntities List of entities persisted in the target table for * this association. - * @param array|bool $options List of options to be passed to the internal `delete` call, + * @param array|bool $options List of options to be passed to the internal `delete` call, * or a `boolean` as `cleanProperty` key shortcut. * @throws \InvalidArgumentException If non persisted entities are passed or if * any of them is lacking a primary key value. @@ -932,12 +935,13 @@ function () use ($sourceEntity, $targetEntities, $options): void { } ); + /** @var array<\Cake\Datasource\EntityInterface> $existing */ $existing = $sourceEntity->get($property) ?: []; if (!$options['cleanProperty'] || empty($existing)) { return true; } - /** @var \SplObjectStorage<\Cake\Datasource\EntityInterface, null> $storage*/ + /** @var \SplObjectStorage<\Cake\Datasource\EntityInterface, null> $storage */ $storage = new SplObjectStorage(); foreach ($targetEntities as $e) { $storage->attach($e); @@ -969,7 +973,7 @@ public function setConditions($conditions) /** * Sets the current join table, either the name of the Table instance or the instance itself. * - * @param string|\Cake\ORM\Table $through Name of the Table instance or the instance itself + * @param \Cake\ORM\Table|string $through Name of the Table instance or the instance itself * @return $this */ public function setThrough($through) @@ -982,7 +986,7 @@ public function setThrough($through) /** * Gets the current join table, either the name of the Table instance or the instance itself. * - * @return string|\Cake\ORM\Table + * @return \Cake\ORM\Table|string */ public function getThrough() { @@ -995,7 +999,7 @@ public function getThrough() * Any string expressions, or expression objects will * also be returned in this list. * - * @return mixed Generally an array. If the conditions + * @return array|\Closure|null Generally an array. If the conditions * are not an array, the association conditions will be * returned unmodified. */ @@ -1061,9 +1065,9 @@ protected function junctionConditions(): array * If your association includes conditions or a finder, the junction table will be * included in the query's contained associations. * - * @param string|array|null $type the type of query to perform, if an array is passed, + * @param array|string|null $type the type of query to perform, if an array is passed, * it will be interpreted as the `$options` parameter - * @param array $options The options to for the find + * @param array $options The options to for the find * @see \Cake\ORM\Table::find() * @return \Cake\ORM\Query */ @@ -1162,7 +1166,7 @@ protected function _appendJunctionJoin(Query $query, ?array $conditions = null): * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for * this association * @param array $targetEntities list of entities from the target table to be linked - * @param array $options list of options to be passed to the internal `save`/`delete` calls + * @param array $options list of options to be passed to the internal `save`/`delete` calls * when persisting/updating new links, or deleting existing ones * @throws \InvalidArgumentException if non persisted entities are passed or if * any of them is lacking a primary key value @@ -1173,7 +1177,7 @@ public function replaceLinks(EntityInterface $sourceEntity, array $targetEntitie $bindingKey = (array)$this->getBindingKey(); $primaryValue = $sourceEntity->extract($bindingKey); - if (count(array_filter($primaryValue, 'strlen')) !== count($bindingKey)) { + if (count(Hash::filter($primaryValue)) !== count($bindingKey)) { $message = 'Could not find primary key value for source entity'; throw new InvalidArgumentException($message); } @@ -1184,30 +1188,33 @@ function () use ($sourceEntity, $targetEntities, $primaryValue, $options) { $target = $this->getTarget(); $foreignKey = (array)$this->getForeignKey(); - $prefixedForeignKey = array_map([$junction, 'aliasField'], $foreignKey); + $assocForeignKey = (array)$junction->getAssociation($target->getAlias())->getForeignKey(); + $prefixedForeignKey = array_map([$junction, 'aliasField'], $foreignKey); $junctionPrimaryKey = (array)$junction->getPrimaryKey(); - $assocForeignKey = (array)$junction->getAssociation($target->getAlias())->getForeignKey(); + $junctionQueryAlias = $junction->getAlias() . '__matches'; - $keys = array_combine($foreignKey, $prefixedForeignKey); + $keys = $matchesConditions = []; foreach (array_merge($assocForeignKey, $junctionPrimaryKey) as $key) { - $keys[$key] = $junction->aliasField($key); + $aliased = $junction->aliasField($key); + $keys[$key] = $aliased; + $matchesConditions[$aliased] = new IdentifierExpression($junctionQueryAlias . '.' . $key); } - // Find existing rows so that we can diff with new entities. - // Only hydrate primary/foreign key columns to save time. - // Attach joins first to ensure where conditions have correct - // column types set. - $existing = $this->_appendJunctionJoin($this->find()) + // Use association to create row selection + // with finders & association conditions. + $matches = $this->_appendJunctionJoin($this->find()) ->select($keys) ->where(array_combine($prefixedForeignKey, $primaryValue)); - // Because we're aliasing key fields to look like they are not - // from joined table we need to overwrite the type map as the junction - // table can have a surrogate primary key that doesn't share a type - // with the target table. - $junctionTypes = array_intersect_key($junction->getSchema()->typeMap(), $keys); - $existing->getSelectTypeMap()->setTypes($junctionTypes); + // Create a subquery join to ensure we get + // the correct entity passed to callbacks. + $existing = $junction->selectQuery() + ->from([$junctionQueryAlias => $matches]) + ->innerJoin( + [$junction->getAlias() => $junction->getTable()], + $matchesConditions + ); $jointEntities = $this->_collectJointEntities($sourceEntity, $targetEntities); $inserts = $this->_diffLinks($existing, $jointEntities, $targetEntities, $options); @@ -1244,10 +1251,10 @@ function () use ($sourceEntity, $targetEntities, $primaryValue, $options) { * `$targetEntities` that were not deleted from calculating the difference. * * @param \Cake\ORM\Query $existing a query for getting existing links - * @param \Cake\Datasource\EntityInterface[] $jointEntities link entities that should be persisted + * @param array<\Cake\Datasource\EntityInterface> $jointEntities link entities that should be persisted * @param array $targetEntities entities in target table that are related to * the `$jointEntities` - * @param array $options list of options accepted by `Table::delete()` + * @param array $options list of options accepted by `Table::delete()` * @return array|false Array of entities not deleted or false in case of deletion failure for atomic saves. */ protected function _diffLinks( @@ -1263,26 +1270,42 @@ protected function _diffLinks( $assocForeignKey = (array)$belongsTo->getForeignKey(); $keys = array_merge($foreignKey, $assocForeignKey); - $deletes = $indexed = $present = []; + $deletes = $unmatchedEntityKeys = $present = []; foreach ($jointEntities as $i => $entity) { - $indexed[$i] = $entity->extract($keys); + $unmatchedEntityKeys[$i] = $entity->extract($keys); $present[$i] = array_values($entity->extract($assocForeignKey)); } - foreach ($existing as $result) { - $fields = $result->extract($keys); + foreach ($existing as $existingLink) { + $existingKeys = $existingLink->extract($keys); $found = false; - foreach ($indexed as $i => $data) { - if ($fields === $data) { - unset($indexed[$i]); + foreach ($unmatchedEntityKeys as $i => $unmatchedKeys) { + $matched = false; + foreach ($keys as $key) { + if (is_object($unmatchedKeys[$key]) && is_object($existingKeys[$key])) { + // If both sides are an object then use == so that value objects + // are seen as equivalent. + $matched = $existingKeys[$key] == $unmatchedKeys[$key]; + } else { + // Use strict equality for all other values. + $matched = $existingKeys[$key] === $unmatchedKeys[$key]; + } + // Stop checks on first failure. + if (!$matched) { + break; + } + } + if ($matched) { + // Remove the unmatched entity so we don't look at it again. + unset($unmatchedEntityKeys[$i]); $found = true; break; } } if (!$found) { - $deletes[] = $result; + $deletes[] = $existingLink; } } @@ -1315,7 +1338,7 @@ protected function _diffLinks( * * @param \Cake\Datasource\EntityInterface $sourceEntity the row belonging to the `source` side * of this association - * @param \Cake\Datasource\EntityInterface[] $targetEntities list of entities belonging to the `target` side + * @param array<\Cake\Datasource\EntityInterface> $targetEntities list of entities belonging to the `target` side * of this association * @return bool * @throws \InvalidArgumentException @@ -1347,7 +1370,7 @@ protected function _checkPersistenceStatus(EntityInterface $sourceEntity, array * association. * @throws \InvalidArgumentException if any of the entities is lacking a primary * key value - * @return \Cake\Datasource\EntityInterface[] + * @return array<\Cake\Datasource\EntityInterface> */ protected function _collectJointEntities(EntityInterface $sourceEntity, array $targetEntities): array { @@ -1452,7 +1475,7 @@ protected function _junctionTableName(?string $name = null): string /** * Parse extra options passed in the constructor. * - * @param array $options original list of options passed in constructor + * @param array $options original list of options passed in constructor * @return void */ protected function _options(array $options): void diff --git a/Association/DependentDeleteHelper.php b/Association/DependentDeleteHelper.php index 1b7a7d80..e965d5de 100644 --- a/Association/DependentDeleteHelper.php +++ b/Association/DependentDeleteHelper.php @@ -2,17 +2,17 @@ declare(strict_types=1); /** - * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) - * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * 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. (http://cakefoundation.org) - * @link http://cakephp.org CakePHP(tm) Project + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project * @since 3.5.0 - * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @license https://www.opensource.org/licenses/mit-license.php MIT License */ namespace Cake\ORM\Association; @@ -33,7 +33,7 @@ class DependentDeleteHelper * * @param \Cake\ORM\Association $association The association callbacks are being cascaded on. * @param \Cake\Datasource\EntityInterface $entity The entity that started the cascaded delete. - * @param array $options The options for the original delete. + * @param array $options The options for the original delete. * @return bool Success. */ public function cascadeDelete(Association $association, EntityInterface $entity, array $options = []): bool @@ -45,7 +45,11 @@ public function cascadeDelete(Association $association, EntityInterface $entity, /** @psalm-suppress InvalidArgument */ $foreignKey = array_map([$association, 'aliasField'], (array)$association->getForeignKey()); $bindingKey = (array)$association->getBindingKey(); - $conditions = array_combine($foreignKey, $entity->extract($bindingKey)); + $bindingValue = $entity->extract($bindingKey); + if (in_array(null, $bindingValue, true)) { + return true; + } + $conditions = array_combine($foreignKey, $bindingValue); if ($association->getCascadeCallbacks()) { foreach ($association->find()->where($conditions)->all()->toList() as $related) { diff --git a/Association/HasMany.php b/Association/HasMany.php index fa59e21f..5d95621d 100644 --- a/Association/HasMany.php +++ b/Association/HasMany.php @@ -33,6 +33,9 @@ * will have one or multiple records per each one in the source side. * * An example of a HasMany association would be Author has many Articles. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class HasMany extends Association { @@ -60,7 +63,7 @@ class HasMany extends Association /** * Valid strategies for this type of association * - * @var string[] + * @var array */ protected $_validStrategies = [ self::STRATEGY_SELECT, @@ -89,7 +92,7 @@ class HasMany extends Association protected $_saveStrategy = self::SAVE_APPEND; /** - * Returns whether or not the passed table is the owning side for this + * Returns whether the passed table is the owning side for this * association. This means that rows in the 'target' table would miss important * or required information if the row in 'source' did not exist. * @@ -137,7 +140,7 @@ public function getSaveStrategy(): string * `$options` * * @param \Cake\Datasource\EntityInterface $entity an entity from the source table - * @param array $options options to be passed to the save method in the target table + * @param array $options options to be passed to the save method in the target table * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns * the saved entity * @see \Cake\ORM\Table::save() @@ -199,7 +202,7 @@ public function saveAssociated(EntityInterface $entity, array $options = []) * entities to be saved. * @param array $entities list of entities * to persist in target table and to link to the parent entity - * @param array $options list of options accepted by `Table::save()`. + * @param array $options list of options accepted by `Table::save()`. * @return bool `true` on success, `false` otherwise. */ protected function _saveTarget( @@ -267,7 +270,7 @@ protected function _saveTarget( * of this association * @param array $targetEntities list of entities belonging to the `target` side * of this association - * @param array $options list of options to be passed to the internal `save` call + * @param array $options list of options to be passed to the internal `save` call * @return bool true on success, false otherwise */ public function link(EntityInterface $sourceEntity, array $targetEntities, array $options = []): bool @@ -311,7 +314,7 @@ public function link(EntityInterface $sourceEntity, array $targetEntities, array * Additionally to the default options accepted by `Table::delete()`, the following * keys are supported: * - * - cleanProperty: Whether or not to remove all the objects in `$targetEntities` that + * - cleanProperty: Whether to remove all the objects in `$targetEntities` that * are stored in `$sourceEntity` (default: true) * * By default this method will unset each of the entity objects stored inside the @@ -335,7 +338,7 @@ public function link(EntityInterface $sourceEntity, array $targetEntities, array * this association * @param array $targetEntities list of entities persisted in the target table for * this association - * @param array|bool $options list of options to be passed to the internal `delete` call. + * @param array|bool $options list of options to be passed to the internal `delete` call. * If boolean it will be used a value for "cleanProperty" option. * @throws \InvalidArgumentException if non persisted entities are passed or if * any of them is lacking a primary key value @@ -424,7 +427,7 @@ function ($assoc) use ($targetEntities) { * @param \Cake\Datasource\EntityInterface $sourceEntity an entity persisted in the source table for * this association * @param array $targetEntities list of entities from the target table to be linked - * @param array $options list of options to be passed to the internal `save`/`delete` calls + * @param array $options list of options to be passed to the internal `save`/`delete` calls * when persisting/updating new links, or deleting existing ones * @throws \InvalidArgumentException if non persisted entities are passed or if * any of them is lacking a primary key value @@ -456,7 +459,7 @@ public function replace(EntityInterface $sourceEntity, array $targetEntities, ar * @param \Cake\Datasource\EntityInterface $entity the entity which should have its associated entities unassigned * @param \Cake\ORM\Table $target The associated table * @param iterable $remainingEntities Entities that should not be deleted - * @param array $options list of options accepted by `Table::delete()` + * @param array $options list of options accepted by `Table::delete()` * @return bool success */ protected function _unlinkAssociated( @@ -504,7 +507,7 @@ function ($v) { * @param array $foreignKey array of foreign key properties * @param \Cake\ORM\Table $target The associated table * @param array $conditions The conditions that specifies what are the objects to be unlinked - * @param array $options list of options accepted by `Table::delete()` + * @param array $options list of options accepted by `Table::delete()` * @return bool success */ protected function _unlink(array $foreignKey, Table $target, array $conditions = [], array $options = []): bool @@ -575,7 +578,7 @@ public function type(): string /** * Whether this association can be expressed directly in a query join * - * @param array $options custom options key that could alter the return value + * @param array $options custom options key that could alter the return value * @return bool if the 'matching' key in $option is true then this function * will return true, false otherwise */ @@ -587,7 +590,7 @@ public function canBeJoined(array $options = []): bool /** * Gets the name of the field representing the foreign key to the source table. * - * @return string|string[] + * @return array|string */ public function getForeignKey() { @@ -637,7 +640,7 @@ public function defaultRowValue(array $row, bool $joined): array /** * Parse extra options passed in the constructor. * - * @param array $options original list of options passed in constructor + * @param array $options original list of options passed in constructor * @return void */ protected function _options(array $options): void diff --git a/Association/HasOne.php b/Association/HasOne.php index bd10694b..ff239477 100644 --- a/Association/HasOne.php +++ b/Association/HasOne.php @@ -22,19 +22,23 @@ use Cake\ORM\Table; use Cake\Utility\Inflector; use Closure; +use function Cake\Core\pluginSplit; /** * Represents an 1 - 1 relationship where the source side of the relation is * related to only one record in the target table and vice versa. * * An example of a HasOne association would be User has one Profile. + * + * @template T of \Cake\ORM\Table + * @mixin T */ class HasOne extends Association { /** * Valid strategies for this type of association * - * @var string[] + * @var array */ protected $_validStrategies = [ self::STRATEGY_JOIN, @@ -44,7 +48,7 @@ class HasOne extends Association /** * Gets the name of the field representing the foreign key to the target table. * - * @return string|string[] + * @return array|string */ public function getForeignKey() { @@ -68,7 +72,7 @@ protected function _propertyName(): string } /** - * Returns whether or not the passed table is the owning side for this + * Returns whether the passed table is the owning side for this * association. This means that rows in the 'target' table would miss important * or required information if the row in 'source' did not exist. * @@ -97,7 +101,7 @@ public function type(): string * `$options` * * @param \Cake\Datasource\EntityInterface $entity an entity from the source table - * @param array $options options to be passed to the save method in the target table + * @param array $options options to be passed to the save method in the target table * @return \Cake\Datasource\EntityInterface|false false if $entity could not be saved, otherwise it returns * the saved entity * @see \Cake\ORM\Table::save() diff --git a/Association/Loader/SelectLoader.php b/Association/Loader/SelectLoader.php index f3be34f5..dce3b320 100644 --- a/Association/Loader/SelectLoader.php +++ b/Association/Loader/SelectLoader.php @@ -56,7 +56,7 @@ class SelectLoader /** * The foreignKey to the target association * - * @var string|array + * @var array|string */ protected $foreignKey; @@ -99,7 +99,7 @@ class SelectLoader * Copies the options array to properties in this class. The keys in the array correspond * to properties in this class. * - * @param array $options Properties to be copied to this class + * @param array $options Properties to be copied to this class */ public function __construct(array $options) { @@ -118,7 +118,7 @@ public function __construct(array $options) * Returns a callable that can be used for injecting association results into a given * iterator. The options accepted by this method are the same as `Association::eagerLoader()` * - * @param array $options Same options as `Association::eagerLoader()` + * @param array $options Same options as `Association::eagerLoader()` * @return \Closure */ public function buildEagerLoader(array $options): Closure @@ -133,7 +133,7 @@ public function buildEagerLoader(array $options): Closure /** * Returns the default options to use for the eagerLoader * - * @return array + * @return array */ protected function _defaultOptions(): array { @@ -151,7 +151,7 @@ protected function _defaultOptions(): array * in the target table that are associated to those specified in $options from * the source table * - * @param array $options options accepted by eagerLoader() + * @param array $options options accepted by eagerLoader() * @return \Cake\ORM\Query * @throws \InvalidArgumentException When a key is required for associations but not selected. */ @@ -161,10 +161,7 @@ protected function _buildQuery(array $options): Query $filter = $options['keys']; $useSubquery = $options['strategy'] === Association::STRATEGY_SUBQUERY; $finder = $this->finder; - - if (!isset($options['fields'])) { - $options['fields'] = []; - } + $options['fields'] = $options['fields'] ?? []; /** @var \Cake\ORM\Query $query */ $query = $finder(); @@ -178,6 +175,11 @@ protected function _buildQuery(array $options): Query ->where($options['conditions']) ->eagerLoaded(true) ->enableHydration($options['query']->isHydrationEnabled()); + if ($options['query']->isResultsCastingEnabled()) { + $fetchQuery->enableResultsCasting(); + } else { + $fetchQuery->disableResultsCasting(); + } if ($useSubquery) { $filter = $this->_buildSubquery($options['query']); @@ -215,7 +217,7 @@ protected function _buildQuery(array $options): Query * $query->contain(['Comments' => ['finder' => ['translations' => []]]]); * $query->contain(['Comments' => ['finder' => ['translations' => ['locales' => ['en_US']]]]]); * - * @param string|array $finderData The finder name or an array having the name as key + * @param array|string $finderData The finder name or an array having the name as key * and options as value. * @return array */ @@ -236,12 +238,16 @@ protected function _extractFinder($finderData): array * If the required fields are missing, throws an exception. * * @param \Cake\ORM\Query $fetchQuery The association fetching query - * @param array $key The foreign key fields to check + * @param array $key The foreign key fields to check * @return void * @throws \InvalidArgumentException */ protected function _assertFieldsPresent(Query $fetchQuery, array $key): void { + if ($fetchQuery->isAutoFieldsEnabled()) { + return; + } + $select = $fetchQuery->aliasFields($fetchQuery->clause('select')); if (empty($select)) { return; @@ -279,7 +285,7 @@ protected function _assertFieldsPresent(Query $fetchQuery, array $key): void * filtering needs to be done using a subquery. * * @param \Cake\ORM\Query $query Target table's query - * @param string|string[] $key the fields that should be used for filtering + * @param array|string $key the fields that should be used for filtering * @param \Cake\ORM\Query $subquery The Subquery to use for filtering * @return \Cake\ORM\Query */ @@ -315,7 +321,7 @@ protected function _addFilteringJoin(Query $query, $key, $subquery): Query * target table query given a filter key and some filtering values. * * @param \Cake\ORM\Query $query Target table's query - * @param string|array $key The fields that should be used for filtering + * @param array|string $key The fields that should be used for filtering * @param mixed $filter The value that should be used to match for $key * @return \Cake\ORM\Query */ @@ -335,7 +341,7 @@ protected function _addFilteringCondition(Query $query, $key, $filter): Query * from $keys with the tuple values in $filter using the provided operator. * * @param \Cake\ORM\Query $query Target table's query - * @param string[] $keys the fields that should be used for filtering + * @param array $keys the fields that should be used for filtering * @param mixed $filter the value that should be used to match for $key * @param string $operator The operator for comparing the tuples * @return \Cake\Database\Expression\TupleComparison @@ -357,8 +363,8 @@ protected function _createTupleCondition(Query $query, array $keys, $filter, $op * Generates a string used as a table field that contains the values upon * which the filter should be applied * - * @param array $options The options for getting the link field. - * @return string|string[] + * @param array $options The options for getting the link field. + * @return array|string * @throws \RuntimeException */ protected function _linkField(array $options) @@ -404,7 +410,8 @@ protected function _buildSubquery(Query $query): Query $filterQuery->contain([], true); $filterQuery->setValueBinder(new ValueBinder()); - if (!$filterQuery->clause('limit')) { + // Ignore limit if there is no order since we need all rows to find matches + if (!$filterQuery->clause('limit') || !$filterQuery->clause('order')) { $filterQuery->limit(null); $filterQuery->order([], true); $filterQuery->offset(null); @@ -424,7 +431,7 @@ protected function _buildSubquery(Query $query): Query * that need to be present to ensure the correct association data is loaded. * * @param \Cake\ORM\Query $query The query to get fields from. - * @return array The list of fields for the subquery. + * @return array The list of fields for the subquery. */ protected function _subqueryFields(Query $query): array { @@ -455,8 +462,8 @@ protected function _subqueryFields(Query $query): array * the foreignKey value corresponding to this association. * * @param \Cake\ORM\Query $fetchQuery The query to get results from - * @param array $options The options passed to the eager loader - * @return array + * @param array $options The options passed to the eager loader + * @return array */ protected function _buildResultMap(Query $fetchQuery, array $options): array { @@ -487,9 +494,9 @@ protected function _buildResultMap(Query $fetchQuery, array $options): array * for injecting the eager loaded rows * * @param \Cake\ORM\Query $fetchQuery the Query used to fetch results - * @param array $resultMap an array with the foreignKey as keys and + * @param array $resultMap an array with the foreignKey as keys and * the corresponding target table results as value. - * @param array $options The options passed to the eagerLoader method + * @param array $options The options passed to the eagerLoader method * @return \Closure */ protected function _resultInjector(Query $fetchQuery, array $resultMap, array $options): Closure @@ -525,8 +532,8 @@ protected function _resultInjector(Query $fetchQuery, array $resultMap, array $o * for injecting the eager loaded rows when the matching needs to * be done with multiple foreign keys * - * @param array $resultMap A keyed arrays containing the target table - * @param string[] $sourceKeys An array with aliased keys to match + * @param array $resultMap A keyed arrays containing the target table + * @param array $sourceKeys An array with aliased keys to match * @param string $nestKey The key under which results should be nested * @return \Closure */ diff --git a/Association/Loader/SelectWithPivotLoader.php b/Association/Loader/SelectWithPivotLoader.php index 128f52f5..318a8e45 100644 --- a/Association/Loader/SelectWithPivotLoader.php +++ b/Association/Loader/SelectWithPivotLoader.php @@ -50,7 +50,7 @@ class SelectWithPivotLoader extends SelectLoader /** * Custom conditions for the junction association * - * @var string|array|\Cake\Database\ExpressionInterface|\Closure|null + * @var \Cake\Database\ExpressionInterface|\Closure|array|string|null */ protected $junctionConditions; @@ -73,7 +73,7 @@ public function __construct(array $options) * * This is used for eager loading records on the target table based on conditions. * - * @param array $options options accepted by eagerLoader() + * @param array $options options accepted by eagerLoader() * @return \Cake\ORM\Query * @throws \InvalidArgumentException When a key is required for associations but not selected. */ @@ -141,8 +141,8 @@ protected function _assertFieldsPresent(Query $fetchQuery, array $key): void * Generates a string used as a table field that contains the values upon * which the filter should be applied * - * @param array $options the options to use for getting the link field. - * @return string|string[] + * @param array $options the options to use for getting the link field. + * @return array|string */ protected function _linkField(array $options) { @@ -165,8 +165,8 @@ protected function _linkField(array $options) * the foreignKey value corresponding to this association. * * @param \Cake\ORM\Query $fetchQuery The query to get results from - * @param array $options The options passed to the eager loader - * @return array + * @param array $options The options passed to the eager loader + * @return array * @throws \RuntimeException when the association property is not part of the results set. */ protected function _buildResultMap(Query $fetchQuery, array $options): array diff --git a/AssociationCollection.php b/AssociationCollection.php index 6a025dd3..9f213145 100644 --- a/AssociationCollection.php +++ b/AssociationCollection.php @@ -23,12 +23,16 @@ use InvalidArgumentException; use IteratorAggregate; use Traversable; +use function Cake\Core\namespaceSplit; +use function Cake\Core\pluginSplit; /** * A container/collection for association classes. * * Contains methods for managing associations, and * ordering operations around saving and deleting. + * + * @template-implements \IteratorAggregate */ class AssociationCollection implements IteratorAggregate { @@ -38,7 +42,7 @@ class AssociationCollection implements IteratorAggregate /** * Stored associations * - * @var \Cake\ORM\Association[] + * @var array<\Cake\ORM\Association> */ protected $_items = []; @@ -66,6 +70,9 @@ public function __construct(?LocatorInterface $tableLocator = null) * @param string $alias The association alias * @param \Cake\ORM\Association $association The association to add. * @return \Cake\ORM\Association The association object being added. + * @template T of \Cake\ORM\Association + * @psalm-param T $association + * @psalm-return T */ public function add(string $alias, Association $association): Association { @@ -79,9 +86,12 @@ public function add(string $alias, Association $association): Association * * @param string $className The name of association class. * @param string $associated The alias for the target table. - * @param array $options List of options to configure the association definition. + * @param array $options List of options to configure the association definition. * @return \Cake\ORM\Association * @throws \InvalidArgumentException + * @template T of \Cake\ORM\Association + * @psalm-param class-string $className + * @psalm-return T */ public function load(string $className, string $associated, array $options = []): Association { @@ -90,14 +100,6 @@ public function load(string $className, string $associated, array $options = []) ]; $association = new $className($associated, $options); - if (!$association instanceof Association) { - $message = sprintf( - 'The association must extend `%s` class, `%s` given.', - Association::class, - get_class($association) - ); - throw new InvalidArgumentException($message); - } return $this->add($association->getName(), $association); } @@ -110,11 +112,7 @@ public function load(string $className, string $associated, array $options = []) */ public function get(string $alias): ?Association { - if (isset($this->_items[$alias])) { - return $this->_items[$alias]; - } - - return null; + return $this->_items[$alias] ?? null; } /** @@ -138,7 +136,7 @@ public function getByProperty(string $prop): ?Association * Check for an attached association by name. * * @param string $alias The association alias to get. - * @return bool Whether or not the association exists. + * @return bool Whether the association exists. */ public function has(string $alias): bool { @@ -148,7 +146,7 @@ public function has(string $alias): bool /** * Get the names of all the associations in the collection. * - * @return string[] + * @return array */ public function keys(): array { @@ -158,9 +156,9 @@ public function keys(): array /** * Get an array of associations matching a specific type. * - * @param string|array $class The type of associations you want. + * @param array|string $class The type of associations you want. * For example 'BelongsTo' or array like ['BelongsTo', 'HasOne'] - * @return \Cake\ORM\Association[] An array of Association objects. + * @return array<\Cake\ORM\Association> An array of Association objects. * @since 3.5.3 */ public function getByType($class): array @@ -179,7 +177,7 @@ public function getByType($class): array /** * Drop/remove an association. * - * Once removed the association will not longer be reachable + * Once removed the association will no longer be reachable * * @param string $alias The alias name. * @return void @@ -192,7 +190,7 @@ public function remove(string $alias): void /** * Remove all registered associations. * - * Once removed associations will not longer be reachable + * Once removed associations will no longer be reachable * * @return void */ @@ -213,7 +211,7 @@ public function removeAll(): void * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for. * @param array $associations The list of associations to save parents from. * associations not in this list will not be saved. - * @param array $options The options for the save operation. + * @param array $options The options for the save operation. * @return bool Success */ public function saveParents(Table $table, EntityInterface $entity, array $associations, array $options = []): bool @@ -235,7 +233,7 @@ public function saveParents(Table $table, EntityInterface $entity, array $associ * @param \Cake\Datasource\EntityInterface $entity The entity to save associated data for. * @param array $associations The list of associations to save children from. * associations not in this list will not be saved. - * @param array $options The options for the save operation. + * @param array $options The options for the save operation. * @return bool Success */ public function saveChildren(Table $table, EntityInterface $entity, array $associations, array $options): bool @@ -253,7 +251,7 @@ public function saveChildren(Table $table, EntityInterface $entity, array $assoc * @param \Cake\ORM\Table $table The table the save is currently operating on * @param \Cake\Datasource\EntityInterface $entity The entity to save * @param array $associations Array of associations to save. - * @param array $options Original options + * @param array $options Original options * @param bool $owningSide Compared with association classes' * isOwningSide method. * @return bool Success @@ -297,8 +295,8 @@ protected function _saveAssociations( * * @param \Cake\ORM\Association $association The association object to save with. * @param \Cake\Datasource\EntityInterface $entity The entity to save - * @param array $nested Options for deeper associations - * @param array $options Original options + * @param array $nested Options for deeper associations + * @param array $options Original options * @return bool Success */ protected function _save( @@ -322,7 +320,7 @@ protected function _save( * Cascade first across associations for which cascadeCallbacks is true. * * @param \Cake\Datasource\EntityInterface $entity The entity to delete associations for. - * @param array $options The options used in the delete operation. + * @param array $options The options used in the delete operation. * @return bool */ public function cascadeDelete(EntityInterface $entity, array $options): bool @@ -354,7 +352,7 @@ public function cascadeDelete(EntityInterface $entity, array $options): bool * array. If true is passed, then it returns all association names * in this collection. * - * @param bool|array $keys the list of association names to normalize + * @param array|bool $keys the list of association names to normalize * @return array */ public function normalizeKeys($keys): array @@ -373,8 +371,7 @@ public function normalizeKeys($keys): array /** * Allow looping through the associations * - * @return \Cake\ORM\Association[] - * @psalm-return \Traversable + * @return \Traversable */ public function getIterator(): Traversable { diff --git a/Behavior.php b/Behavior.php index 1f7939ab..48577088 100644 --- a/Behavior.php +++ b/Behavior.php @@ -21,6 +21,7 @@ use Cake\Event\EventListenerInterface; use ReflectionClass; use ReflectionMethod; +use function Cake\Core\deprecationWarning; /** * Base class for behaviors. @@ -46,7 +47,7 @@ * * ### Callback methods * - * Behaviors can listen to any events fired on a Table. By default + * Behaviors can listen to any events fired on a Table. By default, * CakePHP provides a number of lifecycle events your behaviors can * listen to: * @@ -54,7 +55,7 @@ * Fired before each find operation. By stopping the event and supplying a * return value you can bypass the find operation entirely. Any changes done * to the $query instance will be retained for the rest of the find. The - * $primary parameter indicates whether or not this is the root query, + * $primary parameter indicates whether this is the root query, * or an associated query. * * - `buildValidator(EventInterface $event, Validator $validator, string $name)` @@ -129,7 +130,7 @@ class Behavior implements EventListenerInterface * Stores the reflected method + finder methods per class. * This prevents reflecting the same class multiple times in a single process. * - * @var array + * @var array */ protected static $_reflectionCache = []; @@ -138,7 +139,7 @@ class Behavior implements EventListenerInterface * * These are merged with user-provided configuration when the behavior is used. * - * @var array + * @var array */ protected $_defaultConfig = []; @@ -148,7 +149,7 @@ class Behavior implements EventListenerInterface * Merges config with the default and store in the config property * * @param \Cake\ORM\Table $table The table this behavior is attached to. - * @param array $config The config for this behavior. + * @param array $config The config for this behavior. */ public function __construct(Table $table, array $config = []) { @@ -173,7 +174,7 @@ public function __construct(Table $table, array $config = []) * Implement this method to avoid having to overwrite * the constructor and call parent. * - * @param array $config The configuration settings provided to this behavior. + * @param array $config The configuration settings provided to this behavior. * @return void */ public function initialize(array $config): void @@ -207,8 +208,8 @@ public function table(): Table * Removes aliased methods that would otherwise be duplicated by userland configuration. * * @param string $key The key to filter. - * @param array $defaults The default method mappings. - * @param array $config The customized method mappings. + * @param array $defaults The default method mappings. + * @param array $config The customized method mappings. * @return array A de-duped list of config data. */ protected function _resolveMethodAliases(string $key, array $defaults, array $config): array @@ -273,7 +274,7 @@ public function verifyConfig(): void * Override this method if you need to add non-conventional event listeners. * Or if you want your behavior to listen to non-standard events. * - * @return array + * @return array */ public function implementedEvents(): array { diff --git a/Behavior/CounterCacheBehavior.php b/Behavior/CounterCacheBehavior.php index 68746fb0..ce851146 100644 --- a/Behavior/CounterCacheBehavior.php +++ b/Behavior/CounterCacheBehavior.php @@ -106,7 +106,7 @@ class CounterCacheBehavior extends Behavior /** * Store the fields which should be ignored * - * @var array + * @var array> */ protected $_ignoreDirty = []; @@ -208,7 +208,7 @@ protected function _processAssociations(EventInterface $event, EntityInterface $ * @param \Cake\Event\EventInterface $event Event instance. * @param \Cake\Datasource\EntityInterface $entity Entity * @param \Cake\ORM\Association $assoc The association object - * @param array $settings The settings for for counter cache for this association + * @param array $settings The settings for counter cache for this association * @return void * @throws \RuntimeException If invalid callable is passed. */ @@ -289,7 +289,7 @@ protected function _shouldUpdateCount(array $conditions) /** * Fetches and returns the count for a single field in an association * - * @param array $config The counter cache configuration for a single field + * @param array $config The counter cache configuration for a single field * @param array $conditions Additional conditions given to the query * @return int The number of relations matching the given config and conditions */ @@ -301,10 +301,7 @@ protected function _getCount(array $config, array $conditions): int unset($config['finder']); } - if (!isset($config['conditions'])) { - $config['conditions'] = []; - } - $config['conditions'] = array_merge($conditions, $config['conditions']); + $config['conditions'] = array_merge($conditions, $config['conditions'] ?? []); $query = $this->_table->find($finder, $config); return $query->count(); diff --git a/Behavior/TimestampBehavior.php b/Behavior/TimestampBehavior.php index d2b6f7f8..57e3ec4e 100644 --- a/Behavior/TimestampBehavior.php +++ b/Behavior/TimestampBehavior.php @@ -20,7 +20,7 @@ use Cake\Database\TypeFactory; use Cake\Datasource\EntityInterface; use Cake\Event\EventInterface; -use Cake\I18n\Time; +use Cake\I18n\FrozenTime; use Cake\ORM\Behavior; use DateTimeInterface; use RuntimeException; @@ -44,7 +44,7 @@ class TimestampBehavior extends Behavior * the code is executed, to set to an explicit date time value - set refreshTimetamp to false * and call setTimestamp() on the behavior class before use. * - * @var array + * @var array */ protected $_defaultConfig = [ 'implementedFinders' => [], @@ -64,7 +64,7 @@ class TimestampBehavior extends Behavior /** * Current timestamp * - * @var \Cake\I18n\Time|null + * @var \Cake\I18n\FrozenTime|null */ protected $_ts; @@ -74,7 +74,7 @@ class TimestampBehavior extends Behavior * If events are specified - do *not* merge them with existing events, * overwrite the events to listen on * - * @param array $config The config for this behavior. + * @param array $config The config for this behavior. * @return void */ public function initialize(array $config): void @@ -131,7 +131,7 @@ public function handleEvent(EventInterface $event, EntityInterface $entity): boo * * The implemented events of this behavior depend on configuration * - * @return array + * @return array */ public function implementedEvents(): array { @@ -147,7 +147,7 @@ public function implementedEvents(): array * * @param \DateTimeInterface|null $ts Timestamp * @param bool $refreshTimestamp If true timestamp is refreshed. - * @return \Cake\I18n\Time + * @return \Cake\I18n\FrozenTime */ public function timestamp(?DateTimeInterface $ts = null, bool $refreshTimestamp = false): DateTimeInterface { @@ -155,9 +155,9 @@ public function timestamp(?DateTimeInterface $ts = null, bool $refreshTimestamp if ($this->_config['refreshTimestamp']) { $this->_config['refreshTimestamp'] = false; } - $this->_ts = new Time($ts); + $this->_ts = new FrozenTime($ts); } elseif ($this->_ts === null || $refreshTimestamp) { - $this->_ts = new Time(); + $this->_ts = new FrozenTime(); } return $this->_ts; diff --git a/Behavior/Translate/EavStrategy.php b/Behavior/Translate/EavStrategy.php index 2a9f0e46..4b1e89ec 100644 --- a/Behavior/Translate/EavStrategy.php +++ b/Behavior/Translate/EavStrategy.php @@ -52,7 +52,7 @@ class EavStrategy implements TranslateStrategyInterface * * These are merged with user-provided configuration. * - * @var array + * @var array */ protected $_defaultConfig = [ 'fields' => [], @@ -70,7 +70,7 @@ class EavStrategy implements TranslateStrategyInterface * Constructor * * @param \Cake\ORM\Table $table The table this strategy is attached to. - * @param array $config The config for this strategy. + * @param array $config The config for this strategy. */ public function __construct(Table $table, array $config = []) { diff --git a/Behavior/Translate/ShadowTableStrategy.php b/Behavior/Translate/ShadowTableStrategy.php index efcfecee..2408090b 100644 --- a/Behavior/Translate/ShadowTableStrategy.php +++ b/Behavior/Translate/ShadowTableStrategy.php @@ -27,6 +27,7 @@ use Cake\ORM\Query; use Cake\ORM\Table; use Cake\Utility\Hash; +use function Cake\Core\pluginSplit; /** * This class provides a way to translate dynamic data by keeping translations @@ -45,7 +46,7 @@ class ShadowTableStrategy implements TranslateStrategyInterface * * These are merged with user-provided configuration. * - * @var array + * @var array */ protected $_defaultConfig = [ 'fields' => [], @@ -62,7 +63,7 @@ class ShadowTableStrategy implements TranslateStrategyInterface * Constructor * * @param \Cake\ORM\Table $table Table instance. - * @param array $config Configuration. + * @param array $config Configuration. */ public function __construct(Table $table, array $config = []) { @@ -135,7 +136,10 @@ public function beforeFind(EventInterface $event, Query $query, ArrayObject $opt $fieldsAdded = $this->addFieldsToQuery($query, $config); $orderByTranslatedField = $this->iterateClause($query, 'order', $config); - $filteredByTranslatedField = $this->traverseClause($query, 'where', $config); + $filteredByTranslatedField = + $this->traverseClause($query, 'where', $config) || + $config['onlyTranslated'] || + ($options['filterByCurrentLocale'] ?? null); if (!$fieldsAdded && !$orderByTranslatedField && !$filteredByTranslatedField) { return; @@ -199,7 +203,7 @@ protected function setupHasOneAssociation(string $locale, ArrayObject $options): * add the locale field though. * * @param \Cake\ORM\Query $query The query to check. - * @param array $config The config to use for adding fields. + * @param array $config The config to use for adding fields. * @return bool Whether a join to the translation table is required. */ protected function addFieldsToQuery($query, array $config) @@ -241,7 +245,7 @@ protected function addFieldsToQuery($query, array $config) * * @param \Cake\ORM\Query $query the query to check. * @param string $name The clause name. - * @param array $config The config to use for adding fields. + * @param array $config The config to use for adding fields. * @return bool Whether a join to the translation table is required. */ protected function iterateClause($query, $name = '', $config = []): bool @@ -286,7 +290,7 @@ function ($c, &$field) use ($fields, $alias, $mainTableAlias, $mainTableFields, * * @param \Cake\ORM\Query $query the query to check. * @param string $name The clause name. - * @param array $config The config to use for adding fields. + * @param array $config The config to use for adding fields. * @return bool Whether a join to the translation table is required. */ protected function traverseClause($query, $name = '', $config = []): bool @@ -532,7 +536,10 @@ protected function rowMapper($results, $locale) public function groupTranslations($results): CollectionInterface { return $results->map(function ($row) { - $translations = (array)$row['_i18n']; + if (!($row instanceof EntityInterface)) { + return $row; + } + $translations = (array)$row->get('_i18n'); if (empty($translations) && $row->get('_translations')) { return $row; } @@ -588,7 +595,7 @@ protected function bundleTranslatedFields($entity) /** * Lazy define and return the main table fields. * - * @return array + * @return array */ protected function mainFields() { @@ -608,7 +615,7 @@ protected function mainFields() /** * Lazy define and return the translation table fields. * - * @return array + * @return array */ protected function translatedFields() { diff --git a/Behavior/Translate/TranslateStrategyInterface.php b/Behavior/Translate/TranslateStrategyInterface.php index 9f8d9a20..41536e6c 100644 --- a/Behavior/Translate/TranslateStrategyInterface.php +++ b/Behavior/Translate/TranslateStrategyInterface.php @@ -2,17 +2,17 @@ declare(strict_types=1); /** - * CakePHP(tm) : Rapid Development Framework (http://cakephp.org) - * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org) + * 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. (http://cakefoundation.org) - * @link http://cakephp.org CakePHP(tm) Project + * @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org) + * @link https://cakephp.org CakePHP(tm) Project * @since 4.0.0 - * @license http://www.opensource.org/licenses/mit-license.php MIT License + * @license https://www.opensource.org/licenses/mit-license.php MIT License */ namespace Cake\ORM\Behavior\Translate; diff --git a/Behavior/Translate/TranslateStrategyTrait.php b/Behavior/Translate/TranslateStrategyTrait.php index ea1fc290..bdfbb9b8 100644 --- a/Behavior/Translate/TranslateStrategyTrait.php +++ b/Behavior/Translate/TranslateStrategyTrait.php @@ -106,7 +106,7 @@ public function getLocale(): string */ protected function unsetEmptyFields($entity) { - /** @var \Cake\ORM\Entity[] $translations */ + /** @var array<\Cake\ORM\Entity> $translations */ $translations = (array)$entity->get('_translations'); foreach ($translations as $locale => $translation) { $fields = $translation->extract($this->_config['fields'], false); @@ -141,7 +141,7 @@ protected function unsetEmptyFields($entity) * * @param \Cake\ORM\Marshaller $marshaller The marhshaller of the table the behavior is attached to. * @param array $map The property map being built. - * @param array $options The options array used in the marshalling call. + * @param array $options The options array used in the marshalling call. * @return array A map of `[property => callable]` of additional properties to marshal. */ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array @@ -178,7 +178,7 @@ public function buildMarshalMap(Marshaller $marshaller, array $map, array $optio // Set errors into the root entity, so validation errors match the original form data position. if ($errors) { - $entity->setErrors($errors); + $entity->setErrors(['_translations' => $errors]); } return $translations; diff --git a/Behavior/Translate/TranslateTrait.php b/Behavior/Translate/TranslateTrait.php index 23670a31..95741243 100644 --- a/Behavior/Translate/TranslateTrait.php +++ b/Behavior/Translate/TranslateTrait.php @@ -31,7 +31,7 @@ trait TranslateTrait * it. * * @param string $language Language to return entity for. - * @return $this|\Cake\Datasource\EntityInterface + * @return \Cake\Datasource\EntityInterface|$this */ public function translation(string $language) { diff --git a/Behavior/TranslateBehavior.php b/Behavior/TranslateBehavior.php index 5daecbb6..576ea890 100644 --- a/Behavior/TranslateBehavior.php +++ b/Behavior/TranslateBehavior.php @@ -25,6 +25,7 @@ use Cake\ORM\Query; use Cake\ORM\Table; use Cake\Utility\Inflector; +use function Cake\Core\namespaceSplit; /** * This behavior provides a way to translate dynamic data by keeping translations @@ -45,7 +46,7 @@ class TranslateBehavior extends Behavior implements PropertyMarshalInterface * * These are merged with user-provided configuration when the behavior is used. * - * @var array + * @var array */ protected $_defaultConfig = [ 'implementedFinders' => ['translations' => 'findTranslations'], @@ -103,7 +104,7 @@ class TranslateBehavior extends Behavior implements PropertyMarshalInterface * are created/modified. Default `null`. * * @param \Cake\ORM\Table $table The table this behavior is attached to. - * @param array $config The config for this behavior. + * @param array $config The config for this behavior. */ public function __construct(Table $table, array $config = []) { @@ -119,7 +120,7 @@ public function __construct(Table $table, array $config = []) /** * Initialize hook * - * @param array $config The config for this behavior. + * @param array $config The config for this behavior. * @return void */ public function initialize(array $config): void @@ -202,7 +203,7 @@ public function setStrategy(TranslateStrategyInterface $strategy) /** * Gets the Model callbacks this behavior is interested in. * - * @return array + * @return array */ public function implementedEvents(): array { @@ -222,7 +223,7 @@ public function implementedEvents(): array * * @param \Cake\ORM\Marshaller $marshaller The marhshaller of the table the behavior is attached to. * @param array $map The property map being built. - * @param array $options The options array used in the marshalling call. + * @param array $options The options array used in the marshalling call. * @return array A map of `[property => callable]` of additional properties to marshal. */ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array @@ -306,7 +307,7 @@ public function translationField(string $field): string * for each record. * * @param \Cake\ORM\Query $query The original query to modify - * @param array $options Options + * @param array $options Options * @return \Cake\ORM\Query */ public function findTranslations(Query $query, array $options): Query diff --git a/Behavior/TreeBehavior.php b/Behavior/TreeBehavior.php index af4f3282..493bea37 100644 --- a/Behavior/TreeBehavior.php +++ b/Behavior/TreeBehavior.php @@ -18,6 +18,7 @@ use Cake\Collection\CollectionInterface; use Cake\Database\Expression\IdentifierExpression; +use Cake\Database\Expression\QueryExpression; use Cake\Datasource\EntityInterface; use Cake\Datasource\Exception\RecordNotFoundException; use Cake\Event\EventInterface; @@ -52,7 +53,7 @@ class TreeBehavior extends Behavior * * These are merged with user-provided configuration when the behavior is used. * - * @var array + * @var array */ protected $_defaultConfig = [ 'implementedFinders' => [ @@ -75,6 +76,7 @@ class TreeBehavior extends Behavior 'scope' => null, 'level' => null, 'recoverOrder' => null, + 'cascadeCallbacks' => false, ]; /** @@ -226,16 +228,27 @@ public function beforeDelete(EventInterface $event, EntityInterface $entity) $diff = $right - $left + 1; if ($diff > 2) { - $query = $this->_scope($this->_table->query()) - ->delete() - ->where(function ($exp) use ($config, $left, $right) { - /** @var \Cake\Database\Expression\QueryExpression $exp */ - return $exp - ->gte($config['leftField'], $left + 1) - ->lte($config['leftField'], $right - 1); - }); - $statement = $query->execute(); - $statement->closeCursor(); + if ($this->getConfig('cascadeCallbacks')) { + $query = $this->_scope($this->_table->selectQuery()) + ->where(function (QueryExpression $exp) use ($config, $left, $right) { + return $exp + ->gte($config['leftField'], $left + 1) + ->lte($config['leftField'], $right - 1); + }); + $entities = $query->toArray(); + foreach ($entities as $entityToDelete) { + $this->_table->delete($entityToDelete, ['atomic' => false]); + } + } else { + $query = $this->_scope($this->_table->deleteQuery()) + ->where(function (QueryExpression $exp) use ($config, $left, $right) { + return $exp + ->gte($config['leftField'], $left + 1) + ->lte($config['leftField'], $right - 1); + }); + $statement = $query->execute(); + $statement->closeCursor(); + } } $this->_sync($diff, '-', "> {$right}"); @@ -371,7 +384,7 @@ function ($exp) use ($config) { * is passed in the options containing the id of the node to get its path for. * * @param \Cake\ORM\Query $query The constructed query to modify - * @param array $options the list of options for the query + * @param array $options the list of options for the query * @return \Cake\ORM\Query * @throws \InvalidArgumentException If the 'for' key is missing in options */ @@ -435,7 +448,7 @@ public function childCount(EntityInterface $node, bool $direct = false): int * If the direct option is set to true, only the direct children are returned (based upon the parent_id field) * * @param \Cake\ORM\Query $query Query. - * @param array $options Array of options as described above + * @param array $options Array of options as described above * @return \Cake\ORM\Query * @throws \InvalidArgumentException When the 'for' key is not passed in $options */ @@ -487,7 +500,7 @@ function ($field) { * - spacer: A string to be used as prefix for denoting the depth in the tree for each item * * @param \Cake\ORM\Query $query Query. - * @param array $options Array of options as described above. + * @param array $options Array of options as described above. * @return \Cake\ORM\Query */ public function findTreeList(Query $query, array $options): Query @@ -517,7 +530,7 @@ public function findTreeList(Query $query, array $options): Query * - spacer: A string to be used as prefix for denoting the depth in the tree for each item. * * @param \Cake\ORM\Query $query The query object to format. - * @param array $options Array of options as described above. + * @param array $options Array of options as described above. * @return \Cake\ORM\Query Augmented query. */ public function formatTreeList(Query $query, array $options = []): Query @@ -601,12 +614,12 @@ protected function _removeFromTree(EntityInterface $node) * Reorders the node without changing its parent. * * If the node is the first child, or is a top level node with no previous node - * this method will return false + * this method will return the same node without any changes * * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|true $number How many places to move the node, or true to move to first position * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found - * @return \Cake\Datasource\EntityInterface|false $node The node after being moved or false on failure + * @return \Cake\Datasource\EntityInterface|false $node The node after being moved or false if `$number` is < 1 */ public function moveUp(EntityInterface $node, $number = 1) { @@ -626,7 +639,7 @@ public function moveUp(EntityInterface $node, $number = 1) * * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|true $number How many places to move the node, or true to move to first position - * @return \Cake\Datasource\EntityInterface $node The node after being moved or false on failure + * @return \Cake\Datasource\EntityInterface $node The node after being moved * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found */ protected function _moveUp(EntityInterface $node, $number): EntityInterface @@ -693,12 +706,12 @@ protected function _moveUp(EntityInterface $node, $number): EntityInterface * Reorders the node without changing the parent. * * If the node is the last child, or is a top level node with no subsequent node - * this method will return false + * this method will return the same node without any changes * * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|true $number How many places to move the node or true to move to last position * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found - * @return \Cake\Datasource\EntityInterface|false the entity after being moved or false on failure + * @return \Cake\Datasource\EntityInterface|false the entity after being moved or false if `$number` is < 1 */ public function moveDown(EntityInterface $node, $number = 1) { @@ -718,7 +731,7 @@ public function moveDown(EntityInterface $node, $number = 1) * * @param \Cake\Datasource\EntityInterface $node The node to move * @param int|true $number How many places to move the node, or true to move to last position - * @return \Cake\Datasource\EntityInterface $node The node after being moved or false on failure + * @return \Cake\Datasource\EntityInterface $node The node after being moved * @throws \Cake\Datasource\Exception\RecordNotFoundException When node was not found */ protected function _moveDown(EntityInterface $node, $number): EntityInterface @@ -828,47 +841,41 @@ public function recover(): void /** * Recursive method used to recover a single level of the tree * - * @param int $counter The Last left column value that was assigned + * @param int $lftRght The starting lft/rght value * @param mixed $parentId the parent id of the level to be recovered * @param int $level Node level - * @return int The next value to use for the left column + * @return int The next lftRght value */ - protected function _recoverTree(int $counter = 0, $parentId = null, $level = -1): int + protected function _recoverTree(int $lftRght = 1, $parentId = null, $level = 0): int { $config = $this->getConfig(); [$parent, $left, $right] = [$config['parent'], $config['left'], $config['right']]; $primaryKey = $this->_getPrimaryKey(); - $aliasedPrimaryKey = $this->_table->aliasField($primaryKey); - $order = $config['recoverOrder'] ?: $aliasedPrimaryKey; + $order = $config['recoverOrder'] ?: $primaryKey; - $query = $this->_scope($this->_table->query()) - ->select([$aliasedPrimaryKey]) - ->where([$this->_table->aliasField($parent) . ' IS' => $parentId]) + $nodes = $this->_scope($this->_table->selectQuery()) + ->select($primaryKey) + ->where([$parent . ' IS' => $parentId]) ->order($order) - ->disableHydration(); + ->disableHydration() + ->all(); - $leftCounter = $counter; - $nextLevel = $level + 1; - foreach ($query as $row) { - $counter++; - $counter = $this->_recoverTree($counter, $row[$primaryKey], $nextLevel); - } + foreach ($nodes as $node) { + $nodeLft = $lftRght++; + $lftRght = $this->_recoverTree($lftRght, $node[$primaryKey], $level + 1); - if ($parentId === null) { - return $counter; - } + $fields = [$left => $nodeLft, $right => $lftRght++]; + if ($config['level']) { + $fields[$config['level']] = $level; + } - $fields = [$left => $leftCounter, $right => $counter + 1]; - if ($config['level']) { - $fields[$config['level']] = $level; + $this->_table->updateAll( + $fields, + [$primaryKey => $node[$primaryKey]] + ); } - $this->_table->updateAll( - $fields, - [$primaryKey => $parentId] - ); - - return $counter + 1; + return $lftRght; } /** @@ -909,7 +916,7 @@ protected function _sync(int $shift, string $dir, string $conditions, bool $mark $config = $this->_config; foreach ([$config['leftField'], $config['rightField']] as $field) { - $query = $this->_scope($this->_table->query()); + $query = $this->_scope($this->_table->updateQuery()); $exp = $query->newExpr(); $movement = clone $exp; @@ -923,10 +930,7 @@ protected function _sync(int $shift, string $dir, string $conditions, bool $mark $where = clone $exp; $where->add($field)->add($conditions)->setConjunction(''); - $query->update() - ->set($exp->eq($field, $movement)) - ->where($where); - + $query->set($exp->eq($field, $movement))->where($where); $query->execute()->closeCursor(); } } @@ -968,7 +972,7 @@ protected function _ensureFields(EntityInterface $entity): void return; } - $fresh = $this->_table->get($entity->get($this->_getPrimaryKey()), $fields); + $fresh = $this->_table->get($entity->get($this->_getPrimaryKey())); $entity->set($fresh->extract($fields), ['guard' => false]); foreach ($fields as $field) { @@ -994,7 +998,7 @@ protected function _getPrimaryKey(): string /** * Returns the depth level of a node in the tree. * - * @param int|string|\Cake\Datasource\EntityInterface $entity The entity or primary key get the level of. + * @param \Cake\Datasource\EntityInterface|string|int $entity The entity or primary key get the level of. * @return int|false Integer of the level or false if the node does not exist. */ public function getLevel($entity) diff --git a/BehaviorRegistry.php b/BehaviorRegistry.php index 7f7b2c2c..9f006a1b 100644 --- a/BehaviorRegistry.php +++ b/BehaviorRegistry.php @@ -46,14 +46,14 @@ class BehaviorRegistry extends ObjectRegistry implements EventDispatcherInterfac /** * Method mappings. * - * @var array + * @var array */ protected $_methodMap = []; /** * Finder method mappings. * - * @var array + * @var array */ protected $_finderMap = []; @@ -135,7 +135,7 @@ protected function _throwMissingClassError(string $class, ?string $plugin): void * * @param string $class The classname that is missing. * @param string $alias The alias of the object. - * @param array $config An array of config to use for the behavior. + * @param array $config An array of config to use for the behavior. * @return \Cake\ORM\Behavior The constructed behavior class. * @psalm-suppress MoreSpecificImplementedParamType */ @@ -203,6 +203,31 @@ protected function _getMethods(Behavior $instance, string $class, string $alias) return compact('methods', 'finders'); } + /** + * Remove an object from the registry. + * + * If this registry has an event manager, the object will be detached from any events as well. + * + * @param string $name The name of the object to remove from the registry. + * @return $this + */ + public function unload(string $name) + { + $instance = $this->get($name); + $result = parent::unload($name); + + $methods = array_change_key_case($instance->implementedMethods()); + foreach (array_keys($methods) as $method) { + unset($this->_methodMap[$method]); + } + $finders = array_change_key_case($instance->implementedFinders()); + foreach (array_keys($finders) as $finder) { + unset($this->_finderMap[$finder]); + } + + return $result; + } + /** * Check if any loaded behavior implements a method. * diff --git a/EagerLoadable.php b/EagerLoadable.php index c6e98c88..fc5e226f 100644 --- a/EagerLoadable.php +++ b/EagerLoadable.php @@ -36,7 +36,7 @@ class EagerLoadable /** * A list of other associations to load from this level. * - * @var \Cake\ORM\EagerLoadable[] + * @var array<\Cake\ORM\EagerLoadable> */ protected $_associations = []; @@ -51,7 +51,7 @@ class EagerLoadable * A list of options to pass to the association object for loading * the records. * - * @var array + * @var array */ protected $_config = []; @@ -80,14 +80,14 @@ class EagerLoadable protected $_propertyPath; /** - * Whether or not this level can be fetched using a join. + * Whether this level can be fetched using a join. * * @var bool */ protected $_canBeJoined = false; /** - * Whether or not this level was meant for a "matching" fetch + * Whether this level was meant for a "matching" fetch * operation * * @var bool|null @@ -126,7 +126,7 @@ class EagerLoadable * The keys maps to the settable properties in this class. * * @param string $name The Association name. - * @param array $config The list of properties to set. + * @param array $config The list of properties to set. */ public function __construct(string $name, array $config = []) { @@ -157,7 +157,7 @@ public function addAssociation(string $name, EagerLoadable $association): void /** * Returns the Association class instance to use for loading the records. * - * @return \Cake\ORM\EagerLoadable[] + * @return array<\Cake\ORM\EagerLoadable> */ public function associations(): array { @@ -210,7 +210,7 @@ public function propertyPath(): ?string } /** - * Sets whether or not this level can be fetched using a join. + * Sets whether this level can be fetched using a join. * * @param bool $possible The value to set. * @return $this @@ -223,7 +223,7 @@ public function setCanBeJoined(bool $possible) } /** - * Gets whether or not this level can be fetched using a join. + * Gets whether this level can be fetched using a join. * * @return bool */ @@ -236,7 +236,7 @@ public function canBeJoined(): bool * Sets the list of options to pass to the association object for loading * the records. * - * @param array $config The value to set. + * @param array $config The value to set. * @return $this */ public function setConfig(array $config) @@ -250,7 +250,7 @@ public function setConfig(array $config) * Gets the list of options to pass to the association object for loading * the records. * - * @return array + * @return array */ public function getConfig(): array { @@ -258,7 +258,7 @@ public function getConfig(): array } /** - * Gets whether or not this level was meant for a + * Gets whether this level was meant for a * "matching" fetch operation. * * @return bool|null @@ -291,7 +291,7 @@ public function targetProperty(): ?string * Returns a representation of this object that can be passed to * Cake\ORM\EagerLoader::contain() * - * @return array + * @return array */ public function asContainArray(): array { diff --git a/EagerLoader.php b/EagerLoader.php index 671acfa3..d4fa10a2 100644 --- a/EagerLoader.php +++ b/EagerLoader.php @@ -34,7 +34,7 @@ class EagerLoader * Nested array describing the association to be fetched * and the options to apply for each of them, if any * - * @var array + * @var array */ protected $_containments = []; @@ -42,7 +42,7 @@ class EagerLoader * Contains a nested array with the compiled containments tree * This is a normalized version of the user provided containments array. * - * @var \Cake\ORM\EagerLoadable[]|\Cake\ORM\EagerLoadable|null + * @var \Cake\ORM\EagerLoadable|array<\Cake\ORM\EagerLoadable>|null */ protected $_normalized; @@ -50,7 +50,7 @@ class EagerLoader * List of options accepted by associations in contain() * index by key for faster access * - * @var array + * @var array */ protected $_containOptions = [ 'associations' => 1, @@ -69,7 +69,7 @@ class EagerLoader /** * A list of associations that should be loaded with a separate query * - * @var \Cake\ORM\EagerLoadable[] + * @var array<\Cake\ORM\EagerLoadable> */ protected $_loadExternal = []; @@ -91,12 +91,12 @@ class EagerLoader * A map of table aliases pointing to the association objects they represent * for the query. * - * @var array + * @var array */ protected $_joinsMap = []; /** - * Controls whether or not fields from associated tables + * Controls whether fields from associated tables * will be eagerly loaded. When set to false, no fields will * be loaded from associations. * @@ -137,7 +137,7 @@ public function contain($associations, ?callable $queryBuilder = null): array if ($queryBuilder) { if (!is_string($associations)) { throw new InvalidArgumentException( - sprintf('Cannot set containments. To use $queryBuilder, $associations must be a string') + 'Cannot set containments. To use $queryBuilder, $associations must be a string' ); } @@ -187,7 +187,7 @@ public function clearContain(): void } /** - * Sets whether or not contained associations will load fields automatically. + * Sets whether contained associations will load fields automatically. * * @param bool $enable The value to set. * @return $this @@ -212,7 +212,7 @@ public function disableAutoFields() } /** - * Gets whether or not contained associations will load fields automatically. + * Gets whether contained associations will load fields automatically. * * @return bool The current value. */ @@ -230,41 +230,37 @@ public function isAutoFieldsEnabled(): bool * * ### Options * - * - 'joinType': INNER, OUTER, ... - * - 'fields': Fields to contain + * - `joinType`: INNER, OUTER, ... + * - `fields`: Fields to contain + * - `negateMatch`: Whether to add conditions negate match on target association * - * @param string $assoc A single association or a dot separated path of associations. + * @param string $associationPath Dot separated association path, 'Name1.Name2.Name3' * @param callable|null $builder the callback function to be used for setting extra * options to the filtering query - * @param array $options Extra options for the association matching. + * @param array $options Extra options for the association matching. * @return $this */ - public function setMatching(string $assoc, ?callable $builder = null, array $options = []) + public function setMatching(string $associationPath, ?callable $builder = null, array $options = []) { if ($this->_matching === null) { $this->_matching = new static(); } - if (!isset($options['joinType'])) { - $options['joinType'] = Query::JOIN_TYPE_INNER; - } + $options += ['joinType' => Query::JOIN_TYPE_INNER]; + $sharedOptions = ['negateMatch' => false, 'matching' => true] + $options; - $assocs = explode('.', $assoc); - $last = array_pop($assocs); - $containments = []; - $pointer = &$containments; - $opts = ['matching' => true] + $options; - /** @psalm-suppress InvalidArrayOffset */ - unset($opts['negateMatch']); - - foreach ($assocs as $name) { - $pointer[$name] = $opts; - $pointer = &$pointer[$name]; + $contains = []; + $nested = &$contains; + foreach (explode('.', $associationPath) as $association) { + // Add contain to parent contain using association name as key + $nested[$association] = $sharedOptions; + // Set to next nested level + $nested = &$nested[$association]; } - $pointer[$last] = ['queryBuilder' => $builder, 'matching' => true] + $options; - - $this->_matching->contain($containments); + // Add all options to target association contain which is the last in nested chain + $nested = ['matching' => true, 'queryBuilder' => $builder] + $options; + $this->_matching->contain($contains); return $this; } @@ -288,11 +284,11 @@ public function getMatching(): array * loaded for a table. The normalized array will restructure the original array * by sorting all associations under one key and special options under another. * - * Each of the levels of the associations tree will converted to a Cake\ORM\EagerLoadable + * Each of the levels of the associations tree will be converted to a {@link \Cake\ORM\EagerLoadable} * object, that contains all the information required for the association objects * to load the information from the database. * - * Additionally it will set an 'instance' key per association containing the + * Additionally, it will set an 'instance' key per association containing the * association instance from the corresponding source table * * @param \Cake\ORM\Table $repository The table containing the association that @@ -442,7 +438,7 @@ public function attachAssociations(Query $query, Table $repository, bool $includ * * @param \Cake\ORM\Table $repository The table containing the associations to be * attached - * @return \Cake\ORM\EagerLoadable[] + * @return array<\Cake\ORM\EagerLoadable> */ public function attachableAssociations(Table $repository): array { @@ -456,11 +452,11 @@ public function attachableAssociations(Table $repository): array /** * Returns an array with the associations that need to be fetched using a - * separate query, each array value will contain a Cake\ORM\EagerLoadable object. + * separate query, each array value will contain a {@link \Cake\ORM\EagerLoadable} object. * * @param \Cake\ORM\Table $repository The table containing the associations * to be loaded - * @return \Cake\ORM\EagerLoadable[] + * @return array<\Cake\ORM\EagerLoadable> */ public function externalAssociations(Table $repository): array { @@ -479,8 +475,8 @@ public function externalAssociations(Table $repository): array * * @param \Cake\ORM\Table $parent owning side of the association * @param string $alias name of the association to be loaded - * @param array $options list of extra options to use for this association - * @param array $paths An array with two values, the first one is a list of dot + * @param array $options list of extra options to use for this association + * @param array $paths An array with two values, the first one is a list of dot * separated strings representing associations that lead to this `$alias` in the * chain of associations to be loaded. The second value is the path to follow in * entities' properties to fetch a record of the corresponding association. @@ -586,9 +582,9 @@ protected function _correctStrategy(EagerLoadable $loadable): void * Helper function used to compile a list of all associations that can be * joined in the query. * - * @param \Cake\ORM\EagerLoadable[] $associations list of associations from which to obtain joins. - * @param \Cake\ORM\EagerLoadable[] $matching list of associations that should be forcibly joined. - * @return \Cake\ORM\EagerLoadable[] + * @param array<\Cake\ORM\EagerLoadable> $associations list of associations from which to obtain joins. + * @param array<\Cake\ORM\EagerLoadable> $matching list of associations that should be forcibly joined. + * @return array<\Cake\ORM\EagerLoadable> */ protected function _resolveJoins(array $associations, array $matching = []): array { @@ -634,7 +630,7 @@ public function loadExternal(Query $query, StatementInterface $statement): State return $statement; } - $driver = $query->getConnection()->getDriver(); + $driver = $query->getConnection()->getDriver($query->getConnectionRole()); [$collected, $statement] = $this->_collectKeys($external, $query, $statement); // No records found, skip trying to attach associations. @@ -690,10 +686,10 @@ public function loadExternal(Query $query, StatementInterface $statement): State * * - alias: The association alias * - instance: The association instance - * - canBeJoined: Whether or not the association will be loaded using a JOIN + * - canBeJoined: Whether the association will be loaded using a JOIN * - entityClass: The entity that should be used for hydrating the results * - nestKey: A dotted path that can be used to correctly insert the data into the results. - * - matching: Whether or not it is an association loaded through `matching()`. + * - matching: Whether it is an association loaded through `matching()`. * * @param \Cake\ORM\Table $table The table containing the association that * will be normalized @@ -710,9 +706,8 @@ public function associationsMap(Table $table): array /** @psalm-suppress PossiblyNullReference */ $map = $this->_buildAssociationsMap($map, $this->_matching->normalized($table), true); $map = $this->_buildAssociationsMap($map, $this->normalized($table)); - $map = $this->_buildAssociationsMap($map, $this->_joinsMap); - return $map; + return $this->_buildAssociationsMap($map, $this->_joinsMap); } /** @@ -720,8 +715,8 @@ public function associationsMap(Table $table): array * associationsMap() method. * * @param array $map An initial array for the map. - * @param \Cake\ORM\EagerLoadable[] $level An array of EagerLoadable instances. - * @param bool $matching Whether or not it is an association loaded through `matching()`. + * @param array<\Cake\ORM\EagerLoadable> $level An array of EagerLoadable instances. + * @param bool $matching Whether it is an association loaded through `matching()`. * @return array */ protected function _buildAssociationsMap(array $map, array $level, bool $matching = false): array @@ -756,9 +751,9 @@ protected function _buildAssociationsMap(array $map, array $level, bool $matchin * @param string $alias The table alias as it appears in the query. * @param \Cake\ORM\Association $assoc The association object the alias represents; * will be normalized - * @param bool $asMatching Whether or not this join results should be treated as a + * @param bool $asMatching Whether this join results should be treated as a * 'matching' association. - * @param string $targetProperty The property name where the results of the join should be nested at. + * @param string|null $targetProperty The property name where the results of the join should be nested at. * If not passed, the default property for the association will be used. * @return void */ @@ -781,7 +776,7 @@ public function addToJoinsMap( * Helper function used to return the keys from the query records that will be used * to eagerly load associations. * - * @param \Cake\ORM\EagerLoadable[] $external the list of external associations to be loaded + * @param array<\Cake\ORM\EagerLoadable> $external the list of external associations to be loaded * @param \Cake\ORM\Query $query The query from which the results where generated * @param \Cake\Database\StatementInterface $statement The statement to work on * @return array @@ -823,7 +818,7 @@ protected function _collectKeys(array $external, Query $query, $statement): arra * defined in $collectKeys * * @param \Cake\Database\Statement\BufferedStatement $statement The statement to read from. - * @param array $collectKeys The keys to collect + * @param array $collectKeys The keys to collect * @return array */ protected function _groupKeys(BufferedStatement $statement, array $collectKeys): array diff --git a/Entity.php b/Entity.php index a7167875..14e5d57e 100644 --- a/Entity.php +++ b/Entity.php @@ -44,8 +44,8 @@ class Entity implements EntityInterface, InvalidPropertyInterface * $entity = new Entity(['id' => 1, 'name' => 'Andrew']) * ``` * - * @param array $properties hash of properties to set in this entity - * @param array $options list of options to use when creating this entity + * @param array $properties hash of properties to set in this entity + * @param array $options list of options to use when creating this entity */ public function __construct(array $properties = [], array $options = []) { diff --git a/Exception/PersistenceFailedException.php b/Exception/PersistenceFailedException.php index 5d9d0302..291d8ced 100644 --- a/Exception/PersistenceFailedException.php +++ b/Exception/PersistenceFailedException.php @@ -40,9 +40,9 @@ class PersistenceFailedException extends CakeException * Constructor. * * @param \Cake\Datasource\EntityInterface $entity The entity on which the persistence operation failed - * @param string|array $message Either the string of the error message, or an array of attributes + * @param array|string $message Either the string of the error message, or an array of attributes * that are made available in the view, and sprintf()'d into Exception::$_messageTemplate - * @param int $code The code of the error, is also the HTTP status code for the error. + * @param int|null $code The code of the error, is also the HTTP status code for the error. * @param \Throwable|null $previous the previous exception. */ public function __construct(EntityInterface $entity, $message, ?int $code = null, ?Throwable $previous = null) diff --git a/LazyEagerLoader.php b/LazyEagerLoader.php index cf2ddb55..0e7a0103 100644 --- a/LazyEagerLoader.php +++ b/LazyEagerLoader.php @@ -36,11 +36,11 @@ class LazyEagerLoader * * The properties for the associations to be loaded will be overwritten on each entity. * - * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $entities a single entity or list of entities + * @param \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface> $entities a single entity or list of entities * @param array $contain A `contain()` compatible array. * @see \Cake\ORM\Query::contain() * @param \Cake\ORM\Table $source The table to use for fetching the top level entities - * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] + * @return \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface> */ public function loadInto($entities, array $contain, Table $source) { @@ -116,8 +116,8 @@ protected function _getQuery(CollectionInterface $objects, array $contain, Table * in the top level entities. * * @param \Cake\ORM\Table $source The table having the top level associations - * @param string[] $associations The name of the top level associations - * @return string[] + * @param array $associations The name of the top level associations + * @return array */ protected function _getPropertyMap(Table $source, array $associations): array { @@ -135,11 +135,11 @@ protected function _getPropertyMap(Table $source, array $associations): array * Injects the results of the eager loader query into the original list of * entities. * - * @param \Cake\Datasource\EntityInterface[]|\Traversable $objects The original list of entities - * @param \Cake\Collection\CollectionInterface|\Cake\ORM\Query $results The loaded results - * @param string[] $associations The top level associations that were loaded + * @param iterable<\Cake\Datasource\EntityInterface> $objects The original list of entities + * @param \Cake\ORM\Query $results The loaded results + * @param array $associations The top level associations that were loaded * @param \Cake\ORM\Table $source The table where the entities came from - * @return array + * @return array<\Cake\Datasource\EntityInterface> */ protected function _injectResults(iterable $objects, $results, array $associations, Table $source): array { @@ -147,6 +147,7 @@ protected function _injectResults(iterable $objects, $results, array $associatio $properties = $this->_getPropertyMap($source, $associations); $primaryKey = (array)$source->getPrimaryKey(); $results = $results + ->all() ->indexBy(function ($e) use ($primaryKey) { /** @var \Cake\Datasource\EntityInterface $e */ return implode(';', $e->extract($primaryKey)); diff --git a/Locator/LocatorAwareTrait.php b/Locator/LocatorAwareTrait.php index 3c17d437..1aa4a9e3 100644 --- a/Locator/LocatorAwareTrait.php +++ b/Locator/LocatorAwareTrait.php @@ -17,12 +17,21 @@ namespace Cake\ORM\Locator; use Cake\Datasource\FactoryLocator; +use Cake\ORM\Table; +use UnexpectedValueException; /** * Contains method for setting and accessing LocatorInterface instance */ trait LocatorAwareTrait { + /** + * This object's default table alias. + * + * @var string|null + */ + protected $defaultTable = null; + /** * Table locator instance * @@ -58,4 +67,28 @@ public function getTableLocator(): LocatorInterface /** @var \Cake\ORM\Locator\LocatorInterface */ return $this->_tableLocator; } + + /** + * Convenience method to get a table instance. + * + * @param string|null $alias The alias name you want to get. Should be in CamelCase format. + * If `null` then the value of $defaultTable property is used. + * @param array $options The options you want to build the table with. + * If a table has already been loaded the registry options will be ignored. + * @return \Cake\ORM\Table + * @throws \Cake\Core\Exception\CakeException If `$alias` argument and `$defaultTable` property both are `null`. + * @see \Cake\ORM\TableLocator::get() + * @since 4.3.0 + */ + public function fetchTable(?string $alias = null, array $options = []): Table + { + $alias = $alias ?? $this->defaultTable; + if (empty($alias)) { + throw new UnexpectedValueException( + 'You must provide an `$alias` or set the `$defaultTable` property to a non empty string.' + ); + } + + return $this->getTableLocator()->get($alias, $options); + } } diff --git a/Locator/LocatorInterface.php b/Locator/LocatorInterface.php index 68db76d8..24516e8e 100644 --- a/Locator/LocatorInterface.php +++ b/Locator/LocatorInterface.php @@ -16,13 +16,14 @@ */ namespace Cake\ORM\Locator; +use Cake\Datasource\Locator\LocatorInterface as BaseLocatorInterface; use Cake\Datasource\RepositoryInterface; use Cake\ORM\Table; /** * Registries for Table objects should implement this interface. */ -interface LocatorInterface extends \Cake\Datasource\Locator\LocatorInterface +interface LocatorInterface extends BaseLocatorInterface { /** * Returns configuration for an alias or the full configuration array for @@ -37,9 +38,9 @@ public function getConfig(?string $alias = null): array; * Stores a list of options to be used when instantiating an object * with a matching alias. * - * @param string|array $alias Name of the alias or array to completely + * @param array|string $alias Name of the alias or array to completely * overwrite current config. - * @param array|null $options list of options for the alias + * @param array|null $options list of options for the alias * @return $this * @throws \RuntimeException When you attempt to configure an existing * table instance. @@ -50,7 +51,7 @@ public function setConfig($alias, $options = null); * Get a table instance from the registry. * * @param string $alias The alias name you want to get. - * @param array $options The options you want to build the table with. + * @param array $options The options you want to build the table with. * @return \Cake\ORM\Table */ public function get(string $alias, array $options = []): Table; diff --git a/Locator/TableLocator.php b/Locator/TableLocator.php index a89d1401..3fefe3f4 100644 --- a/Locator/TableLocator.php +++ b/Locator/TableLocator.php @@ -25,6 +25,7 @@ use Cake\ORM\Table; use Cake\Utility\Inflector; use RuntimeException; +use function Cake\Core\pluginSplit; /** * Provides a default registry/factory for Table objects. @@ -34,14 +35,14 @@ class TableLocator extends AbstractLocator implements LocatorInterface /** * Contains a list of locations where table classes should be looked for. * - * @var array + * @var array */ protected $locations = []; /** * Configuration for aliases. * - * @var array + * @var array */ protected $_config = []; @@ -56,7 +57,7 @@ class TableLocator extends AbstractLocator implements LocatorInterface * Contains a list of Table objects that were created out of the * built-in Table class. The list is indexed by table alias * - * @var \Cake\ORM\Table[] + * @var array<\Cake\ORM\Table> */ protected $_fallbacked = []; @@ -78,7 +79,7 @@ class TableLocator extends AbstractLocator implements LocatorInterface /** * Constructor. * - * @param array|null $locations Locations where tables should be looked for. + * @param array|null $locations Locations where tables should be looked for. * If none provided, the default `Model\Table` under your app's namespace is used. */ public function __construct(?array $locations = null) @@ -171,7 +172,7 @@ public function getConfig(?string $alias = null): array * This is important because table associations are resolved at runtime * and cyclic references need to be handled correctly. * - * The options that can be passed are the same as in Cake\ORM\Table::__construct(), but the + * The options that can be passed are the same as in {@link \Cake\ORM\Table::__construct()}, but the * `className` key is also recognized. * * ### Options @@ -194,7 +195,7 @@ public function getConfig(?string $alias = null): array * the same alias, the registry will only store the first instance. * * @param string $alias The alias name you want to get. Should be in CamelCase format. - * @param array $options The options you want to build the table with. + * @param array $options The options you want to build the table with. * If a table has already been loaded the options will be ignored. * @return \Cake\ORM\Table * @throws \RuntimeException When you try to configure an alias that already exists. @@ -215,8 +216,6 @@ protected function createInstance(string $alias, array $options) $options = ['alias' => $classAlias] + $options; } elseif (!isset($options['alias'])) { $options['className'] = $alias; - /** @psalm-suppress PossiblyFalseOperand */ - $alias = substr($alias, strrpos($alias, '\\') + 1, -5); } if (isset($this->_config[$alias])) { @@ -274,7 +273,7 @@ protected function createInstance(string $alias, array $options) * Gets the table class name. * * @param string $alias The alias name you want to get. Should be in CamelCase format. - * @param array $options Table options array. + * @param array $options Table options array. * @return string|null */ protected function _getClassName(string $alias, array $options = []): ?string @@ -300,7 +299,7 @@ protected function _getClassName(string $alias, array $options = []): ?string /** * Wrapper for creating table instances * - * @param array $options The alias to check for. + * @param array $options The alias to check for. * @return \Cake\ORM\Table */ protected function _create(array $options): Table @@ -339,7 +338,7 @@ public function clear(): void * debugging common mistakes when setting up associations or created new table * classes. * - * @return \Cake\ORM\Table[] + * @return array<\Cake\ORM\Table> */ public function genericInstances(): array { diff --git a/Marshaller.php b/Marshaller.php index f79d5569..2caa7e57 100644 --- a/Marshaller.php +++ b/Marshaller.php @@ -23,8 +23,11 @@ use Cake\Datasource\EntityInterface; use Cake\Datasource\InvalidPropertyInterface; use Cake\ORM\Association\BelongsToMany; +use Cake\Utility\Hash; use InvalidArgumentException; use RuntimeException; +use function Cake\Core\deprecationWarning; +use function Cake\Core\getTypeName; /** * Contains logic to convert array data into entities. @@ -61,7 +64,7 @@ public function __construct(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. + * @param array $options List of options containing the 'associated' key. * @throws \InvalidArgumentException When associations do not exist. * @return array */ @@ -82,9 +85,7 @@ protected function _buildPropertyMap(array $data, array $options): array } // Map associations - if (!isset($options['associated'])) { - $options['associated'] = []; - } + $options['associated'] = $options['associated'] ?? []; $include = $this->_normalizeAssociations($options['associated']); foreach ($include as $key => $nested) { if (is_int($key) && is_scalar($nested)) { @@ -170,7 +171,7 @@ protected function _buildPropertyMap(array $data, array $options): array * ``` * * @param array $data The data to hydrate. - * @param array $options List of options + * @param array $options List of options * @return \Cake\Datasource\EntityInterface * @see \Cake\ORM\Table::newEntity() * @see \Cake\ORM\Entity::$_accessible @@ -241,7 +242,7 @@ public function one(array $data, array $options = []): EntityInterface * 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 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. @@ -258,6 +259,11 @@ protected function _validate(array $data, array $options, bool $isNew): array } elseif (is_string($options['validate'])) { $validator = $this->_table->getValidator($options['validate']); } elseif (is_object($options['validate'])) { + deprecationWarning( + 'Passing validator instance for the `validate` option is deprecated,' + . ' use `ValidatorAwareTrait::setValidator() instead.`' + ); + /** @var \Cake\Validation\Validator $validator */ $validator = $options['validate']; } @@ -275,7 +281,7 @@ protected function _validate(array $data, array $options, bool $isNew): array * 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. + * @param array $options The options passed to this marshaller. * @return array An array containing prepared data and options. */ protected function _prepareDataAndOptions(array $data, array $options): array @@ -283,7 +289,7 @@ protected function _prepareDataAndOptions(array $data, array $options): array $options += ['validate' => true]; $tableName = $this->_table->getAlias(); - if (isset($data[$tableName])) { + if (isset($data[$tableName]) && is_array($data[$tableName])) { $data += $data[$tableName]; unset($data[$tableName]); } @@ -300,8 +306,8 @@ protected function _prepareDataAndOptions(array $data, array $options): array * * @param \Cake\ORM\Association $assoc The association to marshall * @param mixed $value The data to hydrate. If not an array, this method will return null. - * @param array $options List of options. - * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface>|null */ protected function _marshalAssociation(Association $assoc, $value, array $options) { @@ -350,8 +356,8 @@ protected function _marshalAssociation(Association $assoc, $value, array $option * 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. + * @param array $options List of options + * @return array<\Cake\Datasource\EntityInterface> An array of hydrated records. * @see \Cake\ORM\Table::newEntities() * @see \Cake\ORM\Entity::$_accessible */ @@ -376,8 +382,8 @@ public function many(array $data, array $options = []): array * * @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. + * @param array $options List of options. + * @return array<\Cake\Datasource\EntityInterface> An array of built entities. * @throws \BadMethodCallException * @throws \InvalidArgumentException * @throws \RuntimeException @@ -471,7 +477,7 @@ protected function _belongsToMany(BelongsToMany $assoc, array $data, array $opti * * @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. + * @return array<\Cake\Datasource\EntityInterface> An array of entities. */ protected function _loadAssociatedByIds(Association $assoc, array $ids): array { @@ -516,7 +522,7 @@ protected function _loadAssociatedByIds(Association $assoc, array $ids): array * ### Options: * * - associated: Associations listed here will be marshalled as well. - * - validate: Whether or not to validate data before hydrating the entities. Can + * - validate: Whether to validate data before hydrating the entities. Can * also be set to a string to use a specific validator. Defaults to true/default. * - fields: An allowed list of fields to be assigned to the entity. If not present * the accessible fields list in the entity will be used. @@ -535,7 +541,7 @@ protected function _loadAssociatedByIds(Association $assoc, array $ids): array * @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. + * @param array $options List of options. * @return \Cake\Datasource\EntityInterface * @see \Cake\ORM\Entity::$_accessible */ @@ -642,7 +648,7 @@ public function merge(EntityInterface $entity, array $data, array $options = []) * * ### Options: * - * - validate: Whether or not to validate data before hydrating the entities. Can + * - validate: Whether 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. * - fields: An allowed list of fields to be assigned to the entity. If not present, @@ -652,10 +658,9 @@ public function merge(EntityInterface $entity, array $data, array $options = []) * @param iterable<\Cake\Datasource\EntityInterface> $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[] + * @param array $options List of options. + * @return array<\Cake\Datasource\EntityInterface> * @see \Cake\ORM\Entity::$_accessible - * @psalm-suppress NullArrayOffset */ public function mergeMany(iterable $entities, array $data, array $options = []): array { @@ -675,10 +680,8 @@ public function mergeMany(iterable $entities, array $data, array $options = []): }) ->toArray(); - /** @psalm-suppress InvalidArrayOffset */ - $new = $indexed[null] ?? []; - /** @psalm-suppress InvalidArrayOffset */ - unset($indexed[null]); + $new = $indexed[''] ?? []; + unset($indexed['']); $output = []; foreach ($entities as $entity) { @@ -700,7 +703,7 @@ public function mergeMany(iterable $entities, array $data, array $options = []): return explode(';', (string)$key); }) ->filter(function ($keys) use ($primary) { - return count(array_filter($keys, 'strlen')) === count($primary); + return count(Hash::filter($keys)) === count($primary); }) ->reduce(function ($conditions, $keys) use ($primary) { $fields = array_map([$this->_table, 'aliasField'], $primary); @@ -733,11 +736,11 @@ public function mergeMany(iterable $entities, array $data, array $options = []): /** * Creates a new sub-marshaller and merges the associated data. * - * @param \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[] $original The original entity + * @param \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface> $original The original entity * @param \Cake\ORM\Association $assoc The association to merge * @param mixed $value The array of data to hydrate. If not an array, this method will return null. - * @param array $options List of options. - * @return \Cake\Datasource\EntityInterface|\Cake\Datasource\EntityInterface[]|null + * @param array $options List of options. + * @return \Cake\Datasource\EntityInterface|array<\Cake\Datasource\EntityInterface>|null */ protected function _mergeAssociation($original, Association $assoc, $value, array $options) { @@ -753,7 +756,6 @@ protected function _mergeAssociation($original, Association $assoc, $value, arra $types = [Association::ONE_TO_ONE, Association::MANY_TO_ONE]; $type = $assoc->type(); if (in_array($type, $types, true)) { - /** @psalm-suppress PossiblyInvalidArgument, ArgumentTypeCoercion */ return $marshaller->merge($original, $value, $options); } if ($type === Association::MANY_TO_MANY) { @@ -780,11 +782,11 @@ protected function _mergeAssociation($original, Association $assoc, $value, arra * Creates a new sub-marshaller and merges the associated data for a BelongstoMany * association. * - * @param \Cake\Datasource\EntityInterface[] $original The original entities list. + * @param array<\Cake\Datasource\EntityInterface> $original The original entities list. * @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[] + * @param array $options List of options. + * @return array<\Cake\Datasource\EntityInterface> */ protected function _mergeBelongsToMany(array $original, BelongsToMany $assoc, array $value, array $options): array { @@ -810,11 +812,11 @@ protected function _mergeBelongsToMany(array $original, BelongsToMany $assoc, ar /** * Merge the special _joinData property into the entity set. * - * @param \Cake\Datasource\EntityInterface[] $original The original entities list. + * @param array<\Cake\Datasource\EntityInterface> $original The original entities list. * @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 + * @param array $options List of options. + * @return array<\Cake\Datasource\EntityInterface> An array of entities */ protected function _mergeJoinData(array $original, BelongsToMany $assoc, array $value, array $options): array { @@ -873,7 +875,7 @@ protected function _mergeJoinData(array $original, BelongsToMany $assoc, array $ * * @param \Cake\Datasource\EntityInterface $entity The entity that was marshaled. * @param array $data readOnly $data to use. - * @param array $options List of options that are readOnly. + * @param array $options List of options that are readOnly. * @return void */ protected function dispatchAfterMarshal(EntityInterface $entity, array $data, array $options = []): void diff --git a/PropertyMarshalInterface.php b/PropertyMarshalInterface.php index f52ce749..8317d9eb 100644 --- a/PropertyMarshalInterface.php +++ b/PropertyMarshalInterface.php @@ -29,7 +29,7 @@ interface PropertyMarshalInterface * * @param \Cake\ORM\Marshaller $marshaller The marhshaller of the table the behavior is attached to. * @param array $map The property map being built. - * @param array $options The options array used in the marshalling call. + * @param array $options The options array used in the marshalling call. * @return array A map of `[property => callable]` of additional properties to marshal. */ public function buildMarshalMap(Marshaller $marshaller, array $map, array $options): array; diff --git a/Query.php b/Query.php index d7dba8ec..3f40d5b6 100644 --- a/Query.php +++ b/Query.php @@ -31,6 +31,7 @@ use JsonSerializable; use RuntimeException; use Traversable; +use function Cake\Core\deprecationWarning; /** * Extends the base Query class to provide new methods related to association @@ -43,7 +44,7 @@ * @method \Cake\ORM\Table getRepository() Returns the default table object that will be used by this query, * that is, the table that will appear in the from clause. * @method \Cake\Collection\CollectionInterface each(callable $c) Passes each of the query results to the callable - * @method \Cake\Collection\CollectionInterface sortBy($callback, int $dir) Sorts the query with the callback + * @method \Cake\Collection\CollectionInterface sortBy(callable|string $path, int $order = \SORT_DESC, int $sort = \SORT_NUMERIC) Sorts the query with the callback * @method \Cake\Collection\CollectionInterface filter(callable $c = null) Keeps the results using passing the callable test * @method \Cake\Collection\CollectionInterface reject(callable $c) Removes the results passing the callable test * @method bool every(callable $c) Returns true if all the results pass the callable test @@ -51,28 +52,28 @@ * @method \Cake\Collection\CollectionInterface map(callable $c) Modifies each of the results using the callable * @method mixed reduce(callable $c, $zero = null) Folds all the results into a single value using the callable. * @method \Cake\Collection\CollectionInterface extract($field) Extracts a single column from each row - * @method mixed max($field) Returns the maximum value for a single column in all the results. - * @method mixed min($field) Returns the minimum value for a single column in all the results. - * @method \Cake\Collection\CollectionInterface groupBy(string|callable $field) In-memory group all results by the value of a column. - * @method \Cake\Collection\CollectionInterface indexBy(string|callable $callback) Returns the results indexed by the value of a column. - * @method \Cake\Collection\CollectionInterface countBy(string|callable $field) Returns the number of unique values for a column - * @method float sumOf(string|callable $field) Returns the sum of all values for a single column + * @method mixed max($field, $sort = \SORT_NUMERIC) Returns the maximum value for a single column in all the results. + * @method mixed min($field, $sort = \SORT_NUMERIC) Returns the minimum value for a single column in all the results. + * @method \Cake\Collection\CollectionInterface groupBy(callable|string $field) In-memory group all results by the value of a column. + * @method \Cake\Collection\CollectionInterface indexBy(callable|string $callback) Returns the results indexed by the value of a column. + * @method \Cake\Collection\CollectionInterface countBy(callable|string $field) Returns the number of unique values for a column + * @method int|float sumOf($field = null) Returns the sum of all values for a single column * @method \Cake\Collection\CollectionInterface shuffle() In-memory randomize the order the results are returned * @method \Cake\Collection\CollectionInterface sample(int $size = 10) In-memory shuffle the results and return a subset of them. * @method \Cake\Collection\CollectionInterface take(int $size = 1, int $from = 0) In-memory limit and offset for the query results. * @method \Cake\Collection\CollectionInterface skip(int $howMany) Skips some rows from the start of the query result. * @method mixed last() Return the last row of the query result - * @method \Cake\Collection\CollectionInterface append(array|\Traversable $items) Appends more rows to the result of the query. + * @method \Cake\Collection\CollectionInterface append(mixed $items) Appends more rows to the result of the query. * @method \Cake\Collection\CollectionInterface combine($k, $v, $g = null) Returns the values of the column $v index by column $k, * and grouped by $g. * @method \Cake\Collection\CollectionInterface nest($k, $p, $n = 'children') Creates a tree structure by nesting the values of column $p into that * with the same value for $k using $n as the nesting key. * @method array toArray() Returns a key-value array with the results of this query. * @method array toList() Returns a numerically indexed array with the results of this query. - * @method \Cake\Collection\CollectionInterface stopWhen(callable $c) Returns each row until the callable returns true. - * @method \Cake\Collection\CollectionInterface zip(array|\Traversable $c) Returns the first result of both the query and $c in an array, + * @method \Cake\Collection\CollectionInterface stopWhen(callable|array $c) Returns each row until the callable returns true. + * @method \Cake\Collection\CollectionInterface zip(iterable $c) Returns the first result of both the query and $c in an array, * then the second results and so on. - * @method \Cake\Collection\CollectionInterface zipWith($collections, callable $callable) Returns each of the results out of calling $c + * @method \Cake\Collection\CollectionInterface zipWith(iterable $collections, callable $callable) Returns each of the results out of calling $c * with the first rows of the query and each of the items, then the second rows and so on. * @method \Cake\Collection\CollectionInterface chunk(int $size) Groups the results in arrays of $size rows each. * @method bool isEmpty() Returns true if this query found no results. @@ -116,7 +117,7 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface protected $_hasFields; /** - * Tracks whether or not the original query should include + * Tracks whether the original query should include * fields from the top level table. * * @var bool|null @@ -178,7 +179,7 @@ class Query extends DatabaseQuery implements JsonSerializable, QueryInterface public function __construct(Connection $connection, Table $table) { parent::__construct($connection); - $this->repository($table); + $this->setRepository($table); if ($this->_repository !== null) { $this->addDefaultTypes($this->_repository); @@ -220,7 +221,7 @@ public function __construct(Connection $connection, Table $table) * all the fields in the schema of the table or the association will be added to * the select clause. * - * @param array|\Cake\Database\ExpressionInterface|callable|string|\Cake\ORM\Table|\Cake\ORM\Association $fields Fields + * @param \Cake\Database\ExpressionInterface|\Cake\ORM\Table|\Cake\ORM\Association|callable|array|string $fields Fields * to be added to the list. * @param bool $overwrite whether to reset fields with passed list or not * @return $this @@ -242,6 +243,24 @@ public function select($fields = [], bool $overwrite = false) return parent::select($fields, $overwrite); } + /** + * Behaves the exact same as `select()` except adds the field to the list of fields selected and + * does not disable auto-selecting fields for Associations. + * + * Use this instead of calling `select()` then `enableAutoFields()` to re-enable auto-fields. + * + * @param \Cake\Database\ExpressionInterface|\Cake\ORM\Table|\Cake\ORM\Association|callable|array|string $fields Fields + * to be added to the list. + * @return $this + */ + public function selectAlso($fields) + { + $this->select($fields); + $this->_autoFields = true; + + return $this; + } + /** * All the fields associated with the passed table except the excluded * fields will be added to the select clause of the query. Passed excluded fields should not be aliased. @@ -250,7 +269,7 @@ public function select($fields = [], bool $overwrite = false) * pass overwrite boolean true which will reset the select clause removing all previous additions. * * @param \Cake\ORM\Table|\Cake\ORM\Association $table The table to use to get an array of columns - * @param string[] $excludedFields The un-aliased column names you do not want selected from $table + * @param array $excludedFields The un-aliased column names you do not want selected from $table * @param bool $overwrite Whether to reset/remove previous selected fields * @return $this * @throws \InvalidArgumentException If Association|Table is not passed in first argument @@ -492,8 +511,8 @@ public function clearContain() * * @param \Cake\ORM\Table $table The table instance to pluck associations from. * @param \Cake\Database\TypeMap $typeMap The typemap to check for columns in. - * This typemap is indirectly mutated via Cake\ORM\Query::addDefaultTypes() - * @param array $associations The nested tree of associations to walk. + * This typemap is indirectly mutated via {@link \Cake\ORM\Query::addDefaultTypes()} + * @param array $associations The nested tree of associations to walk. * @return void */ protected function _addAssociationsToTypeMap(Table $table, TypeMap $typeMap, array $associations): void @@ -823,7 +842,7 @@ public function notMatching(string $assoc, ?callable $builder = null) * $options = $query->getOptions(); * ``` * - * @param array $options The options to be applied + * @param array $options The options to be applied * @return $this * @see getOptions() */ @@ -977,8 +996,8 @@ protected function _performCount(): int ->disableAutoFields() ->execute(); } else { - $statement = $this->getConnection()->newQuery() - ->select($count) + $statement = $this->getConnection() + ->selectQuery($count) ->from(['count_source' => $query]) ->execute(); } @@ -1065,7 +1084,7 @@ public function isHydrationEnabled(): bool * * @param \Closure|string|false $key Either the cache key or a function to generate the cache key. * When using a function, this query instance will be supplied as an argument. - * @param string|\Cake\Cache\CacheEngine $config Either the name of the cache config to use, or + * @param \Cake\Cache\CacheEngine|string $config Either the name of the cache config to use, or * a cache config instance. * @return $this * @throws \RuntimeException When you attempt to cache a non-select query. @@ -1215,6 +1234,10 @@ protected function _addDefaultSelectTypes(): void $types = []; foreach ($select as $alias => $value) { + if ($value instanceof TypedResultInterface) { + $types[$alias] = $value->getReturnType(); + continue; + } if (isset($typeMap[$alias])) { $types[$alias] = $typeMap[$alias]; continue; @@ -1222,9 +1245,6 @@ protected function _addDefaultSelectTypes(): void if (is_string($value) && isset($typeMap[$value])) { $types[$alias] = $typeMap[$value]; } - if ($value instanceof TypedResultInterface) { - $types[$alias] = $value->getReturnType(); - } } $this->getSelectTypeMap()->addDefaults($types); } @@ -1233,7 +1253,7 @@ protected function _addDefaultSelectTypes(): void * {@inheritDoc} * * @param string $finder The finder method to use. - * @param array $options The options for the finder. + * @param array $options The options for the finder. * @return static Returns a modified query. * @psalm-suppress MoreSpecificReturnType */ @@ -1264,7 +1284,7 @@ protected function _dirty(): void * This changes the query type to be 'update'. * Can be combined with set() and where() methods to create update queries. * - * @param string|\Cake\Database\ExpressionInterface|null $table Unused parameter. + * @param \Cake\Database\ExpressionInterface|string|null $table Unused parameter. * @return $this */ public function update($table = null) @@ -1305,7 +1325,7 @@ public function delete(?string $table = null) * Can be combined with the where() method to create delete queries. * * @param array $columns The columns to insert into. - * @param array $types A map between columns & their datatypes. + * @param array $types A map between columns & their datatypes. * @return $this */ public function insert(array $columns, array $types = []) @@ -1382,7 +1402,7 @@ public function jsonSerialize(): ResultSetInterface } /** - * Sets whether or not the ORM should automatically append fields. + * Sets whether the ORM should automatically append fields. * * By default calling select() will disable auto-fields. You can re-enable * auto-fields with this method. @@ -1410,7 +1430,7 @@ public function disableAutoFields() } /** - * Gets whether or not the ORM should automatically append fields. + * Gets whether the ORM should automatically append fields. * * By default calling select() will disable auto-fields. You can re-enable * auto-fields with enableAutoFields(). @@ -1439,4 +1459,19 @@ protected function _decorateResults(Traversable $result): ResultSetInterface return $result; } + + /** + * Helper for ORM\Query exceptions + * + * @param string $method The method that is invalid. + * @param string $message An additional message. + * @return void + * @internal + */ + protected function _deprecatedMethod($method, $message = '') + { + $class = static::class; + $text = "As of 4.5.0 calling {$method}() on {$class} is deprecated. " . $message; + deprecationWarning($text); + } } diff --git a/Query/DeleteQuery.php b/Query/DeleteQuery.php new file mode 100644 index 00000000..40470210 --- /dev/null +++ b/Query/DeleteQuery.php @@ -0,0 +1,337 @@ +_type === 'delete' && empty($this->_parts['from'])) { + $repository = $this->getRepository(); + $this->from([$repository->getAlias() => $repository->getTable()]); + } + + return parent::sql($binder); + } + + /** + * @inheritDoc + */ + public function delete(?string $table = null) + { + $this->_deprecatedMethod('delete()', 'Remove this method call.'); + + return parent::delete($table); + } + + /** + * @inheritDoc + */ + public function cache($key, $config = 'default') + { + $this->_deprecatedMethod('cache()', 'Use execute() instead.'); + + return parent::cache($key, $config); + } + + /** + * @inheritDoc + */ + public function all(): ResultSetInterface + { + $this->_deprecatedMethod('all()', 'Use execute() instead.'); + + return parent::all(); + } + + /** + * @inheritDoc + */ + public function select($fields = [], bool $overwrite = false) + { + $this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.'); + + return parent::select($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function distinct($on = [], $overwrite = false) + { + $this->_deprecatedMethod('distinct()'); + + return parent::distinct($on, $overwrite); + } + + /** + * @inheritDoc + */ + public function modifier($modifiers, $overwrite = false) + { + $this->_deprecatedMethod('modifier()'); + + return parent::modifier($modifiers, $overwrite); + } + + /** + * @inheritDoc + */ + public function join($tables, $types = [], $overwrite = false) + { + $this->_deprecatedMethod('join()'); + + return parent::join($tables, $types, $overwrite); + } + + /** + * @inheritDoc + */ + public function removeJoin(string $name) + { + $this->_deprecatedMethod('removeJoin()'); + + return parent::removeJoin($name); + } + + /** + * @inheritDoc + */ + public function leftJoin($table, $conditions = [], $types = []) + { + $this->_deprecatedMethod('leftJoin()'); + + return parent::leftJoin($table, $conditions, $types); + } + + /** + * @inheritDoc + */ + public function rightJoin($table, $conditions = [], $types = []) + { + $this->_deprecatedMethod('rightJoin()'); + + return parent::rightJoin($table, $conditions, $types); + } + + /** + * @inheritDoc + */ + public function leftJoinWith(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('leftJoinWith()'); + + return parent::leftJoinWith($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function innerJoin($table, $conditions = [], $types = []) + { + $this->_deprecatedMethod('innerJoin()'); + + return parent::innerJoin($table, $conditions, $types); + } + + /** + * @inheritDoc + */ + public function innerJoinWith(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('innerJoinWith()'); + + return parent::innerJoinWith($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function group($fields, $overwrite = false) + { + $this->_deprecatedMethod('group()'); + + return parent::group($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function having($conditions = null, $types = [], $overwrite = false) + { + $this->_deprecatedMethod('having()'); + + return parent::having($conditions, $types, $overwrite); + } + + /** + * @inheritDoc + */ + public function andHaving($conditions, $types = []) + { + $this->_deprecatedMethod('andHaving()'); + + return parent::andHaving($conditions, $types); + } + + /** + * @inheritDoc + */ + public function page(int $num, ?int $limit = null) + { + $this->_deprecatedMethod('page()'); + + return parent::page($num, $limit); + } + + /** + * @inheritDoc + */ + public function union($query, $overwrite = false) + { + $this->_deprecatedMethod('union()'); + + return parent::union($query, $overwrite); + } + + /** + * @inheritDoc + */ + public function unionAll($query, $overwrite = false) + { + $this->_deprecatedMethod('union()'); + + return parent::unionAll($query, $overwrite); + } + + /** + * @inheritDoc + */ + public function insert(array $columns, array $types = []) + { + $this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.'); + + return parent::insert($columns, $types); + } + + /** + * @inheritDoc + */ + public function into(string $table) + { + $this->_deprecatedMethod('into()', 'Use from() instead.'); + + return parent::into($table); + } + + /** + * @inheritDoc + */ + public function values($data) + { + $this->_deprecatedMethod('values()'); + + return parent::values($data); + } + + /** + * @inheritDoc + */ + public function update($table = null) + { + $this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.'); + + return parent::update($table); + } + + /** + * @inheritDoc + */ + public function set($key, $value = null, $types = []) + { + $this->_deprecatedMethod('set()'); + + return parent::set($key, $value, $types); + } + + /** + * @inheritDoc + */ + public function matching(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('matching()'); + + return parent::matching($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function notMatching(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('notMatching()'); + + return parent::notMatching($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function contain($associations, $override = false) + { + $this->_deprecatedMethod('contain()'); + + return parent::contain($associations, $override); + } + + /** + * @inheritDoc + */ + public function getContain(): array + { + $this->_deprecatedMethod('getContain()'); + + return parent::getContain(); + } + + /** + * @inheritDoc + */ + public function find(string $finder, array $options = []) + { + $this->_deprecatedMethod('find()'); + + return parent::find($finder, $options); + } +} diff --git a/Query/InsertQuery.php b/Query/InsertQuery.php new file mode 100644 index 00000000..6c3462f9 --- /dev/null +++ b/Query/InsertQuery.php @@ -0,0 +1,413 @@ +_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.'); + + return parent::delete($table); + } + + /** + * @inheritDoc + */ + public function cache($key, $config = 'default') + { + $this->_deprecatedMethod('cache()', 'Use execute() instead.'); + + return parent::cache($key, $config); + } + + /** + * @inheritDoc + */ + public function all(): ResultSetInterface + { + $this->_deprecatedMethod('all()', 'Use execute() instead.'); + + return parent::all(); + } + + /** + * @inheritDoc + */ + public function select($fields = [], bool $overwrite = false) + { + $this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.'); + + return parent::select($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function distinct($on = [], $overwrite = false) + { + $this->_deprecatedMethod('distinct()'); + + return parent::distinct($on, $overwrite); + } + + /** + * @inheritDoc + */ + public function modifier($modifiers, $overwrite = false) + { + $this->_deprecatedMethod('modifier()'); + + return parent::modifier($modifiers, $overwrite); + } + + /** + * @inheritDoc + */ + public function join($tables, $types = [], $overwrite = false) + { + $this->_deprecatedMethod('join()'); + + return parent::join($tables, $types, $overwrite); + } + + /** + * @inheritDoc + */ + public function removeJoin(string $name) + { + $this->_deprecatedMethod('removeJoin()'); + + return parent::removeJoin($name); + } + + /** + * @inheritDoc + */ + public function leftJoin($table, $conditions = [], $types = []) + { + $this->_deprecatedMethod('leftJoin()'); + + return parent::leftJoin($table, $conditions, $types); + } + + /** + * @inheritDoc + */ + public function rightJoin($table, $conditions = [], $types = []) + { + $this->_deprecatedMethod('rightJoin()'); + + return parent::rightJoin($table, $conditions, $types); + } + + /** + * @inheritDoc + */ + public function leftJoinWith(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('leftJoinWith()'); + + return parent::leftJoinWith($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function innerJoin($table, $conditions = [], $types = []) + { + $this->_deprecatedMethod('innerJoin()'); + + return parent::innerJoin($table, $conditions, $types); + } + + /** + * @inheritDoc + */ + public function innerJoinWith(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('innerJoinWith()'); + + return parent::innerJoinWith($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function group($fields, $overwrite = false) + { + $this->_deprecatedMethod('group()'); + + return parent::group($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function having($conditions = null, $types = [], $overwrite = false) + { + $this->_deprecatedMethod('having()'); + + return parent::having($conditions, $types, $overwrite); + } + + /** + * @inheritDoc + */ + public function andHaving($conditions, $types = []) + { + $this->_deprecatedMethod('andHaving()'); + + return parent::andHaving($conditions, $types); + } + + /** + * @inheritDoc + */ + public function page(int $num, ?int $limit = null) + { + $this->_deprecatedMethod('page()'); + + return parent::page($num, $limit); + } + + /** + * @inheritDoc + */ + public function offset($offset) + { + $this->_deprecatedMethod('offset()'); + + return parent::offset($offset); + } + + /** + * @inheritDoc + */ + public function union($query, $overwrite = false) + { + $this->_deprecatedMethod('union()'); + + return parent::union($query, $overwrite); + } + + /** + * @inheritDoc + */ + public function unionAll($query, $overwrite = false) + { + $this->_deprecatedMethod('union()'); + + return parent::unionAll($query, $overwrite); + } + + /** + * @inheritDoc + */ + public function from($tables = [], $overwrite = false) + { + $this->_deprecatedMethod('from()', 'Use into() instead.'); + + return parent::from($tables, $overwrite); + } + + /** + * @inheritDoc + */ + public function update($table = null) + { + $this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.'); + + return parent::update($table); + } + + /** + * @inheritDoc + */ + public function set($key, $value = null, $types = []) + { + $this->_deprecatedMethod('set()'); + + return parent::set($key, $value, $types); + } + + /** + * @inheritDoc + */ + public function matching(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('matching()'); + + return parent::matching($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function notMatching(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('notMatching()'); + + return parent::notMatching($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function contain($associations, $override = false) + { + $this->_deprecatedMethod('contain()'); + + return parent::contain($associations, $override); + } + + /** + * @inheritDoc + */ + public function getContain(): array + { + $this->_deprecatedMethod('getContain()'); + + return parent::getContain(); + } + + /** + * @inheritDoc + */ + public function find(string $finder, array $options = []) + { + $this->_deprecatedMethod('find()'); + + return parent::find($finder, $options); + } + + /** + * @inheritDoc + */ + public function where($conditions = null, array $types = [], bool $overwrite = false) + { + $this->_deprecatedMethod('where()'); + + return parent::where($conditions, $types, $overwrite); + } + + /** + * @inheritDoc + */ + public function whereNotNull($fields) + { + $this->_deprecatedMethod('whereNotNull()'); + + return parent::whereNotNull($fields); + } + + /** + * @inheritDoc + */ + public function whereNull($fields) + { + $this->_deprecatedMethod('whereNull()'); + + return parent::whereNull($fields); + } + + /** + * @inheritDoc + */ + public function whereInList(string $field, array $values, array $options = []) + { + $this->_deprecatedMethod('whereInList()'); + + return parent::whereInList($field, $values, $options); + } + + /** + * @inheritDoc + */ + public function whereNotInList(string $field, array $values, array $options = []) + { + $this->_deprecatedMethod('whereNotInList()'); + + return parent::whereNotInList($field, $values, $options); + } + + /** + * @inheritDoc + */ + public function whereNotInListOrNull(string $field, array $values, array $options = []) + { + $this->_deprecatedMethod('whereNotInListOrNull()'); + + return parent::whereNotInListOrNull($field, $values, $options); + } + + /** + * @inheritDoc + */ + public function andWhere($conditions, array $types = []) + { + $this->_deprecatedMethod('andWhere()'); + + return parent::andWhere($conditions, $types); + } + + /** + * @inheritDoc + */ + public function order($fields, $overwrite = false) + { + $this->_deprecatedMethod('order()'); + + return parent::order($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function orderAsc($field, $overwrite = false) + { + $this->_deprecatedMethod('orderAsc()'); + + return parent::orderAsc($field, $overwrite); + } + + /** + * @inheritDoc + */ + public function orderDesc($field, $overwrite = false) + { + $this->_deprecatedMethod('orderDesc()'); + + return parent::orderDesc($field, $overwrite); + } +} diff --git a/Query/SelectQuery.php b/Query/SelectQuery.php new file mode 100644 index 00000000..06942fab --- /dev/null +++ b/Query/SelectQuery.php @@ -0,0 +1,92 @@ +_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.'); + + return parent::delete($table); + } + + /** + * @inheritDoc + */ + public function insert(array $columns, array $types = []) + { + $this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.'); + + return parent::insert($columns, $types); + } + + /** + * @inheritDoc + */ + public function into(string $table) + { + $this->_deprecatedMethod('into()', 'Use from() instead.'); + + return parent::into($table); + } + + /** + * @inheritDoc + */ + public function values($data) + { + $this->_deprecatedMethod('values()'); + + return parent::values($data); + } + + /** + * @inheritDoc + */ + public function update($table = null) + { + $this->_deprecatedMethod('update()', 'Create your query with updateQuery() instead.'); + + return parent::update($table); + } + + /** + * @inheritDoc + */ + public function set($key, $value = null, $types = []) + { + $this->_deprecatedMethod('set()'); + + return parent::set($key, $value, $types); + } +} diff --git a/Query/UpdateQuery.php b/Query/UpdateQuery.php new file mode 100644 index 00000000..ff25a89d --- /dev/null +++ b/Query/UpdateQuery.php @@ -0,0 +1,267 @@ +_type === 'update' && empty($this->_parts['update'])) { + $repository = $this->getRepository(); + $this->update($repository->getTable()); + } + + return parent::sql($binder); + } + + /** + * @inheritDoc + */ + public function delete(?string $table = null) + { + $this->_deprecatedMethod('delete()', 'Create your query with deleteQuery() instead.'); + + return parent::delete($table); + } + + /** + * @inheritDoc + */ + public function cache($key, $config = 'default') + { + $this->_deprecatedMethod('cache()', 'Use execute() instead.'); + + return parent::cache($key, $config); + } + + /** + * @inheritDoc + */ + public function all(): ResultSetInterface + { + $this->_deprecatedMethod('all()', 'Use execute() instead.'); + + return parent::all(); + } + + /** + * @inheritDoc + */ + public function select($fields = [], bool $overwrite = false) + { + $this->_deprecatedMethod('select()', 'Create your query with selectQuery() instead.'); + + return parent::select($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function distinct($on = [], $overwrite = false) + { + $this->_deprecatedMethod('distinct()'); + + return parent::distinct($on, $overwrite); + } + + /** + * @inheritDoc + */ + public function modifier($modifiers, $overwrite = false) + { + $this->_deprecatedMethod('modifier()'); + + return parent::modifier($modifiers, $overwrite); + } + + /** + * @inheritDoc + */ + public function group($fields, $overwrite = false) + { + $this->_deprecatedMethod('group()'); + + return parent::group($fields, $overwrite); + } + + /** + * @inheritDoc + */ + public function having($conditions = null, $types = [], $overwrite = false) + { + $this->_deprecatedMethod('having()'); + + return parent::having($conditions, $types, $overwrite); + } + + /** + * @inheritDoc + */ + public function andHaving($conditions, $types = []) + { + $this->_deprecatedMethod('andHaving()'); + + return parent::andHaving($conditions, $types); + } + + /** + * @inheritDoc + */ + public function page(int $num, ?int $limit = null) + { + $this->_deprecatedMethod('page()'); + + return parent::page($num, $limit); + } + + /** + * @inheritDoc + */ + public function offset($offset) + { + $this->_deprecatedMethod('offset()'); + + return parent::offset($offset); + } + + /** + * @inheritDoc + */ + public function union($query, $overwrite = false) + { + $this->_deprecatedMethod('union()'); + + return parent::union($query, $overwrite); + } + + /** + * @inheritDoc + */ + public function unionAll($query, $overwrite = false) + { + $this->_deprecatedMethod('union()'); + + return parent::unionAll($query, $overwrite); + } + + /** + * @inheritDoc + */ + public function insert(array $columns, array $types = []) + { + $this->_deprecatedMethod('insert()', 'Create your query with insertQuery() instead.'); + + return parent::insert($columns, $types); + } + + /** + * @inheritDoc + */ + public function into(string $table) + { + $this->_deprecatedMethod('into()', 'Use update() instead.'); + + return parent::into($table); + } + + /** + * @inheritDoc + */ + public function values($data) + { + $this->_deprecatedMethod('values()'); + + return parent::values($data); + } + + /** + * @inheritDoc + */ + public function from($tables = [], $overwrite = false) + { + $this->_deprecatedMethod('from()', 'Use update() instead.'); + + return parent::from($tables, $overwrite); + } + + /** + * @inheritDoc + */ + public function matching(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('matching()'); + + return parent::matching($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function notMatching(string $assoc, ?callable $builder = null) + { + $this->_deprecatedMethod('notMatching()'); + + return parent::notMatching($assoc, $builder); + } + + /** + * @inheritDoc + */ + public function contain($associations, $override = false) + { + $this->_deprecatedMethod('contain()'); + + return parent::contain($associations, $override); + } + + /** + * @inheritDoc + */ + public function getContain(): array + { + $this->_deprecatedMethod('getContain()'); + + return parent::getContain(); + } + + /** + * @inheritDoc + */ + public function find(string $finder, array $options = []) + { + $this->_deprecatedMethod('find()'); + + return parent::find($finder, $options); + } +} diff --git a/ResultSet.php b/ResultSet.php index 147322b1..012c4002 100644 --- a/ResultSet.php +++ b/ResultSet.php @@ -29,6 +29,9 @@ * This object is responsible for correctly nesting result keys reported from * the query, casting each field to the correct type and executing the extra * queries required for eager loading external associations. + * + * @template T of \Cake\Datasource\EntityInterface|array + * @implements \Cake\Datasource\ResultSetInterface */ class ResultSet implements ResultSetInterface { @@ -51,7 +54,8 @@ class ResultSet implements ResultSetInterface /** * Last record fetched from the statement * - * @var array|object + * @var \Cake\Datasource\EntityInterface|array + * @psalm-var T */ protected $_current; @@ -73,7 +77,7 @@ class ResultSet implements ResultSetInterface * List of associations that should be placed under the `_matchingData` * result key. * - * @var array + * @var array */ protected $_matchingMap = []; @@ -88,7 +92,7 @@ class ResultSet implements ResultSetInterface * Map of fields that are fetched from the statement with * their type and the table they belong to * - * @var array + * @var array */ protected $_map = []; @@ -96,14 +100,14 @@ class ResultSet implements ResultSetInterface * List of matching associations and the column keys to expect * from each of them. * - * @var array + * @var array */ protected $_matchingMapColumns = []; /** * Results that have been fetched or hydrated into the results. * - * @var array|\SplFixedArray + * @var \SplFixedArray|array */ protected $_results = []; @@ -129,7 +133,7 @@ class ResultSet implements ResultSetInterface protected $_entityClass; /** - * Whether or not to buffer results fetched from the statement + * Whether to buffer results fetched from the statement * * @var bool */ @@ -161,7 +165,7 @@ public function __construct(Query $query, StatementInterface $statement) { $repository = $query->getRepository(); $this->_statement = $statement; - $this->_driver = $query->getConnection()->getDriver(); + $this->_driver = $query->getConnection()->getDriver($query->getConnectionRole()); $this->_defaultTable = $repository; $this->_calculateAssociationMap($query); $this->_hydrate = $query->isHydrationEnabled(); @@ -182,8 +186,10 @@ public function __construct(Query $query, StatementInterface $statement) * * Part of Iterator interface. * - * @return array|object + * @return \Cake\Datasource\EntityInterface|array + * @psalm-return T */ + #[\ReturnTypeWillChange] public function current() { return $this->_current; @@ -275,7 +281,8 @@ public function valid(): bool * * This method will also close the underlying statement cursor. * - * @return array|object|null + * @return \Cake\Datasource\EntityInterface|array|null + * @psalm-return T|null */ public function first() { @@ -298,6 +305,16 @@ public function first() * @return string Serialized object */ public function serialize(): string + { + return serialize($this->__serialize()); + } + + /** + * Serializes a resultset. + * + * @return array + */ + public function __serialize(): array { if (!$this->_useBuffering) { $msg = 'You cannot serialize an un-buffered ResultSet. ' @@ -310,10 +327,10 @@ public function serialize(): string } if ($this->_results instanceof SplFixedArray) { - return serialize($this->_results->toArray()); + return $this->_results->toArray(); } - return serialize($this->_results); + return $this->_results; } /** @@ -326,8 +343,18 @@ public function serialize(): string */ public function unserialize($serialized) { - $results = (array)(unserialize($serialized) ?: []); - $this->_results = SplFixedArray::fromArray($results); + $this->__unserialize((array)(unserialize($serialized) ?: [])); + } + + /** + * Unserializes a resultset. + * + * @param array $data Data array. + * @return void + */ + public function __unserialize(array $data): void + { + $this->_results = SplFixedArray::fromArray($data); $this->_useBuffering = true; $this->_count = $this->_results->count(); } @@ -435,7 +462,7 @@ protected function _fetchResult() * Correctly nests results keys including those coming from associations * * @param array $row Array containing columns and values or false if there is no results - * @return array|\Cake\Datasource\EntityInterface Results + * @return \Cake\Datasource\EntityInterface|array Results */ protected function _groupResult(array $row) { @@ -472,9 +499,7 @@ protected function _groupResult(array $row) // If the default table is not in the results, set // it to an empty array so that any contained // associations hydrate correctly. - if (!isset($results[$defaultAlias])) { - $results[$defaultAlias] = []; - } + $results[$defaultAlias] = $results[$defaultAlias] ?? []; unset($presentAliases[$defaultAlias]); @@ -549,12 +574,17 @@ protected function _groupResult(array $row) * Returns an array that can be used to describe the internal state of this * object. * - * @return array + * @return array */ public function __debugInfo() { + $currentIndex = $this->_index; + // toArray() adjusts the current index, so we have to reset it + $items = $this->toArray(); + $this->_index = $currentIndex; + return [ - 'items' => $this->toArray(), + 'items' => $items, ]; } } diff --git a/Rule/ExistsIn.php b/Rule/ExistsIn.php index 6494b2a5..912c7199 100644 --- a/Rule/ExistsIn.php +++ b/Rule/ExistsIn.php @@ -30,7 +30,7 @@ class ExistsIn /** * The list of fields to check * - * @var array + * @var array */ protected $_fields; @@ -44,7 +44,7 @@ class ExistsIn /** * Options for the constructor * - * @var array + * @var array */ protected $_options = []; @@ -54,10 +54,10 @@ class ExistsIn * Available option for $options is 'allowNullableNulls' flag. * Set to true to accept composite foreign keys where one or more nullable columns are null. * - * @param string|array $fields The field or fields to check existence as primary key. + * @param array|string $fields The field or fields to check existence as primary key. * @param \Cake\ORM\Table|\Cake\ORM\Association|string $repository The repository where the * field will be looked for, or the association name for the repository. - * @param array $options The options that modify the rules behavior. + * @param array $options The options that modify the rule's behavior. * Options 'allowNullableNulls' will make the rule pass if given foreign keys are set to `null`. * Notice: allowNullableNulls cannot pass by database columns set to `NOT NULL`. */ @@ -74,7 +74,7 @@ public function __construct($fields, $repository, array $options = []) * Performs the existence check * * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields - * @param array $options Options passed to the check, + * @param array $options Options passed to the check, * where the `repository` key is required. * @throws \RuntimeException When the rule refers to an undefined association. * @return bool @@ -147,7 +147,7 @@ function ($key) use ($target) { } /** - * Checks whether or not the given entity fields are nullable and null. + * Checks whether the given entity fields are nullable and null. * * @param \Cake\Datasource\EntityInterface $entity The entity to check. * @param \Cake\ORM\Table $source The table to use schema from. diff --git a/Rule/IsUnique.php b/Rule/IsUnique.php index 24cfeae0..a86bfe3e 100644 --- a/Rule/IsUnique.php +++ b/Rule/IsUnique.php @@ -17,6 +17,7 @@ namespace Cake\ORM\Rule; use Cake\Datasource\EntityInterface; +use Cake\Utility\Hash; /** * Checks that a list of fields from an entity are unique in the table @@ -26,14 +27,14 @@ class IsUnique /** * The list of fields to check * - * @var string[] + * @var array */ protected $_fields; /** * The unique check options * - * @var array + * @var array */ protected $_options = [ 'allowMultipleNulls' => false, @@ -46,8 +47,8 @@ class IsUnique * * - `allowMultipleNulls` Allows any field to have multiple null values. Defaults to false. * - * @param string[] $fields The list of fields to check uniqueness for - * @param array $options The options for unique checks. + * @param array $fields The list of fields to check uniqueness for + * @param array $options The options for unique checks. */ public function __construct(array $fields, array $options = []) { @@ -60,7 +61,7 @@ public function __construct(array $fields, array $options = []) * * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields * where the `repository` key is required. - * @param array $options Options passed to the check, + * @param array $options Options passed to the check, * @return bool */ public function __invoke(EntityInterface $entity, array $options): bool @@ -79,7 +80,7 @@ public function __invoke(EntityInterface $entity, array $options): bool if ($entity->isNew() === false) { $keys = (array)$options['repository']->getPrimaryKey(); $keys = $this->_alias($alias, $entity->extract($keys)); - if (array_filter($keys, 'strlen')) { + if (Hash::filter($keys)) { $conditions['NOT'] = $keys; } } @@ -92,7 +93,7 @@ public function __invoke(EntityInterface $entity, array $options): bool * * @param string $alias The alias to add. * @param array $conditions The conditions to alias. - * @return array + * @return array */ protected function _alias(string $alias, array $conditions): array { diff --git a/Rule/LinkConstraint.php b/Rule/LinkConstraint.php index 6a25ba23..fe2b1cc7 100644 --- a/Rule/LinkConstraint.php +++ b/Rule/LinkConstraint.php @@ -19,6 +19,7 @@ use Cake\Datasource\EntityInterface; use Cake\ORM\Association; use Cake\ORM\Table; +use function Cake\Core\getTypeName; /** * Checks whether links to a given association exist / do not exist. @@ -88,7 +89,7 @@ public function __construct($association, string $requiredLinkStatus) * Performs the actual link check. * * @param \Cake\Datasource\EntityInterface $entity The entity involved in the operation. - * @param array $options Options passed from the rules checker. + * @param array $options Options passed from the rules checker. * @return bool Whether the check was successful. */ public function __invoke(EntityInterface $entity, array $options): bool @@ -126,9 +127,9 @@ public function __invoke(EntityInterface $entity, array $options): bool /** * Alias fields. * - * @param array $fields The fields that should be aliased. + * @param array $fields The fields that should be aliased. * @param \Cake\ORM\Table $source The object to use for aliasing. - * @return array The aliased fields + * @return array The aliased fields */ protected function _aliasFields(array $fields, Table $source): array { diff --git a/Rule/ValidCount.php b/Rule/ValidCount.php index f7e23521..89d18d03 100644 --- a/Rule/ValidCount.php +++ b/Rule/ValidCount.php @@ -46,7 +46,7 @@ public function __construct(string $field) * Performs the count check * * @param \Cake\Datasource\EntityInterface $entity The entity from where to extract the fields. - * @param array $options Options passed to the check. + * @param array $options Options passed to the check. * @return bool True if successful, else false. */ public function __invoke(EntityInterface $entity, array $options): bool diff --git a/RulesChecker.php b/RulesChecker.php index 56184322..dd6a8ee4 100644 --- a/RulesChecker.php +++ b/RulesChecker.php @@ -23,6 +23,8 @@ use Cake\ORM\Rule\LinkConstraint; use Cake\ORM\Rule\ValidCount; use Cake\Utility\Inflector; +use function Cake\Core\getTypeName; +use function Cake\I18n\__d; /** * ORM flavoured rules checker. @@ -47,8 +49,8 @@ class RulesChecker extends BaseRulesChecker * * - `allowMultipleNulls` Allows any field to have multiple null values. Defaults to false. * - * @param string[] $fields The list of fields to check for uniqueness. - * @param string|array|null $message The error message to show in case the rule does not pass. Can + * @param array $fields The list of fields to check for uniqueness. + * @param array|string|null $message The error message to show in case the rule does not pass. Can * also be an array of options. When an array, the 'message' key can be used to provide a message. * @return \Cake\Datasource\RuleInvoker */ @@ -89,10 +91,10 @@ public function isUnique(array $fields, $message = null): RuleInvoker * 'message' sets a custom error message. * Set 'allowNullableNulls' to true to accept composite foreign keys where one or more nullable columns are null. * - * @param string|string[] $field The field or list of fields to check for existence by + * @param array|string $field The field or list of fields to check for existence by * primary key lookup in the other table. * @param \Cake\ORM\Table|\Cake\ORM\Association|string $table The table name where the fields existence will be checked. - * @param string|array|null $message The error message to show in case the rule does not pass. Can + * @param array|string|null $message The error message to show in case the rule does not pass. Can * also be an array of options. When an array, the 'message' key can be used to provide a message. * @return \Cake\Datasource\RuleInvoker */ diff --git a/SaveOptionsBuilder.php b/SaveOptionsBuilder.php index 12270d58..ad81f905 100644 --- a/SaveOptionsBuilder.php +++ b/SaveOptionsBuilder.php @@ -26,6 +26,7 @@ * you to avoid mistakes by validating the options as you build them. * * @see \Cake\Datasource\RulesChecker + * @deprecated 4.4.0 Use a normal array for options instead. */ class SaveOptionsBuilder extends ArrayObject { @@ -34,7 +35,7 @@ class SaveOptionsBuilder extends ArrayObject /** * Options * - * @var array + * @var array */ protected $_options = []; @@ -49,7 +50,7 @@ class SaveOptionsBuilder extends ArrayObject * Constructor. * * @param \Cake\ORM\Table $table A table instance. - * @param array $options Options to parse when instantiating. + * @param array $options Options to parse when instantiating. */ public function __construct(Table $table, array $options = []) { @@ -65,7 +66,7 @@ public function __construct(Table $table, array $options = []) * This can be used to turn an options array into the object. * * @throws \InvalidArgumentException If a given option key does not exist. - * @param array $array Options array. + * @param array $array Options array. * @return $this */ public function parseArrayOptions(array $array) @@ -80,7 +81,7 @@ public function parseArrayOptions(array $array) /** * Set associated options. * - * @param string|array $associated String or array of associations. + * @param array|string $associated String or array of associations. * @return $this */ public function associated($associated) @@ -200,7 +201,7 @@ public function atomic(bool $atomic) } /** - * @return array + * @return array */ public function toArray(): array { diff --git a/Table.php b/Table.php index 2cb3e958..b101dcdd 100644 --- a/Table.php +++ b/Table.php @@ -20,6 +20,7 @@ use BadMethodCallException; use Cake\Core\App; use Cake\Core\Configure; +use Cake\Core\Exception\CakeException; use Cake\Database\Connection; use Cake\Database\Schema\TableSchemaInterface; use Cake\Database\TypeFactory; @@ -39,13 +40,21 @@ use Cake\ORM\Exception\MissingEntityException; use Cake\ORM\Exception\PersistenceFailedException; use Cake\ORM\Exception\RolledbackTransactionException; +use Cake\ORM\Query\DeleteQuery; +use Cake\ORM\Query\InsertQuery; +use Cake\ORM\Query\SelectQuery; +use Cake\ORM\Query\UpdateQuery; use Cake\ORM\Rule\IsUnique; use Cake\Utility\Inflector; use Cake\Validation\ValidatorAwareInterface; use Cake\Validation\ValidatorAwareTrait; use Exception; use InvalidArgumentException; +use ReflectionMethod; use RuntimeException; +use function Cake\Core\deprecationWarning; +use function Cake\Core\getTypeName; +use function Cake\Core\namespaceSplit; /** * Represents a single database table. @@ -89,7 +98,7 @@ * - `Model.beforeFind` Fired before each find operation. By stopping the event and * supplying a return value you can bypass the find operation entirely. Any * changes done to the $query instance will be retained for the rest of the find. The - * `$primary` parameter indicates whether or not this is the root query, or an + * `$primary` parameter indicates whether this is the root query, or an * associated query. * * - `Model.buildValidator` Allows listeners to modify validation rules @@ -214,14 +223,14 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc /** * The name of the field that represents the primary key in the table * - * @var string|string[]|null + * @var array|string|null */ protected $_primaryKey; /** - * The name of the field that represents a human readable representation of a row + * The name of the field that represents a human-readable representation of a row * - * @var string|string[]|null + * @var array|string|null */ protected $_displayField; @@ -273,7 +282,7 @@ class Table implements RepositoryInterface, EventListenerInterface, EventDispatc * validation set, or an associative array, where key is the name of the * validation set and value the Validator instance. * - * @param array $config List of options for this table + * @param array $config List of options for this table */ public function __construct(array $config = []) { @@ -353,7 +362,7 @@ public static function defaultConnectionName(): string * } * ``` * - * @param array $config Configuration options passed to the constructor + * @param array $config Configuration options passed to the constructor * @return void */ public function initialize(array $config): void @@ -387,9 +396,11 @@ public function getTable(): string { if ($this->_table === null) { $table = namespaceSplit(static::class); - $table = substr(end($table), 0, -5); + $table = substr(end($table), 0, -5) ?: $this->_alias; if (!$table) { - $table = $this->getAlias(); + throw new CakeException( + 'You must specify either the `alias` or the `table` option for the constructor.' + ); } $this->_table = Inflector::underscore($table); } @@ -419,7 +430,12 @@ public function getAlias(): string { if ($this->_alias === null) { $alias = namespaceSplit(static::class); - $alias = substr(end($alias), 0, -5) ?: $this->getTable(); + $alias = substr(end($alias), 0, -5) ?: $this->_table; + if (!$alias) { + throw new CakeException( + 'You must specify either the `alias` or the `table` option for the constructor.' + ); + } $this->_alias = $alias; } @@ -507,11 +523,17 @@ public function getConnection(): Connection public function getSchema(): TableSchemaInterface { if ($this->_schema === null) { - $this->_schema = $this->_initializeSchema( - $this->getConnection() - ->getSchemaCollection() - ->describe($this->getTable()) - ); + $this->_schema = $this->getConnection() + ->getSchemaCollection() + ->describe($this->getTable()); + + $method = new ReflectionMethod($this, '_initializeSchema'); + if ($method->getDeclaringClass()->getName() != Table::class) { + deprecationWarning( + 'Table::_initializeSchema() is deprecated. Override `getSchema()` with a parent call instead.' + ); + $this->_schema = $this->_initializeSchema($this->_schema); + } if (Configure::read('debug')) { $this->checkAliasLengths(); } @@ -526,7 +548,7 @@ public function getSchema(): TableSchemaInterface * If an array is passed, a new TableSchemaInterface will be constructed * out of it and used as the schema for this table. * - * @param array|\Cake\Database\Schema\TableSchemaInterface $schema Schema to be used for this table + * @param \Cake\Database\Schema\TableSchemaInterface|array $schema Schema to be used for this table * @return $this */ public function setSchema($schema) @@ -624,15 +646,13 @@ protected function _initializeSchema(TableSchemaInterface $schema): TableSchemaI */ public function hasField(string $field): bool { - $schema = $this->getSchema(); - - return $schema->getColumn($field) !== null; + return $this->getSchema()->getColumn($field) !== null; } /** * Sets the primary key field name. * - * @param string|string[] $key Sets a new name to be used as primary key + * @param array|string $key Sets a new name to be used as primary key * @return $this */ public function setPrimaryKey($key) @@ -645,7 +665,7 @@ public function setPrimaryKey($key) /** * Returns the primary key field name. * - * @return string|string[] + * @return array|string */ public function getPrimaryKey() { @@ -663,7 +683,7 @@ public function getPrimaryKey() /** * Sets the display field. * - * @param string|string[] $field Name to be used as display field. + * @param array|string $field Name to be used as display field. * @return $this */ public function setDisplayField($field) @@ -676,23 +696,34 @@ public function setDisplayField($field) /** * Returns the display field. * - * @return string|string[]|null + * @return array|string */ public function getDisplayField() { - if ($this->_displayField === null) { - $schema = $this->getSchema(); - $primary = (array)$this->getPrimaryKey(); - $this->_displayField = array_shift($primary); - if ($schema->getColumn('title')) { - $this->_displayField = 'title'; + if ($this->_displayField !== null) { + return $this->_displayField; + } + + $schema = $this->getSchema(); + foreach (['title', 'name', 'label'] as $field) { + if ($schema->hasColumn($field)) { + return $this->_displayField = $field; } - if ($schema->getColumn('name')) { - $this->_displayField = 'name'; + } + + foreach ($schema->columns() as $column) { + $columnSchema = $schema->getColumn($column); + if ( + $columnSchema && + $columnSchema['null'] !== true && + $columnSchema['type'] === 'string' && + !preg_match('/pass|token|secret/i', $column) + ) { + return $this->_displayField = $column; } } - return $this->_displayField; + return $this->_displayField = $this->getPrimaryKey(); } /** @@ -769,7 +800,7 @@ public function setEntityClass(string $name) * Behaviors are generally loaded during Table::initialize(). * * @param string $name The name of the behavior. Can be a short class reference. - * @param array $options The options for the behavior to use. + * @param array $options The options for the behavior to use. * @return $this * @throws \RuntimeException If a behavior is being reloaded. * @see \Cake\ORM\Behavior @@ -793,7 +824,7 @@ public function addBehavior(string $name, array $options = []) * ]); * ``` * - * @param array $behaviors All of the behaviors to load. + * @param array $behaviors All the behaviors to load. * @return $this * @throws \RuntimeException If a behavior is being reloaded. */ @@ -860,16 +891,14 @@ public function getBehavior(string $name): Behavior )); } - $behavior = $this->_behaviors->get($name); - - return $behavior; + return $this->_behaviors->get($name); } /** * Check if a behavior with the given alias has been loaded. * * @param string $name The behavior alias to check. - * @return bool Whether or not the behavior exists. + * @return bool Whether the behavior exists. */ public function hasBehavior(string $name): bool { @@ -1036,17 +1065,14 @@ public function addAssociations(array $params) * * @param string $associated the alias for the target table. This is used to * uniquely identify the association - * @param array $options list of options to configure the association definition + * @param array $options list of options to configure the association definition * @return \Cake\ORM\Association\BelongsTo */ public function belongsTo(string $associated, array $options = []): BelongsTo { $options += ['sourceTable' => $this]; - /** @var \Cake\ORM\Association\BelongsTo $association */ - $association = $this->_associations->load(BelongsTo::class, $associated, $options); - - return $association; + return $this->_associations->load(BelongsTo::class, $associated, $options); } /** @@ -1082,17 +1108,14 @@ public function belongsTo(string $associated, array $options = []): BelongsTo * * @param string $associated the alias for the target table. This is used to * uniquely identify the association - * @param array $options list of options to configure the association definition + * @param array $options list of options to configure the association definition * @return \Cake\ORM\Association\HasOne */ public function hasOne(string $associated, array $options = []): HasOne { $options += ['sourceTable' => $this]; - /** @var \Cake\ORM\Association\HasOne $association */ - $association = $this->_associations->load(HasOne::class, $associated, $options); - - return $association; + return $this->_associations->load(HasOne::class, $associated, $options); } /** @@ -1134,17 +1157,14 @@ public function hasOne(string $associated, array $options = []): HasOne * * @param string $associated the alias for the target table. This is used to * uniquely identify the association - * @param array $options list of options to configure the association definition + * @param array $options list of options to configure the association definition * @return \Cake\ORM\Association\HasMany */ public function hasMany(string $associated, array $options = []): HasMany { $options += ['sourceTable' => $this]; - /** @var \Cake\ORM\Association\HasMany $association */ - $association = $this->_associations->load(HasMany::class, $associated, $options); - - return $association; + return $this->_associations->load(HasMany::class, $associated, $options); } /** @@ -1188,17 +1208,14 @@ public function hasMany(string $associated, array $options = []): HasMany * * @param string $associated the alias for the target table. This is used to * uniquely identify the association - * @param array $options list of options to configure the association definition + * @param array $options list of options to configure the association definition * @return \Cake\ORM\Association\BelongsToMany */ public function belongsToMany(string $associated, array $options = []): BelongsToMany { $options += ['sourceTable' => $this]; - /** @var \Cake\ORM\Association\BelongsToMany $association */ - $association = $this->_associations->load(BelongsToMany::class, $associated, $options); - - return $association; + return $this->_associations->load(BelongsToMany::class, $associated, $options); } /** @@ -1256,15 +1273,12 @@ public function belongsToMany(string $associated, array $options = []): BelongsT * Would invoke the `findPublished` method. * * @param string $type the type of query to perform - * @param array $options An array that will be passed to Query::applyOptions() + * @param array $options An array that will be passed to Query::applyOptions() * @return \Cake\ORM\Query The query builder */ public function find(string $type = 'all', array $options = []): Query { - $query = $this->query(); - $query->select(); - - return $this->callFinder($type, $query, $options); + return $this->callFinder($type, $this->selectQuery()->select(), $options); } /** @@ -1274,7 +1288,7 @@ public function find(string $type = 'all', array $options = []): Query * can override this method in subclasses to modify how `find('all')` works. * * @param \Cake\ORM\Query $query The query to find with - * @param array $options The options to use for the find + * @param array $options The options to use for the find * @return \Cake\ORM\Query The query builder */ public function findAll(Query $query, array $options): Query @@ -1288,7 +1302,7 @@ public function findAll(Query $query, array $options): Query * * When calling this finder, the fields passed are used to determine what should * be used as the array key, value and optionally what to group the results by. - * By default the primary key for the model is used for the key, and the display + * By default, the primary key for the model is used for the key, and the display * field as value. * * The results of this finder will be in the following form: @@ -1313,14 +1327,14 @@ public function findAll(Query $query, array $options): Query * ``` * * The `valueField` can also be an array, in which case you can also specify - * the `valueSeparator` option to control how the values will be concatinated: + * the `valueSeparator` option to control how the values will be concatenated: * * ``` * $table->find('list', [ * 'valueField' => ['first_name', 'last_name'], * 'valueSeparator' => ' | ', * ]); - * + * ``` * * The results of this finder will be in the following form: * @@ -1355,7 +1369,7 @@ public function findAll(Query $query, array $options): Query * ``` * * @param \Cake\ORM\Query $query The query to find with - * @param array $options The options for the find + * @param array $options The options for the find * @return \Cake\ORM\Query The query builder */ public function findList(Query $query, array $options): Query @@ -1414,13 +1428,13 @@ public function findList(Query $query, array $options): Query * ``` * $table->find('threaded', [ * 'keyField' => 'id', - * 'parentField' => 'ancestor_id' + * 'parentField' => 'ancestor_id', * 'nestingKey' => 'children' * ]); * ``` * * @param \Cake\ORM\Query $query The query to find with - * @param array $options The options to find with + * @param array $options The options to find with * @return \Cake\ORM\Query The query builder */ public function findThreaded(Query $query, array $options): Query @@ -1447,10 +1461,10 @@ public function findThreaded(Query $query, array $options): Query * This is an auxiliary function used for result formatters that can accept * composite keys when comparing values. * - * @param array $options the original options passed to a finder - * @param string[] $keys the keys to check in $options to build matchers from + * @param array $options the original options passed to a finder + * @param array $keys the keys to check in $options to build matchers from * the associated value - * @return array + * @return array */ protected function _setFieldMatchers(array $options, array $keys): array { @@ -1491,7 +1505,7 @@ protected function _setFieldMatchers(array $options, array $keys): array * ``` * * @param mixed $primaryKey primary key value to find - * @param array $options options accepted by `Table::find()` + * @param array $options options accepted by `Table::find()` * @return \Cake\Datasource\EntityInterface * @throws \Cake\Datasource\Exception\RecordNotFoundException if the record with such id * could not be found @@ -1502,12 +1516,21 @@ protected function _setFieldMatchers(array $options, array $keys): array */ public function get($primaryKey, array $options = []): EntityInterface { + if ($primaryKey === null) { + throw new InvalidPrimaryKeyException(sprintf( + 'Record not found in table "%s" with primary key [NULL]', + $this->getTable() + )); + } + $key = (array)$this->getPrimaryKey(); $alias = $this->getAlias(); foreach ($key as $index => $keyname) { $key[$index] = $alias . '.' . $keyname; } - $primaryKey = (array)$primaryKey; + if (!is_array($primaryKey)) { + $primaryKey = [$primaryKey]; + } if (count($key) !== count($primaryKey)) { $primaryKey = $primaryKey ?: [null]; $primaryKey = array_map(function ($key) { @@ -1532,7 +1555,7 @@ public function get($primaryKey, array $options = []): EntityInterface if ($cacheConfig) { if (!$cacheKey) { $cacheKey = sprintf( - 'get:%s.%s%s', + 'get-%s-%s-%s', $this->getConnection()->configName(), $this->getTable(), json_encode($primaryKey) @@ -1599,13 +1622,13 @@ protected function _transactionCommitted(bool $atomic, bool $primary): bool * transaction (default: true) * - defaults: Whether to use the search criteria as default values for the new entity (default: true) * - * @param array|callable|\Cake\ORM\Query $search The criteria to find existing + * @param \Cake\ORM\Query|callable|array $search The criteria to find existing * records by. Note that when you pass a query object you'll have to use * the 2nd arg of the method to modify the entity data before saving. * @param callable|null $callback A callback that will be invoked for newly * created entities. This callback will be called *before* the entity * is persisted. - * @param array $options The options to use when saving. + * @param array $options The options to use when saving. * @return \Cake\Datasource\EntityInterface An entity. * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved */ @@ -1630,12 +1653,12 @@ public function findOrCreate($search, ?callable $callback = null, $options = []) /** * Performs the actual find and/or create of an entity based on the passed options. * - * @param array|callable|\Cake\ORM\Query $search The criteria to find an existing record by, or a callable tha will + * @param \Cake\ORM\Query|callable|array $search The criteria to find an existing record by, or a callable tha will * customize the find query. * @param callable|null $callback A callback that will be invoked for newly * created entities. This callback will be called *before* the entity * is persisted. - * @param array $options The options to use when saving. + * @param array $options The options to use when saving. * @return \Cake\Datasource\EntityInterface|array An entity. * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved * @throws \InvalidArgumentException @@ -1671,7 +1694,7 @@ protected function _processFindOrCreate($search, ?callable $callback = null, $op /** * Gets the query object for findOrCreate(). * - * @param array|callable|\Cake\ORM\Query $search The criteria to find existing records by. + * @param \Cake\ORM\Query|callable|array $search The criteria to find existing records by. * @return \Cake\ORM\Query */ protected function _getFindOrCreateQuery($search): Query @@ -1700,9 +1723,56 @@ protected function _getFindOrCreateQuery($search): Query */ public function query(): Query { + deprecationWarning( + 'As of 4.5.0 using query() is deprecated. Instead use `insertQuery()`, ' . + '`deleteQuery()`, `selectQuery()` or `updateQuery()`. The query objects ' . + 'returned by these methods will emit deprecations that will become fatal errors in 5.0.' . + 'See https://book.cakephp.org/4/en/appendices/4-5-migration-guide.html for more information.' + ); + return new Query($this->getConnection(), $this); } + /** + * Creates a new DeleteQuery instance for a table. + * + * @return \Cake\ORM\Query\DeleteQuery + */ + public function deleteQuery(): DeleteQuery + { + return new DeleteQuery($this->getConnection(), $this); + } + + /** + * Creates a new InsertQuery instance for a table. + * + * @return \Cake\ORM\Query\InsertQuery + */ + public function insertQuery(): InsertQuery + { + return new InsertQuery($this->getConnection(), $this); + } + + /** + * Creates a new SelectQuery instance for a table. + * + * @return \Cake\ORM\Query\SelectQuery + */ + public function selectQuery(): SelectQuery + { + return new SelectQuery($this->getConnection(), $this); + } + + /** + * Creates a new UpdateQuery instance for a table. + * + * @return \Cake\ORM\Query\UpdateQuery + */ + public function updateQuery(): UpdateQuery + { + return new UpdateQuery($this->getConnection(), $this); + } + /** * Creates a new Query::subquery() instance for a table. * @@ -1719,11 +1789,10 @@ public function subquery(): Query */ public function updateAll($fields, $conditions): int { - $query = $this->query(); - $query->update() + $statement = $this->updateQuery() ->set($fields) - ->where($conditions); - $statement = $query->execute(); + ->where($conditions) + ->execute(); $statement->closeCursor(); return $statement->rowCount(); @@ -1734,10 +1803,9 @@ public function updateAll($fields, $conditions): int */ public function deleteAll($conditions): int { - $query = $this->query() - ->delete() - ->where($conditions); - $statement = $query->execute(); + $statement = $this->deleteQuery() + ->where($conditions) + ->execute(); $statement->closeCursor(); return $statement->rowCount(); @@ -1767,7 +1835,7 @@ public function exists($conditions): bool * * - atomic: Whether to execute the save and callbacks inside a database * transaction (default: true) - * - checkRules: Whether or not to check the rules on entity before saving, if the checking + * - checkRules: Whether to check the rules on entity before saving, if the checking * fails, it will abort the save operation. (default:true) * - associated: If `true` it will save 1st level associated entities as they are found * in the passed `$entity` whenever the property defined for the association @@ -1775,7 +1843,7 @@ public function exists($conditions): bool * to be saved. It is possible to provide different options for saving on associated * table objects using this key by making the custom options the array value. * If `false` no associated records will be saved. (default: `true`) - * - checkExisting: Whether or not to check if the entity already exists, assuming that the + * - checkExisting: Whether to check if the entity already exists, assuming that the * entity is marked as not new, and the primary key has been set. * * ### Events @@ -1840,13 +1908,14 @@ public function exists($conditions): bool * ``` * * @param \Cake\Datasource\EntityInterface $entity the entity to be saved - * @param array|\ArrayAccess|\Cake\ORM\SaveOptionsBuilder $options The options to use when saving. + * @param \Cake\ORM\SaveOptionsBuilder|\ArrayAccess|array $options The options to use when saving. * @return \Cake\Datasource\EntityInterface|false * @throws \Cake\ORM\Exception\RolledbackTransactionException If the transaction is aborted in the afterSave event. */ public function save(EntityInterface $entity, $options = []) { if ($options instanceof SaveOptionsBuilder) { + deprecationWarning('SaveOptionsBuilder is deprecated. Use a normal array for options instead.'); $options = $options->toArray(); } @@ -1856,6 +1925,7 @@ public function save(EntityInterface $entity, $options = []) 'checkRules' => true, 'checkExisting' => true, '_primary' => true, + '_cleanOnSuccess' => true, ]); if ($entity->hasErrors((bool)$options['associated'])) { @@ -1875,8 +1945,10 @@ public function save(EntityInterface $entity, $options = []) $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options')); } if ($options['atomic'] || $options['_primary']) { - $entity->clean(); - $entity->setNew(false); + if ($options['_cleanOnSuccess']) { + $entity->clean(); + $entity->setNew(false); + } $entity->setSource($this->getRegistryAlias()); } } @@ -1889,7 +1961,7 @@ public function save(EntityInterface $entity, $options = []) * the entity contains errors or the save was aborted by a callback. * * @param \Cake\Datasource\EntityInterface $entity the entity to be saved - * @param array|\ArrayAccess $options The options to use when saving. + * @param \ArrayAccess|array $options The options to use when saving. * @return \Cake\Datasource\EntityInterface * @throws \Cake\ORM\Exception\PersistenceFailedException When the entity couldn't be saved * @see \Cake\ORM\Table::save() @@ -2067,15 +2139,16 @@ protected function _insert(EntityInterface $entity, array $data) } } - $success = false; if (empty($data)) { - return $success; + return false; } - $statement = $this->query()->insert(array_keys($data)) + $statement = $this->insertQuery() + ->insert(array_keys($data)) ->values($data) ->execute(); + $success = false; if ($statement->rowCount() !== 0) { $success = $entity; $entity->set($filteredKeys, ['guard' => false]); @@ -2106,7 +2179,7 @@ protected function _insert(EntityInterface $entity, array $data) * Note: The ORM will not generate primary key values for composite primary keys. * You can overwrite _newId() in your table class. * - * @param string[] $primary The primary key columns to get a new ID for. + * @param array $primary The primary key columns to get a new ID for. * @return string|null Either null or the primary key value or a list of primary key values. */ protected function _newId(array $primary) @@ -2152,16 +2225,12 @@ protected function _update(EntityInterface $entity, array $data) throw new InvalidArgumentException($message); } - $query = $this->query(); - $statement = $query->update() + $statement = $this->updateQuery() ->set($data) ->where($primaryKey) ->execute(); - $success = false; - if ($statement->errorCode() === '00000') { - $success = $entity; - } + $success = $statement->errorCode() === '00000' ? $entity : false; $statement->closeCursor(); return $success; @@ -2174,9 +2243,9 @@ protected function _update(EntityInterface $entity, array $data) * any one of the records fails to save due to failed validation or database * error. * - * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to save. - * @param array|\ArrayAccess|\Cake\ORM\SaveOptionsBuilder $options Options used when calling Table::save() for each entity. - * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface|false False on failure, entities list on success. + * @param iterable<\Cake\Datasource\EntityInterface> $entities Entities to save. + * @param \Cake\ORM\SaveOptionsBuilder|\ArrayAccess|array $options Options used when calling Table::save() for each entity. + * @return iterable<\Cake\Datasource\EntityInterface>|false False on failure, entities list on success. * @throws \Exception */ public function saveMany(iterable $entities, $options = []) @@ -2195,9 +2264,9 @@ public function saveMany(iterable $entities, $options = []) * any one of the records fails to save due to failed validation or database * error. * - * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to save. - * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity. - * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface Entities list. + * @param iterable<\Cake\Datasource\EntityInterface> $entities Entities to save. + * @param \ArrayAccess|array $options Options used when calling Table::save() for each entity. + * @return iterable<\Cake\Datasource\EntityInterface> Entities list. * @throws \Exception * @throws \Cake\ORM\Exception\PersistenceFailedException If an entity couldn't be saved. */ @@ -2207,14 +2276,19 @@ public function saveManyOrFail(iterable $entities, $options = []): iterable } /** - * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to save. - * @param array|\ArrayAccess|\Cake\ORM\SaveOptionsBuilder $options Options used when calling Table::save() for each entity. + * @param iterable<\Cake\Datasource\EntityInterface> $entities Entities to save. + * @param \Cake\ORM\SaveOptionsBuilder|\ArrayAccess|array $options Options used when calling Table::save() for each entity. * @throws \Cake\ORM\Exception\PersistenceFailedException If an entity couldn't be saved. * @throws \Exception If an entity couldn't be saved. - * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface Entities list. + * @return iterable<\Cake\Datasource\EntityInterface> Entities list. */ protected function _saveMany(iterable $entities, $options = []): iterable { + if ($options instanceof SaveOptionsBuilder) { + deprecationWarning('SaveOptionsBuilder is deprecated. Use a normal array for options instead.'); + $options = $options->toArray(); + } + $options = new ArrayObject( (array)$options + [ 'atomic' => true, @@ -2222,10 +2296,11 @@ protected function _saveMany(iterable $entities, $options = []): iterable '_primary' => true, ] ); + $options['_cleanOnSuccess'] = false; - /** @var bool[] $isNew */ + /** @var array $isNew */ $isNew = []; - $cleanup = function ($entities) use (&$isNew): void { + $cleanupOnFailure = function ($entities) use (&$isNew): void { /** @var array<\Cake\Datasource\EntityInterface> $entities */ foreach ($entities as $key => $entity) { if (isset($isNew[$key]) && $isNew[$key]) { @@ -2250,20 +2325,40 @@ protected function _saveMany(iterable $entities, $options = []): iterable } }); } catch (Exception $e) { - $cleanup($entities); + $cleanupOnFailure($entities); throw $e; } if ($failed !== null) { - $cleanup($entities); + $cleanupOnFailure($entities); throw new PersistenceFailedException($failed, ['saveMany']); } + $cleanupOnSuccess = function (EntityInterface $entity) use (&$cleanupOnSuccess) { + $entity->clean(); + $entity->setNew(false); + + foreach (array_keys($entity->toArray()) as $field) { + $value = $entity->get($field); + + if ($value instanceof EntityInterface) { + $cleanupOnSuccess($value); + } elseif (is_array($value) && current($value) instanceof EntityInterface) { + foreach ($value as $associated) { + $cleanupOnSuccess($associated); + } + } + } + }; + if ($this->_transactionCommitted($options['atomic'], $options['_primary'])) { foreach ($entities as $entity) { $this->dispatchEvent('Model.afterSaveCommit', compact('entity', 'options')); + if ($options['atomic'] || $options['_primary']) { + $cleanupOnSuccess($entity); + } } } @@ -2297,7 +2392,7 @@ protected function _saveMany(iterable $entities, $options = []): iterable * the options used in the delete operation. * * @param \Cake\Datasource\EntityInterface $entity The entity to remove. - * @param array|\ArrayAccess $options The options for the delete. + * @param \ArrayAccess|array $options The options for the delete. * @return bool success */ public function delete(EntityInterface $entity, $options = []): bool @@ -2329,9 +2424,9 @@ public function delete(EntityInterface $entity, $options = []): bool * any one of the records fails to delete due to failed validation or database * error. * - * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to delete. - * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity. - * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface|false Entities list + * @param iterable<\Cake\Datasource\EntityInterface> $entities Entities to delete. + * @param \ArrayAccess|array $options Options used when calling Table::save() for each entity. + * @return iterable<\Cake\Datasource\EntityInterface>|false Entities list * on success, false on failure. * @see \Cake\ORM\Table::delete() for options and events related to this method. */ @@ -2353,9 +2448,9 @@ public function deleteMany(iterable $entities, $options = []) * any one of the records fails to delete due to failed validation or database * error. * - * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to delete. - * @param array|\ArrayAccess $options Options used when calling Table::save() for each entity. - * @return array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface Entities list. + * @param iterable<\Cake\Datasource\EntityInterface> $entities Entities to delete. + * @param \ArrayAccess|array $options Options used when calling Table::save() for each entity. + * @return iterable<\Cake\Datasource\EntityInterface> Entities list. * @throws \Cake\ORM\Exception\PersistenceFailedException * @see \Cake\ORM\Table::delete() for options and events related to this method. */ @@ -2371,8 +2466,8 @@ public function deleteManyOrFail(iterable $entities, $options = []): iterable } /** - * @param array<\Cake\Datasource\EntityInterface>|\Cake\Datasource\ResultSetInterface $entities Entities to delete. - * @param array|\ArrayAccess $options Options used. + * @param iterable<\Cake\Datasource\EntityInterface> $entities Entities to delete. + * @param \ArrayAccess|array $options Options used. * @return \Cake\Datasource\EntityInterface|null */ protected function _deleteMany(iterable $entities, $options = []): ?EntityInterface @@ -2410,7 +2505,7 @@ protected function _deleteMany(iterable $entities, $options = []): ?EntityInterf * has no primary key value, application rules checks failed or the delete was aborted by a callback. * * @param \Cake\Datasource\EntityInterface $entity The entity to remove. - * @param array|\ArrayAccess $options The options for the delete. + * @param \ArrayAccess|array $options The options for the delete. * @return true * @throws \Cake\ORM\Exception\PersistenceFailedException * @see \Cake\ORM\Table::delete() @@ -2470,15 +2565,12 @@ protected function _processDelete(EntityInterface $entity, ArrayObject $options) return $success; } - $query = $this->query(); - $conditions = $entity->extract($primaryKey); - $statement = $query->delete() - ->where($conditions) + $statement = $this->deleteQuery() + ->where($entity->extract($primaryKey)) ->execute(); - $success = $statement->rowCount() > 0; - if (!$success) { - return $success; + if ($statement->rowCount() < 1) { + return false; } $this->dispatchEvent('Model.afterDelete', [ @@ -2486,7 +2578,7 @@ protected function _processDelete(EntityInterface $entity, ArrayObject $options) 'options' => $options, ]); - return $success; + return true; } /** @@ -2503,14 +2595,16 @@ public function hasFinder(string $type): bool } /** - * Calls a finder method directly and applies it to the passed query, - * if no query is passed a new one will be created and returned + * Calls a finder method and applies it to the passed query. * - * @param string $type name of the finder to be called - * @param \Cake\ORM\Query $query The query object to apply the finder options to - * @param array $options List of options to pass to the finder + * @param string $type Name of the finder to be called. + * @param \Cake\ORM\Query $query The query object to apply the finder options to. + * @param array $options List of options to pass to the finder. * @return \Cake\ORM\Query * @throws \BadMethodCallException + * @uses findAll() + * @uses findList() + * @uses findThreaded() */ public function callFinder(string $type, Query $query, array $options = []): Query { @@ -2737,18 +2831,15 @@ public function newEmptyEntity(): EntityInterface * before it is converted into entities. * * @param array $data The data to build an entity with. - * @param array $options A list of options for the object hydration. + * @param array $options A list of options for the object hydration. * @return \Cake\Datasource\EntityInterface * @see \Cake\ORM\Marshaller::one() */ public function newEntity(array $data, array $options = []): EntityInterface { - if (!isset($options['associated'])) { - $options['associated'] = $this->_associations->keys(); - } - $marshaller = $this->marshaller(); + $options['associated'] = $options['associated'] ?? $this->_associations->keys(); - return $marshaller->one($data, $options); + return $this->marshaller()->one($data, $options); } /** @@ -2780,17 +2871,14 @@ public function newEntity(array $data, array $options = []): EntityInterface * before it is converted into entities. * * @param array $data The data to build an entity with. - * @param array $options A list of options for the objects hydration. + * @param array $options A list of options for the objects hydration. * @return array<\Cake\Datasource\EntityInterface> An array of hydrated records. */ public function newEntities(array $data, array $options = []): array { - if (!isset($options['associated'])) { - $options['associated'] = $this->_associations->keys(); - } - $marshaller = $this->marshaller(); + $options['associated'] = $options['associated'] ?? $this->_associations->keys(); - return $marshaller->many($data, $options); + return $this->marshaller()->many($data, $options); } /** @@ -2840,18 +2928,15 @@ public function newEntities(array $data, array $options = []): array * @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 A list of options for the object hydration. + * @param array $options A list of options for the object hydration. * @return \Cake\Datasource\EntityInterface * @see \Cake\ORM\Marshaller::merge() */ public function patchEntity(EntityInterface $entity, array $data, array $options = []): EntityInterface { - if (!isset($options['associated'])) { - $options['associated'] = $this->_associations->keys(); - } - $marshaller = $this->marshaller(); + $options['associated'] = $options['associated'] ?? $this->_associations->keys(); - return $marshaller->merge($entity, $data, $options); + return $this->marshaller()->merge($entity, $data, $options); } /** @@ -2879,20 +2964,17 @@ public function patchEntity(EntityInterface $entity, array $data, array $options * You can use the `Model.beforeMarshal` event to modify request data * before it is converted into entities. * - * @param array<\Cake\Datasource\EntityInterface>|\Traversable $entities the entities that will get the + * @param iterable<\Cake\Datasource\EntityInterface> $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 A list of options for the objects hydration. + * @param array $options A list of options for the objects hydration. * @return array<\Cake\Datasource\EntityInterface> */ public function patchEntities(iterable $entities, array $data, array $options = []): array { - if (!isset($options['associated'])) { - $options['associated'] = $this->_associations->keys(); - } - $marshaller = $this->marshaller(); + $options['associated'] = $options['associated'] ?? $this->_associations->keys(); - return $marshaller->mergeMany($entities, $data, $options); + return $this->marshaller()->mergeMany($entities, $data, $options); } /** @@ -2924,7 +3006,7 @@ public function patchEntities(iterable $entities, array $data, array $options = * the data to be validated. * * @param mixed $value The value of column to be checked for uniqueness. - * @param array $options The options array, optionally containing the 'scope' key. + * @param array $options The options array, optionally containing the 'scope' key. * May also be the validation context, if there are no options. * @param array|null $context Either the validation context or null. * @return bool True if the value is unique, or false if a non-scalar, non-unique value was given. @@ -2983,7 +3065,7 @@ public function validateUnique($value, array $options, ?array $context = null): * - Model.beforeRules => beforeRules * - Model.afterRules => afterRules * - * @return array + * @return array */ public function implementedEvents(): array { @@ -3027,8 +3109,9 @@ public function buildRules(RulesChecker $rules): RulesChecker /** * Gets a SaveOptionsBuilder instance. * - * @param array $options Options to parse by the builder. + * @param array $options Options to parse by the builder. * @return \Cake\ORM\SaveOptionsBuilder + * @deprecated 4.4.0 Use a normal array for options instead. */ public function getSaveOptionsBuilder(array $options = []): SaveOptionsBuilder { @@ -3082,7 +3165,7 @@ protected function validationMethodExists(string $name): bool * Returns an array that can be used to describe the internal state of this * object. * - * @return array + * @return array */ public function __debugInfo() { diff --git a/TableRegistry.php b/TableRegistry.php index 52a0c42a..7119be7d 100644 --- a/TableRegistry.php +++ b/TableRegistry.php @@ -86,9 +86,9 @@ public static function setTableLocator(LocatorInterface $tableLocator): void * See options specification in {@link TableLocator::get()}. * * @param string $alias The alias name you want to get. - * @param array $options The options you want to build the table with. + * @param array $options The options you want to build the table with. * @return \Cake\ORM\Table - * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\TableLocator::get()} instead. Will be removed in 5.0. + * @deprecated 3.6.0 Use {@link \Cake\ORM\Locator\LocatorAwareTrait::fetchTable()} instead. Will be removed in 5.0. */ public static function get(string $alias, array $options = []): Table { diff --git a/composer.json b/composer.json index c0f2675d..d3f1c880 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ "source": "https://github.com/cakephp/orm" }, "require": { - "php": ">=7.2.0", + "php": ">=7.4.0", "cakephp/collection": "^4.0", "cakephp/core": "^4.0", "cakephp/datasource": "^4.0",