diff --git a/src/AbstractActiveRecord.php b/src/AbstractActiveRecord.php index 9be5bc6eb..0be18c184 100644 --- a/src/AbstractActiveRecord.php +++ b/src/AbstractActiveRecord.php @@ -56,17 +56,11 @@ public function __construct( /** * Returns the public and protected property values of an Active Record object. * - * This method is provided because a direct call of {@see get_object_vars()} within the {@see AbstractActiveRecord} - * class will return also private property values of {@see AbstractActiveRecord} class. - * - * @param ActiveRecordInterface $object - * * @return array - * @link https://www.php.net/manual/en/function.get-object-vars.php * * @psalm-return array */ - abstract protected function getObjectVars(ActiveRecordInterface $object): array; + abstract protected function getAttributesInternal(): array; /** * Inserts Active Record values into DB without considering transaction. @@ -112,18 +106,18 @@ public function equals(ActiveRecordInterface $record): bool public function getAttribute(string $name): mixed { - return $this->getObjectVars($this)[$name] ?? null; + return $this->getAttributesInternal()[$name] ?? null; } - public function getAttributes(array $names = null, array $except = []): array + public function getAttributes(array|null $names = null, array $except = []): array { $names ??= $this->attributes(); - if ($except !== []) { + if (!empty($except)) { $names = array_diff($names, $except); } - return array_intersect_key($this->getObjectVars($this), array_flip($names)); + return array_intersect_key($this->getAttributesInternal(), array_flip($names)); } public function getIsNewRecord(): bool @@ -196,10 +190,8 @@ public function getOldPrimaryKey(bool $asArray = false): mixed ); } - if (count($keys) === 1) { - $key = $this->oldAttributes[$keys[0]] ?? null; - - return $asArray ? [$keys[0] => $key] : $key; + if ($asArray === false && count($keys) === 1) { + return $this->oldAttributes[$keys[0]] ?? null; } $values = []; @@ -215,8 +207,8 @@ public function getPrimaryKey(bool $asArray = false): mixed { $keys = $this->primaryKey(); - if (count($keys) === 1) { - return $asArray ? [$keys[0] => $this->getAttribute($keys[0])] : $this->getAttribute($keys[0]); + if ($asArray === false && count($keys) === 1) { + return $this->getAttribute($keys[0]); } $values = []; @@ -345,11 +337,13 @@ public function instantiateQuery(string $arClass): ActiveQueryInterface */ public function isAttributeChanged(string $name, bool $identical = true): bool { - if (!isset($this->oldAttributes[$name])) { - return array_key_exists($name, $this->getObjectVars($this)); + $attributes = $this->getAttributesInternal(); + + if (empty($this->oldAttributes) || !array_key_exists($name, $this->oldAttributes)) { + return array_key_exists($name, $attributes); } - return $this->getAttribute($name) !== $this->oldAttributes[$name]; + return !array_key_exists($name, $attributes) || $attributes[$name] !== $this->oldAttributes[$name]; } public function isPrimaryKey(array $keys): bool @@ -537,6 +531,10 @@ public function optimisticLock(): string|null */ public function populateRecord(array|object $row): void { + if ($row instanceof ActiveRecordInterface) { + $row = $row->getAttributes(); + } + foreach ($row as $name => $value) { $this->populateAttribute($name, $value); $this->oldAttributes[$name] = $value; @@ -674,7 +672,7 @@ public function setAttributes(array $values): void */ public function setIsNewRecord(bool $value): void { - $this->oldAttributes = $value ? null : $this->getObjectVars($this); + $this->oldAttributes = $value ? null : $this->getAttributesInternal(); } /** diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index dd866912b..b68f9a808 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -23,6 +23,12 @@ use Yiisoft\Definitions\Exception\NotInstantiableException; use Yiisoft\Factory\NotFoundException; +use function array_column; +use function array_combine; +use function array_flip; +use function array_intersect_key; +use function array_key_first; +use function array_map; use function array_merge; use function array_values; use function count; @@ -269,7 +275,7 @@ public function populate(array $rows, Closure|string|null $indexBy = null): arra $this->addInverseRelations($models); } - return ArArrayHelper::populate($models, $indexBy); + return ArArrayHelper::index($models, $indexBy); } /** @@ -289,53 +295,52 @@ public function populate(array $rows, Closure|string|null $indexBy = null): arra */ private function removeDuplicatedModels(array $models): array { - $hash = []; + $model = reset($models); - $pks = $this->getARInstance()->primaryKey(); + if ($this->asArray) { + $pks = $this->getARInstance()->primaryKey(); - if (count($pks) > 1) { - // Composite primary key. - foreach ($models as $i => $model) { - $key = []; - foreach ($pks as $pk) { - if (!isset($model[$pk])) { - // Don't continue if the primary key isn't part of the result set. - break 2; - } - $key[] = $model[$pk]; - } - - $key = serialize($key); - - if (isset($hash[$key])) { - unset($models[$i]); - } else { - $hash[$key] = true; - } + if (empty($pks)) { + throw new InvalidConfigException("Primary key of '$this->arClass' can not be empty."); } - } elseif (empty($pks)) { - throw new InvalidConfigException("Primary key of '$this->arClass' can not be empty."); - } else { - // Single column primary key. - $pk = reset($pks); - foreach ($models as $i => $model) { + foreach ($pks as $pk) { if (!isset($model[$pk])) { - // Don't continue if the primary key isn't part of the result set. - break; + return $models; } + } - $key = $model[$pk]; + if (count($pks) === 1) { + $hash = array_column($models, reset($pks)); + } else { + $flippedPks = array_flip($pks); + $hash = array_map( + static fn ($model): string => serialize(array_intersect_key($model, $flippedPks)), + $models + ); + } + } else { + $pks = $model->getPrimaryKey(true); - if (isset($hash[$key])) { - unset($models[$i]); - } else { - $hash[$key] = true; + if (empty($pks)) { + throw new InvalidConfigException("Primary key of '$this->arClass' can not be empty."); + } + + foreach ($pks as $pk) { + if ($pk === null) { + return $models; } } + + if (count($pks) === 1) { + $key = array_key_first($pks); + $hash = array_map(static fn ($model): string => (string) $model->getAttribute($key), $models); + } else { + $hash = array_map(static fn ($model): string => serialize($model->getPrimaryKey(true)), $models); + } } - return array_values($models); + return array_values(array_combine($hash, $models)); } /** diff --git a/src/ActiveRecordInterface.php b/src/ActiveRecordInterface.php index cdcc7d25e..21d8a2327 100644 --- a/src/ActiveRecordInterface.php +++ b/src/ActiveRecordInterface.php @@ -132,7 +132,7 @@ public function getAttribute(string $name): mixed; * * @return array Attribute values (name => value). */ - public function getAttributes(array $names = null, array $except = []): array; + public function getAttributes(array|null $names = null, array $except = []): array; /** * Returns a value indicating whether the current record is new (not saved in the database). diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index da12861f5..e15d8ddec 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -4,24 +4,26 @@ namespace Yiisoft\ActiveRecord; +use Closure; use ReflectionException; -use Stringable; use Throwable; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\NotSupportedException; -use Yiisoft\Db\Expression\ArrayExpression; +use function array_column; use function array_combine; +use function array_diff_key; +use function array_fill_keys; +use function array_filter; +use function array_intersect_key; use function array_keys; use function array_merge; use function array_unique; -use function array_values; use function count; use function is_array; use function is_object; -use function is_scalar; use function is_string; use function key; use function reset; @@ -193,24 +195,21 @@ private function addInverseRelations(array &$result): void return; } - foreach ($result as $i => $relatedModel) { - if ($relatedModel instanceof ActiveRecordInterface) { - if (!isset($inverseRelation)) { - /** @var ActiveQuery $inverseRelation */ - $inverseRelation = $relatedModel->relationQuery($this->inverseOf); - } - $relatedModel->populateRelation( - $this->inverseOf, - $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel - ); - } else { - if (!isset($inverseRelation)) { - /** @var ActiveQuery $inverseRelation */ - $inverseRelation = $this->getARInstance()->relationQuery($this->inverseOf); - } + $relatedModel = reset($result); + + if ($relatedModel instanceof ActiveRecordInterface) { + $inverseRelation = $relatedModel->relationQuery($this->inverseOf); + $primaryModel = $inverseRelation->getMultiple() ? [$this->primaryModel] : $this->primaryModel; + + foreach ($result as $relatedModel) { + $relatedModel->populateRelation($this->inverseOf, $primaryModel); + } + } else { + $inverseRelation = $this->getARInstance()->relationQuery($this->inverseOf); + $primaryModel = $inverseRelation->getMultiple() ? [$this->primaryModel] : $this->primaryModel; - $result[$i][$this->inverseOf] = $inverseRelation->multiple - ? [$this->primaryModel] : $this->primaryModel; + foreach ($result as &$relatedModel) { + $relatedModel[$this->inverseOf] = $primaryModel; } } } @@ -275,20 +274,17 @@ public function populateRelation(string $name, array &$primaryModels): array $models = $this->all(); if (isset($viaModels, $viaQuery)) { - $buckets = $this->buildBuckets($models, $this->link, $viaModels, $viaQuery); + $buckets = $this->buildBuckets($models, $viaModels, $viaQuery); } else { - $buckets = $this->buildBuckets($models, $this->link); + $buckets = $this->buildBuckets($models); } $this->indexBy($indexBy); - $indexBy = $this->getIndexBy(); - if ($indexBy !== null && $this->multiple) { $buckets = $this->indexBuckets($buckets, $indexBy); } - $link = array_values($this->link); if (isset($viaQuery)) { $deepViaQuery = $viaQuery; @@ -296,30 +292,41 @@ public function populateRelation(string $name, array &$primaryModels): array $deepViaQuery = is_array($deepViaQuery->via) ? $deepViaQuery->via[1] : $deepViaQuery->via; } - $link = array_values($deepViaQuery->link); + $link = $deepViaQuery->link; + } else { + $link = $this->link; } foreach ($primaryModels as $i => $primaryModel) { $keys = null; + if ($this->multiple && count($link) === 1) { $primaryModelKey = reset($link); - $keys = $primaryModel[$primaryModelKey] ?? null; + + if ($primaryModel instanceof ActiveRecordInterface) { + $keys = $primaryModel->getAttribute($primaryModelKey); + } else { + $keys = $primaryModel[$primaryModelKey] ?? null; + } } + if (is_array($keys)) { $value = []; + foreach ($keys as $key) { - $key = $this->normalizeModelKey($key); + $key = (string) $key; + if (isset($buckets[$key])) { - if ($indexBy !== null) { - /** if indexBy is set, array_merge will cause renumbering of numeric array */ - foreach ($buckets[$key] as $bucketKey => $bucketValue) { - $value[$bucketKey] = $bucketValue; - } - } else { - $value = array_merge($value, $buckets[$key]); - } + $value[] = $buckets[$key]; } } + + if ($indexBy !== null) { + /** if indexBy is set, array_merge will cause renumbering of numeric array */ + $value = array_replace(...$value); + } else { + $value = array_merge(...$value); + } } else { $key = $this->getModelKey($primaryModel, $link); $value = $buckets[$key] ?? ($this->multiple ? [] : null); @@ -331,6 +338,7 @@ public function populateRelation(string $name, array &$primaryModels): array $primaryModels[$i][$name] = $value; } } + if ($this->inverseOf !== null) { $this->populateInverseRelation($primaryModels, $models, $name, $this->inverseOf); } @@ -354,66 +362,84 @@ private function populateInverseRelation( $model = reset($models); if ($model instanceof ActiveRecordInterface) { - /** @var ActiveQuery $relation */ - $relation = $model->relationQuery($name); - } else { - /** @var ActiveQuery $relation */ - $relation = $this->getARInstance()->relationQuery($name); + $this->populateInverseRelationToModels($models, $primaryModels, $name); + return; } - if ($relation->getMultiple()) { - $buckets = $this->buildBuckets($primaryModels, $relation->getLink(), null, null, false); - if ($model instanceof ActiveRecordInterface) { - foreach ($models as $model) { - $key = $this->getModelKey($model, $relation->getLink()); - if ($model instanceof ActiveRecordInterface) { - $model->populateRelation($name, $buckets[$key] ?? []); + $primaryModel = reset($primaryModels); + + if ($primaryModel instanceof ActiveRecordInterface) { + if ($this->multiple) { + foreach ($primaryModels as $primaryModel) { + $models = $primaryModel->relation($primaryName); + if (!empty($models)) { + $this->populateInverseRelationToModels($models, $primaryModels, $name); + $primaryModel->populateRelation($primaryName, $models); } } } else { - foreach ($primaryModels as $i => $primaryModel) { - if ($this->multiple) { - foreach ($primaryModel as $j => $m) { - $key = $this->getModelKey($m, $relation->getLink()); - $primaryModels[$i][$j][$name] = $buckets[$key] ?? []; - } - } elseif (!empty($primaryModel[$primaryName])) { - $key = $this->getModelKey($primaryModel[$primaryName], $relation->getLink()); - $primaryModels[$i][$primaryName][$name] = $buckets[$key] ?? []; + foreach ($primaryModels as $primaryModel) { + $model = $primaryModel->relation($primaryName); + if (!empty($model)) { + $models = [$model]; + $this->populateInverseRelationToModels($models, $primaryModels, $name); + $primaryModel->populateRelation($primaryName, $models[0]); } } } - } elseif ($this->multiple) { - foreach ($primaryModels as $i => $primaryModel) { - foreach ($primaryModel[$primaryName] as $j => $m) { - if ($m instanceof ActiveRecordInterface) { - $m->populateRelation($name, $primaryModel); - } else { - $primaryModels[$i][$primaryName][$j][$name] = $primaryModel; + } else { + if ($this->multiple) { + foreach ($primaryModels as &$primaryModel) { + if (!empty($primaryModel[$primaryName])) { + $this->populateInverseRelationToModels($primaryModel[$primaryName], $primaryModels, $name); + } + } + } else { + foreach ($primaryModels as &$primaryModel) { + if (!empty($primaryModel[$primaryName])) { + $models = [$primaryModel[$primaryName]]; + $this->populateInverseRelationToModels($models, $primaryModels, $name); + $primaryModel[$primaryName] = $models[0]; } } } + } + } + + private function populateInverseRelationToModels(array &$models, array $primaryModels, string $name): void + { + $model = reset($models); + $isArray = is_array($model); + + /** @var ActiveQuery $relation */ + $relation = $isArray ? $this->getARInstance()->relationQuery($name) : $model->relationQuery($name); + $buckets = $relation->buildBuckets($primaryModels); + $link = $relation->getLink(); + $default = $relation->getMultiple() ? [] : null; + + if ($isArray) { + /** @var array $model */ + foreach ($models as &$model) { + $key = $this->getModelKey($model, $link); + $model[$name] = $buckets[$key] ?? $default; + } } else { - foreach ($primaryModels as $i => $primaryModel) { - if ($primaryModel[$primaryName] instanceof ActiveRecordInterface) { - $primaryModel[$primaryName]->populateRelation($name, $primaryModel); - } elseif (!empty($primaryModel[$primaryName])) { - $primaryModels[$i][$primaryName][$name] = $primaryModel; - } + /** @var ActiveRecordInterface $model */ + foreach ($models as $model) { + $key = $this->getModelKey($model, $link); + $model->populateRelation($name, $buckets[$key] ?? $default); } } } private function buildBuckets( array $models, - array $link, array $viaModels = null, - self $viaQuery = null, - bool $checkMultiple = true + self $viaQuery = null ): array { if ($viaModels !== null) { $map = []; - $linkValues = array_values($link); + $linkValues = $this->link; $viaLink = $viaQuery->link ?? []; $viaLinkKeys = array_keys($viaLink); $viaVia = null; @@ -443,7 +469,7 @@ private function buildBuckets( } $buckets = []; - $linkKeys = array_keys($link); + $linkKeys = array_keys($this->link); if (isset($map)) { foreach ($models as $model) { @@ -462,10 +488,11 @@ private function buildBuckets( } } - if ($checkMultiple && !$this->multiple) { - foreach ($buckets as $i => $bucket) { - $buckets[$i] = reset($bucket); - } + if (!$this->multiple) { + return array_combine( + array_keys($buckets), + array_column($buckets, 0) + ); } return $buckets; @@ -487,24 +514,18 @@ private function mapVia(array $map, array $viaMap): array } /** - * Indexes buckets by column name. + * Indexes buckets by a column name. * - * @param callable|string $indexBy the name of the column by which the query results should be indexed by. This can - * also be a callable(e.g. anonymous function) that returns the index value based on the given row data. + * @param Closure|string $indexBy the name of the column by which the query results should be indexed by. This can + * also be a {@see Closure} that returns the index value based on the given models data. */ - private function indexBuckets(array $buckets, callable|string $indexBy): array + private function indexBuckets(array $buckets, Closure|string $indexBy): array { - $result = []; - - foreach ($buckets as $key => $models) { - $result[$key] = []; - foreach ($models as $model) { - $index = is_string($indexBy) ? $model[$indexBy] : $indexBy($model); - $result[$key][$index] = $model; - } + foreach ($buckets as &$models) { + $models = ArArrayHelper::index($models, $indexBy); } - return $result; + return $buckets; } /** @@ -542,76 +563,87 @@ private function prefixKeyColumns(array $attributes): array protected function filterByModels(array $models): void { $attributes = array_keys($this->link); - $attributes = $this->prefixKeyColumns($attributes); + $model = reset($models); $values = []; + if (count($attributes) === 1) { /** single key */ $attribute = reset($this->link); - foreach ($models as $model) { - $value = isset($model[$attribute]) || (is_object($model) && property_exists($model, $attribute)) ? $model[$attribute] : null; - if ($value !== null) { - if (is_array($value)) { - $values = array_merge($values, $value); - } elseif ($value instanceof ArrayExpression && $value->getDimension() === 1) { - $values = array_merge($values, $value->getValue()); - } else { - $values[] = $value; + + if ($model instanceof ActiveRecordInterface) { + foreach ($models as $model) { + $value = $model->getAttribute($attribute); + + if ($value !== null) { + if (is_array($value)) { + $values = [...$values, ...$value]; + } else { + $values[] = $value; + } + } + } + } else { + foreach ($models as $model) { + if (isset($model[$attribute])) { + $value = $model[$attribute]; + + if (is_array($value)) { + $values = [...$values, ...$value]; + } else { + $values[] = $value; + } } } } - if (empty($values)) { - $this->emulateExecution(); + if (!empty($values)) { + $scalarValues = array_filter($values, 'is_scalar'); + $nonScalarValues = array_diff_key($values, $scalarValues); + + $scalarValues = array_unique($scalarValues); + $values = [...$scalarValues, ...$nonScalarValues]; } } else { - /** - * composite keys ensure keys of $this->link are prefixed the same way as $attributes. - */ - $prefixedLink = array_combine($attributes, $this->link); + $nulls = array_fill_keys($this->link, null); - foreach ($models as $model) { - $v = []; + if ($model instanceof ActiveRecordInterface) { + foreach ($models as $model) { + $value = $model->getAttributes($this->link); - foreach ($prefixedLink as $attribute => $link) { - $v[$attribute] = $model[$link]; + if (!empty($value)) { + $values[] = array_combine($attributes, array_merge($nulls, $value)); + } } + } else { + foreach ($models as $model) { + $value = array_intersect_key($model, $nulls); - $values[] = $v; - - if (empty($v)) { - $this->emulateExecution(); + if (!empty($value)) { + $values[] = array_combine($attributes, array_merge($nulls, $value)); + } } } } - if (!empty($values)) { - $scalarValues = []; - $nonScalarValues = []; - foreach ($values as $value) { - if (is_scalar($value)) { - $scalarValues[] = $value; - } else { - $nonScalarValues[] = $value; - } - } - - $scalarValues = array_unique($scalarValues); - $values = [...$scalarValues, ...$nonScalarValues]; + if (empty($values)) { + $this->emulateExecution(); + $this->andWhere('1=0'); + return; } $this->andWhere(['in', $attributes, $values]); } - private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): false|int|string + private function getModelKey(ActiveRecordInterface|array $activeRecord, array $attributes): string { $key = []; if (is_array($activeRecord)) { foreach ($attributes as $attribute) { if (isset($activeRecord[$attribute])) { - $key[] = $this->normalizeModelKey($activeRecord[$attribute]); + $key[] = (string) $activeRecord[$attribute]; } } } else { @@ -619,36 +651,16 @@ private function getModelKey(ActiveRecordInterface|array $activeRecord, array $a $value = $activeRecord->getAttribute($attribute); if ($value !== null) { - $key[] = $this->normalizeModelKey($value); + $key[] = (string) $value; } } } - if (count($key) > 1) { - return serialize($key); - } - - $key = reset($key); - - return is_scalar($key) ? $key : serialize($key); - } - - /** - * @param int|string|Stringable|null $value raw key value. - * - * @return int|string|null normalized key value. - */ - private function normalizeModelKey(int|string|Stringable|null $value): int|string|null - { - if ($value instanceof Stringable) { - /** - * ensure matching to special objects, which are convertible to string, for cross-DBMS relations, - * for example: `|MongoId` - */ - $value = (string) $value; - } - - return $value; + return match (count($key)) { + 0 => '', + 1 => $key[0], + default => serialize($key), + }; } /** diff --git a/src/ArArrayHelper.php b/src/ArArrayHelper.php index 661623abb..d1b8246e8 100644 --- a/src/ArArrayHelper.php +++ b/src/ArArrayHelper.php @@ -5,11 +5,14 @@ namespace Yiisoft\ActiveRecord; use Closure; +use Traversable; use function array_combine; use function array_key_exists; use function array_map; use function get_object_vars; +use function is_array; +use function iterator_to_array; use function property_exists; use function strrpos; use function substr; @@ -96,8 +99,7 @@ public static function getValueByPath(ActiveRecordInterface|array $array, string } if (property_exists($array, $key)) { - $values = get_object_vars($array); - return array_key_exists($key, $values) ? $values[$key] : $default; + return array_key_exists($key, get_object_vars($array)) ? $array->$key : $default; } if ($array->isRelationPopulated($key)) { @@ -118,7 +120,7 @@ public static function getValueByPath(ActiveRecordInterface|array $array, string } /** - * Populates an array of rows with the specified column value as keys. + * Indexes an array of rows with the specified column value as keys. * * The input array should be multidimensional or an array of {@see ActiveRecordInterface} instances. * @@ -144,7 +146,7 @@ public static function getValueByPath(ActiveRecordInterface|array $array, string * * @return array[] */ - public static function populate(array $rows, Closure|string|null $indexBy = null): array + public static function index(array $rows, Closure|string|null $indexBy = null): array { if ($indexBy === null) { return $rows; @@ -163,4 +165,28 @@ public static function populate(array $rows, Closure|string|null $indexBy = null return $result; } + + /** + * Converts an object into an array. + * + * @param array|object $object The object to be converted into an array. + * + * @return array The array representation of the object. + */ + public static function toArray(array|object $object): array + { + if (is_array($object)) { + return $object; + } + + if ($object instanceof ActiveRecordInterface) { + return $object->getAttributes(); + } + + if ($object instanceof Traversable) { + return iterator_to_array($object); + } + + return get_object_vars($object); + } } diff --git a/src/BaseActiveRecord.php b/src/BaseActiveRecord.php index 02c69ecd0..df4f6305e 100644 --- a/src/BaseActiveRecord.php +++ b/src/BaseActiveRecord.php @@ -162,16 +162,15 @@ public function loadDefaultValues(bool $skipIfSet = true): self public function populateRecord(array|object $row): void { + $row = ArArrayHelper::toArray($row); $columns = $this->getTableSchema()->getColumns(); + $rowColumns = array_intersect_key($row, $columns); - /** @psalm-var array[][] $row */ - foreach ($row as $name => $value) { - if (isset($columns[$name])) { - $row[$name] = $columns[$name]->phpTypecast($value); - } + foreach ($rowColumns as $name => &$value) { + $value = $columns[$name]->phpTypecast($value); } - parent::populateRecord($row); + parent::populateRecord($rowColumns + $row); } public function primaryKey(): array @@ -230,9 +229,9 @@ protected function filterValidColumnNames(array $aliases): array return $columnNames; } - protected function getObjectVars(ActiveRecordInterface $object): array + protected function getAttributesInternal(): array { - return get_object_vars($object); + return get_object_vars($this); } protected function insertInternal(array $attributes = null): bool diff --git a/src/Trait/MagicPropertiesTrait.php b/src/Trait/MagicPropertiesTrait.php index 532c638c4..85825aed0 100644 --- a/src/Trait/MagicPropertiesTrait.php +++ b/src/Trait/MagicPropertiesTrait.php @@ -14,10 +14,6 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\UnknownPropertyException; -use function array_diff; -use function array_flip; -use function array_intersect_key; -use function array_key_exists; use function array_merge; use function get_object_vars; use function in_array; @@ -54,6 +50,7 @@ */ trait MagicPropertiesTrait { + /** @psalm-var array $attributes */ private array $attributes = []; /** @@ -171,49 +168,11 @@ public function __set(string $name, mixed $value): void throw new UnknownPropertyException('Setting unknown property: ' . static::class . '::' . $name); } - public function getAttribute(string $name): mixed - { - if ($name !== 'attributes' && property_exists($this, $name)) { - return get_object_vars($this)[$name] ?? null; - } - - return $this->attributes[$name] ?? null; - } - - public function getAttributes(array $names = null, array $except = []): array - { - $names ??= $this->attributes(); - $attributes = array_merge($this->attributes, get_object_vars($this)); - - if ($except !== []) { - $names = array_diff($names, $except); - } - - return array_intersect_key($attributes, array_flip($names)); - } - public function hasAttribute(string $name): bool { return isset($this->attributes[$name]) || in_array($name, $this->attributes(), true); } - public function isAttributeChanged(string $name, bool $identical = true): bool - { - $hasOldAttribute = array_key_exists($name, $this->getOldAttributes()); - - if (!$hasOldAttribute) { - return property_exists($this, $name) && array_key_exists($name, get_object_vars($this)) - || array_key_exists($name, $this->attributes); - } - - if (property_exists($this, $name)) { - return $this->getOldAttribute($name) !== $this->$name; - } - - return !array_key_exists($name, $this->attributes) - || $this->getOldAttribute($name) !== $this->attributes[$name]; - } - public function setAttribute(string $name, mixed $value): void { if ($this->hasAttribute($name)) { @@ -264,6 +223,12 @@ public function canSetProperty(string $name, bool $checkVars = true): bool || $this->hasAttribute($name); } + /** @psalm-return array */ + protected function getAttributesInternal(): array + { + return array_merge($this->attributes, parent::getAttributesInternal()); + } + protected function populateAttribute(string $name, mixed $value): void { if ($name !== 'attributes' && property_exists($this, $name)) {