diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index f7359dbb0..62b833dcc 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -14,7 +14,7 @@ use Yiisoft\Db\Exception\NotSupportedException; use Yiisoft\Db\Expression\ExpressionInterface; use Yiisoft\Db\Helper\DbArrayHelper; -use Yiisoft\Db\Query\BatchQueryResultInterface; +use Yiisoft\Db\Query\DataReaderInterface; use Yiisoft\Db\Query\Query; use Yiisoft\Db\Query\QueryInterface; use Yiisoft\Db\QueryBuilder\QueryBuilderInterface; @@ -25,7 +25,6 @@ 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; @@ -104,7 +103,7 @@ * @psalm-import-type ARClass from ActiveQueryInterface * @psalm-import-type IndexKey from ArArrayHelper * - * @psalm-property IndexKey $indexBy + * @psalm-property IndexKey|null $indexBy * @psalm-suppress ClassMustBeFinal */ class ActiveQuery extends Query implements ActiveQueryInterface @@ -125,23 +124,13 @@ final public function __construct( parent::__construct($this->getARInstance()->db()); } - public function all(): array + public function each(): DataReaderInterface { - if ($this->shouldEmulateExecution()) { - return []; - } - - return $this->populate($this->createCommand()->queryAll(), $this->indexBy); - } - - public function batch(int $batchSize = 100): BatchQueryResultInterface - { - return parent::batch($batchSize)->setPopulatedMethod($this->populate(...)); - } - - public function each(int $batchSize = 100): BatchQueryResultInterface - { - return parent::each($batchSize)->setPopulatedMethod($this->populate(...)); + /** @psalm-suppress InvalidArgument */ + return $this->createCommand() + ->query() + ->indexBy($this->indexBy) + ->resultCallback($this->populateOne(...)); } /** @@ -235,22 +224,25 @@ public function prepare(QueryBuilderInterface $builder): QueryInterface * @throws NotSupportedException * @throws ReflectionException * @throws Throwable + * + * @psalm-param list $rows + * @psalm-return ( + * $rows is non-empty-list + * ? non-empty-list + * : list + * ) */ - public function populate(array $rows, Closure|string|null $indexBy = null): array + public function populate(array $rows): array { if (empty($rows)) { return []; } - $models = $this->createModels($rows); - - if (empty($models)) { - return []; + if (!empty($this->join) && $this->indexBy === null) { + $rows = $this->removeDuplicatedRows($rows); } - if (!empty($this->join) && $this->getIndexBy() === null) { - $models = $this->removeDuplicatedModels($models); - } + $models = $this->createModels($rows); if (!empty($this->with)) { $this->findWith($this->with, $models); @@ -260,94 +252,68 @@ public function populate(array $rows, Closure|string|null $indexBy = null): arra $this->addInverseRelations($models); } - return ArArrayHelper::index($models, $indexBy); + return $models; } /** - * Removes duplicated models by checking their primary key values. + * Removes duplicated rows by checking their primary key values. * * This method is mainly called when a join query is performed, which may cause duplicated rows being returned. * - * @param ActiveRecordInterface[]|array[] $models The models to be checked. + * @param array[] $rows The rows to be checked. * * @throws CircularReferenceException * @throws Exception * @throws InvalidConfigException * @throws NotInstantiableException * - * @return ActiveRecordInterface[]|array[] The distinctive models. + * @return array[] The distinctive rows. + * + * @psalm-param non-empty-list $rows + * @psalm-return non-empty-list */ - private function removeDuplicatedModels(array $models): array + private function removeDuplicatedRows(array $rows): array { - $model = reset($models); + $instance = $this->getARInstance(); + $pks = $instance->primaryKey(); - if ($this->asArray) { - $instance = $this->getARInstance(); - $pks = $instance->primaryKey(); + if (empty($pks)) { + throw new InvalidConfigException('Primary key of "' . $instance::class . '" can not be empty.'); + } - if (empty($pks)) { - throw new InvalidConfigException('Primary key of "' . $instance::class . '" can not be empty.'); - } - - foreach ($pks as $pk) { - /** @var array $model */ - if (!isset($model[$pk])) { - return $models; - } + foreach ($pks as $pk) { + if (!isset($rows[0][$pk])) { + return $rows; } + } - /** @var array[] $models */ - if (count($pks) === 1) { - $hash = array_column($models, reset($pks)); - } else { - $flippedPks = array_flip($pks); - $hash = array_map( - static fn (array $model): string => serialize(array_intersect_key($model, $flippedPks)), - $models - ); - } + if (count($pks) === 1) { + $hash = array_column($rows, reset($pks)); } else { - /** @var ActiveRecordInterface $model */ - $pks = $model->getPrimaryKey(true); - - if (empty($pks)) { - throw new InvalidConfigException('Primary key of "' . $model::class . '" can not be empty.'); - } - - /** @var ActiveRecordInterface[] $models */ - foreach ($pks as $pk) { - if ($pk === null) { - return $models; - } - } - - if (count($pks) === 1) { - $key = array_key_first($pks); - $hash = array_map( - static fn (ActiveRecordInterface $model): string => (string) $model->get($key), - $models - ); - } else { - $hash = array_map( - static fn (ActiveRecordInterface $model): string => serialize($model->getPrimaryKey(true)), - $models - ); - } + $flippedPks = array_flip($pks); + $hash = array_map( + static fn (array $row): string => serialize(array_intersect_key($row, $flippedPks)), + $rows + ); } - return array_values(array_combine($hash, $models)); + /** @psalm-var non-empty-list */ + return array_values(array_combine($hash, $rows)); } public function one(): array|ActiveRecordInterface|null { - /** @var array|null $row */ - $row = parent::one(); + if ($this->shouldEmulateExecution()) { + return null; + } + + $row = $this->createCommand()->queryOne(); if ($row === null) { return null; } - return $this->populate([$row])[0]; + return $this->populateOne($row); } /** @@ -955,6 +921,11 @@ public function getARInstance(): ActiveRecordInterface return new $class(); } + protected function index(array $rows): array + { + return ArArrayHelper::index($this->populate($rows), $this->indexBy); + } + private function createInstance(): static { return (new static($this->arClass)) @@ -974,4 +945,9 @@ private function createInstance(): static ->params($this->params) ->withQueries($this->withQueries); } + + private function populateOne(array $row): ActiveRecordInterface|array + { + return $this->populate([$row])[0]; + } } diff --git a/src/ActiveQueryInterface.php b/src/ActiveQueryInterface.php index 094ee5156..ff94adb1d 100644 --- a/src/ActiveQueryInterface.php +++ b/src/ActiveQueryInterface.php @@ -351,11 +351,16 @@ public function sql(string|null $value): static; * * @param array[] $rows The raw query result from a database. * - * @psalm-param IndexKey|null $indexBy - * * @return ActiveRecordInterface[]|array[] The converted query result. + * + * @psalm-param list $rows + * @psalm-return ( + * $rows is non-empty-list + * ? non-empty-list + * : list + * ) */ - public function populate(array $rows, Closure|string|null $indexBy = null): array; + public function populate(array $rows): array; /** * Returns related record(s). @@ -640,6 +645,9 @@ public function one(): array|ActiveRecordInterface|null; * @throws InvalidArgumentException|InvalidConfigException|NotSupportedException|Throwable If {@see link()} is * invalid. * @return ActiveRecordInterface[]|array[] The related models. + * + * @psalm-param non-empty-list $primaryModels + * @psalm-param-out non-empty-list $primaryModels */ public function populateRelation(string $name, array &$primaryModels): array; } diff --git a/src/ActiveQueryTrait.php b/src/ActiveQueryTrait.php index 32880b4b7..bc2783b31 100644 --- a/src/ActiveQueryTrait.php +++ b/src/ActiveQueryTrait.php @@ -107,6 +107,9 @@ public function with(array|string ...$with): static * * @throws InvalidConfigException * @return ActiveRecordInterface[]|array[] The model instances. + * + * @psalm-param non-empty-list $rows + * @psalm-return non-empty-list */ protected function createModels(array $rows): array { @@ -114,21 +117,25 @@ protected function createModels(array $rows): array return $rows; } - $arClassInstance = []; - - foreach ($rows as $row) { - $arClass = $this->getARInstance(); + if ($this->resultCallback !== null) { + $rows = ($this->resultCallback)($rows); - if (method_exists($arClass, 'instantiate')) { - $arClass = $arClass->instantiate($row); + if ($rows[0] instanceof ActiveRecordInterface) { + /** @psalm-var non-empty-list */ + return $rows; } + } + + $models = []; + foreach ($rows as $row) { + $arClass = $this->getARInstance(); $arClass->populateRecord($row); - $arClassInstance[] = $arClass; + $models[] = $arClass; } - return $arClassInstance; + return $models; } /** @@ -144,7 +151,8 @@ protected function createModels(array $rows): array * @throws ReflectionException * @throws Throwable * - * @param-out ActiveRecordInterface[]|array[] $models + * @psalm-param non-empty-list $models + * @psalm-param-out non-empty-list $models */ public function findWith(array $with, array &$models): void { diff --git a/src/ActiveRelationTrait.php b/src/ActiveRelationTrait.php index 65d9ee738..aab04161d 100644 --- a/src/ActiveRelationTrait.php +++ b/src/ActiveRelationTrait.php @@ -202,7 +202,8 @@ public function relatedRecords(): ActiveRecordInterface|array|null * * @throws InvalidConfigException * - * @param-out ActiveRecordInterface[]|array[] $result + * @psalm-param non-empty-list $result + * @psalm-param-out non-empty-list $result */ private function addInverseRelations(array &$result): void { @@ -232,9 +233,10 @@ private function addInverseRelations(array &$result): void } /** - * @return ActiveRecordInterface[]|array[] + * @psalm-param non-empty-list $primaryModels + * @psalm-param-out non-empty-list $primaryModels * - * @param-out ActiveRecordInterface[]|array[] $primaryModels + * @return ActiveRecordInterface[]|array[] */ public function populateRelation(string $name, array &$primaryModels): array { @@ -262,16 +264,16 @@ public function populateRelation(string $name, array &$primaryModels): array $models = [$this->one()]; $this->populateInverseRelation($models, $primaryModels); - $primaryModel = reset($primaryModels); + $primaryModel = $primaryModels[0]; if ($primaryModel instanceof ActiveRecordInterface) { $primaryModel->populateRelation($name, $models[0]); } else { /** - * @var array[] $primaryModels - * @psalm-suppress PossiblyNullArrayOffset + * @psalm-var non-empty-list $primaryModels + * @psalm-suppress UndefinedInterfaceMethod */ - $primaryModels[key($primaryModels)][$name] = $models[0]; + $primaryModels[0][$name] = $models[0]; } return $models; @@ -319,12 +321,14 @@ public function populateRelation(string $name, array &$primaryModels): array /** * @throws \Yiisoft\Definitions\Exception\InvalidConfigException + * + * @psalm-param non-empty-list $primaryModels */ private function populateInverseRelation( array &$models, array $primaryModels, ): void { - if ($this->inverseOf === null || empty($models) || empty($primaryModels)) { + if ($this->inverseOf === null || empty($models)) { return; } @@ -643,13 +647,11 @@ private function getModelKeys(ActiveRecordInterface|array $model, array $propert * @throws Throwable * @throws \Yiisoft\Definitions\Exception\InvalidConfigException * @return array[] + * + * @psalm-param non-empty-list $primaryModels */ private function findJunctionRows(array $primaryModels): array { - if (empty($primaryModels)) { - return []; - } - $this->filterByModels($primaryModels); /** @var array[] */ diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 9aa9e4ceb..acb190a4b 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -28,6 +28,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Profile; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\Assert; +use Yiisoft\ActiveRecord\Tests\Support\ModelFactory; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\InvalidCallException; @@ -234,7 +235,7 @@ public function testPopulateRecordCallWhenQueryingOnParentClass(): void $dog = new Dog(); $dog->save(); - $animal = new ActiveQuery(Animal::class); + $animal = (new ActiveQuery(Animal::class))->resultCallback(ModelFactory::create(...)); $animals = $animal->where(['type' => Dog::class])->one(); $this->assertEquals('bark', $animals->getDoes()); diff --git a/tests/BatchQueryResultTest.php b/tests/BatchQueryResultTest.php index ca6d3a89e..038cc426f 100644 --- a/tests/BatchQueryResultTest.php +++ b/tests/BatchQueryResultTest.php @@ -50,8 +50,8 @@ public function testQuery(): void $this->assertCount(3, $allRows); - /** reset */ - $batch->reset(); + /** rewind */ + $batch->rewind(); /** empty query */ $query = $customerQuery->where(['id' => 100]); @@ -89,7 +89,7 @@ public function testQuery(): void $allRows = []; - foreach ($query->each(2) as $index => $row) { + foreach ($query->each() as $index => $row) { $allRows[$index] = $row; } $this->assertCount(3, $allRows); @@ -104,7 +104,7 @@ public function testQuery(): void $allRows = []; - foreach ($query->each(100) as $key => $row) { + foreach ($query->each() as $key => $row) { $allRows[$key] = $row; } diff --git a/tests/MagicActiveRecordTest.php b/tests/MagicActiveRecordTest.php index 051ed35a3..23167fb91 100644 --- a/tests/MagicActiveRecordTest.php +++ b/tests/MagicActiveRecordTest.php @@ -23,6 +23,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\MagicActiveRecord\OrderItemWithNullFK; use Yiisoft\ActiveRecord\Tests\Stubs\MagicActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\Assert; +use Yiisoft\ActiveRecord\Tests\Support\ModelFactory; use Yiisoft\Db\Exception\Exception; use Yiisoft\Db\Exception\InvalidArgumentException; use Yiisoft\Db\Exception\InvalidCallException; @@ -227,7 +228,7 @@ public function testPopulateRecordCallWhenQueryingOnParentClass(): void $dog = new Dog(); $dog->save(); - $animal = new ActiveQuery(Animal::class); + $animal = (new ActiveQuery(Animal::class))->resultCallback(ModelFactory::create(...)); $animals = $animal->where(['type' => Dog::class])->one(); $this->assertEquals('bark', $animals->getDoes()); diff --git a/tests/Stubs/ActiveRecord/Animal.php b/tests/Stubs/ActiveRecord/Animal.php index 5530334b6..b5e5e1e4a 100644 --- a/tests/Stubs/ActiveRecord/Animal.php +++ b/tests/Stubs/ActiveRecord/Animal.php @@ -5,7 +5,6 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord; use Yiisoft\ActiveRecord\ActiveRecord; -use Yiisoft\ActiveRecord\ActiveRecordInterface; /** * Class Animal. @@ -32,13 +31,6 @@ public function getDoes() return $this->does; } - public function instantiate($row): ActiveRecordInterface - { - $class = $row['type']; - - return new $class(); - } - public function setDoes(string $value): void { $this->does = $value; diff --git a/tests/Stubs/MagicActiveRecord/Animal.php b/tests/Stubs/MagicActiveRecord/Animal.php index 2e2d0ae34..c6ad39fbc 100644 --- a/tests/Stubs/MagicActiveRecord/Animal.php +++ b/tests/Stubs/MagicActiveRecord/Animal.php @@ -5,7 +5,6 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\MagicActiveRecord; use Yiisoft\ActiveRecord\Tests\Stubs\MagicActiveRecord; -use Yiisoft\ActiveRecord\ActiveRecordInterface; /** * Class Animal. @@ -32,13 +31,6 @@ public function getDoes() return $this->does; } - public function instantiate($row): ActiveRecordInterface - { - $class = $row['type']; - - return new $class($this->db()); - } - public function setDoes(string $value): void { $this->does = $value; diff --git a/tests/Support/ModelFactory.php b/tests/Support/ModelFactory.php new file mode 100644 index 000000000..5e105d8ec --- /dev/null +++ b/tests/Support/ModelFactory.php @@ -0,0 +1,24 @@ +populateRecord($row); + + $models[] = $model; + } + + return $models; + } +}