From 552216b6c73cdc60a420db27695936f260810cd5 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Sat, 22 Jun 2024 08:31:24 +0700 Subject: [PATCH] Improve, add tests and fix issues --- src/AbstractActiveRecord.php | 29 +++++++++++++++++---------- src/ActiveQuery.php | 6 ++++-- src/ActiveQueryInterface.php | 7 +++++++ tests/ActiveRecordTest.php | 15 ++++++++++++++ tests/Stubs/ActiveRecord/Customer.php | 6 ++++++ 5 files changed, 50 insertions(+), 13 deletions(-) diff --git a/src/AbstractActiveRecord.php b/src/AbstractActiveRecord.php index 3a59df9f3..8741c4651 100644 --- a/src/AbstractActiveRecord.php +++ b/src/AbstractActiveRecord.php @@ -38,6 +38,8 @@ * ActiveRecord is the base class for classes representing relational data in terms of objects. * * See {@see ActiveRecord} for a concrete implementation. + * + * @psalm-import-type ARClass from ActiveQueryInterface */ abstract class AbstractActiveRecord implements ActiveRecordInterface { @@ -260,16 +262,17 @@ public function hasAttribute(string $name): bool * * Call methods declared in {@see ActiveQuery} to further customize the relation. * - * @param string $class The class name of the related record + * @param string|ActiveRecordInterface|Closure $class The class name of the related record, or an instance of the + * related record, or a Closure to create an {@see ActiveRecordInterface} object. * @param array $link The primary-foreign key constraint. The keys of the array refer to the attributes of the * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in * **this** AR class. * * @return ActiveQueryInterface The relational query object. * - * @psalm-param class-string $class + * @psalm-param ARClass $class */ - public function hasMany(string $class, array $link): ActiveQueryInterface + public function hasMany(string|ActiveRecordInterface|Closure $class, array $link): ActiveQueryInterface { return $this->createRelationQuery($class, $link, true); } @@ -298,16 +301,17 @@ public function hasMany(string $class, array $link): ActiveQueryInterface * * Call methods declared in {@see ActiveQuery} to further customize the relation. * - * @param string $class The class name of the related record. + * @param string|ActiveRecordInterface|Closure $class The class name of the related record, or an instance of the + * related record, or a Closure to create an {@see ActiveRecordInterface} object. * @param array $link The primary-foreign key constraint. The keys of the array refer to the attributes of the * record associated with the `$class` model, while the values of the array refer to the corresponding attributes in * **this** AR class. * * @return ActiveQueryInterface The relational query object. * - * @psalm-param class-string $class + * @psalm-param ARClass $class */ - public function hasOne(string $class, array $link): ActiveQueryInterface + public function hasOne(string|ActiveRecordInterface|Closure $class, array $link): ActiveQueryInterface { return $this->createRelationQuery($class, $link, false); } @@ -318,9 +322,12 @@ public function insert(array $attributes = null): bool } /** - * @psalm-param class-string $arClass + * @param string|ActiveRecordInterface|Closure $arClass The class name of the related record, or an instance of the + * related record, or a Closure to create an {@see ActiveRecordInterface} object. + * + * @psalm-param ARClass $arClass */ - public function instantiateQuery(string $arClass): ActiveQueryInterface + public function instantiateQuery(string|ActiveRecordInterface|Closure $arClass): ActiveQueryInterface { return new ActiveQuery($arClass, $this->db); } @@ -1070,18 +1077,18 @@ protected function setRelationDependencies( /** * Creates a query instance for `has-one` or `has-many` relation. * - * @param string $arClass The class name of the related record. + * @param string|ActiveRecordInterface|Closure $arClass The class name of the related record. * @param array $link The primary-foreign key constraint. * @param bool $multiple Whether this query represents a relation to more than one record. * * @return ActiveQueryInterface The relational query object. * - * @psalm-param class-string $arClass + * @psalm-param ARClass $arClass * {@see hasOne()} * {@see hasMany()} */ - protected function createRelationQuery(string $arClass, array $link, bool $multiple): ActiveQueryInterface + protected function createRelationQuery(string|ActiveRecordInterface|Closure $arClass, array $link, bool $multiple): ActiveQueryInterface { return $this->instantiateQuery($arClass)->primaryModel($this)->link($link)->multiple($multiple); } diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index 3008a97ed..60b27c38a 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -102,6 +102,8 @@ * These methods may only be called in a relational context. Same is true for {@see inverseOf()}, which marks a relation * as inverse of another relation and {@see onCondition()} which adds a condition that's to be added to relational * query join condition. + * + * @psalm-import-type ARClass from ActiveQueryInterface */ class ActiveQuery extends Query implements ActiveQueryInterface { @@ -113,7 +115,7 @@ class ActiveQuery extends Query implements ActiveQueryInterface private array $joinWith = []; /** - * @psalm-param class-string|ActiveRecordInterface|Closure $arClass + * @psalm-param ARClass $arClass */ final public function __construct( protected string|ActiveRecordInterface|Closure $arClass, @@ -990,7 +992,7 @@ public function getARClassName(): string public function getARInstance(): ActiveRecordInterface { if ($this->arClass instanceof ActiveRecordInterface) { - return $this->arClass; + return clone $this->arClass; } if ($this->arClass instanceof Closure) { diff --git a/src/ActiveQueryInterface.php b/src/ActiveQueryInterface.php index 7799cd14f..0e1963183 100644 --- a/src/ActiveQueryInterface.php +++ b/src/ActiveQueryInterface.php @@ -22,6 +22,8 @@ * represents a relation between two active record classes and will return related records only. * * A class implementing this interface should also use {@see ActiveQueryTrait} and {@see ActiveRelationTrait}. + * + * @psalm-type ARClass = class-string|ActiveRecordInterface|Closure():ActiveRecordInterface */ interface ActiveQueryInterface extends QueryInterface { @@ -298,6 +300,11 @@ public function getTablesUsedInFrom(): array; */ public function getSql(): string|null; + /** + * @return string|ActiveRecordInterface|Closure The AR class associated with this query. + * + * @psalm-return ARClass + */ public function getARClass(): string|ActiveRecordInterface|Closure; /** diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 55f797d5b..8447e77d4 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -948,4 +948,19 @@ public function testGetDirtyAttributesAfterFind(): void $customer->getDirtyAttributes(['id', 'email', 'address', 'status', 'unknown']), ); } + + public function testRelationWithInstance(): void + { + $this->checkFixture($this->db, 'customer'); + + $customerQuery = new ActiveQuery(Customer::class, $this->db); + $customer = $customerQuery->findOne(2); + + $orders = $customer->getOrdersUsingInstance(); + + $this->assertTrue($customer->isRelationPopulated('ordersUsingInstance')); + $this->assertCount(2, $orders); + $this->assertSame(2, $orders[0]->getId()); + $this->assertSame(3, $orders[1]->getId()); + } } diff --git a/tests/Stubs/ActiveRecord/Customer.php b/tests/Stubs/ActiveRecord/Customer.php index 923c28b3e..99dfe9944 100644 --- a/tests/Stubs/ActiveRecord/Customer.php +++ b/tests/Stubs/ActiveRecord/Customer.php @@ -53,6 +53,7 @@ public function relationQuery(string $name): ActiveQueryInterface 'orderItems' => $this->getOrderItemsQuery(), 'orderItems2' => $this->getOrderItems2Query(), 'items2' => $this->getItems2Query(), + 'ordersUsingInstance' => $this->hasMany(new Order($this->db()), ['customer_id' => 'id']), default => parent::relationQuery($name), }; } @@ -261,4 +262,9 @@ public function getItems2Query(): ActiveQuery return $this->hasMany(Item::class, ['id' => 'item_id']) ->via('orderItems2'); } + + public function getOrdersUsingInstance(): array + { + return $this->relation('ordersUsingInstance'); + } }