From ea6a441eb92c584515d300170fb6e43040e5322e Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 3 Jul 2024 18:06:20 +0700 Subject: [PATCH] Using Dependency Injection With Active Record --- README.md | 3 +- composer-require-checker.json | 3 +- composer.json | 2 + docs/create-model.md | 4 + docs/using-di.md | 101 ++++++++++++++++++ src/Trait/FactoryTrait.php | 59 ++++++++++ tests/ActiveRecordTest.php | 91 ++++++++++++++++ tests/Driver/Mssql/ActiveRecordTest.php | 6 ++ tests/Driver/Mysql/ActiveRecordTest.php | 6 ++ tests/Driver/Oracle/ActiveRecordTest.php | 6 ++ tests/Driver/Pgsql/ActiveRecordTest.php | 6 ++ tests/Driver/Sqlite/ActiveRecordTest.php | 6 ++ .../ActiveRecord/CustomerWithFactory.php | 34 ++++++ tests/Stubs/ActiveRecord/OrderWithFactory.php | 42 ++++++++ tests/Support/ConnectionHelper.php | 9 +- 15 files changed, 368 insertions(+), 10 deletions(-) create mode 100644 docs/using-di.md create mode 100644 src/Trait/FactoryTrait.php create mode 100644 tests/Stubs/ActiveRecord/CustomerWithFactory.php create mode 100644 tests/Stubs/ActiveRecord/OrderWithFactory.php diff --git a/README.md b/README.md index 208ac78de..ffcea217c 100644 --- a/README.md +++ b/README.md @@ -156,7 +156,8 @@ return [ ]; ``` -_For more information about how to configure middleware, follow [Middleware Documentation](https://github.com/yiisoft/docs/blob/master/guide/en/structure/middleware.md)_ +_For more information about how to configure middleware, follow +[Middleware Documentation](https://github.com/yiisoft/docs/blob/master/guide/en/structure/middleware.md)_ Now you can use the Active Record in the action: diff --git a/composer-require-checker.json b/composer-require-checker.json index c17694e98..97c5b755f 100644 --- a/composer-require-checker.json +++ b/composer-require-checker.json @@ -3,6 +3,7 @@ "Psr\\Http\\Message\\ResponseInterface", "Psr\\Http\\Message\\ServerRequestInterface", "Psr\\Http\\Server\\MiddlewareInterface", - "Psr\\Http\\Server\\RequestHandlerInterface" + "Psr\\Http\\Server\\RequestHandlerInterface", + "Yiisoft\\Factory\\Factory" ] } diff --git a/composer.json b/composer.json index dfd088270..8d8629f41 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,7 @@ "yiisoft/cache": "^3.0", "yiisoft/db-sqlite": "dev-master", "yiisoft/di": "^1.0", + "yiisoft/factory": "^1.2", "yiisoft/json": "^1.0", "yiisoft/middleware-dispatcher": "^5.2" }, @@ -54,6 +55,7 @@ "yiisoft/db-pgsql": "For PostgreSQL database support", "yiisoft/db-mssql": "For MSSQL database support", "yiisoft/db-oracle": "For Oracle database support", + "yiisoft/factory": "For factory support", "yiisoft/middleware-dispatcher": "For middleware support" }, "autoload": { diff --git a/docs/create-model.md b/docs/create-model.md index 791bea951..1c6158377 100644 --- a/docs/create-model.md +++ b/docs/create-model.md @@ -289,3 +289,7 @@ $user = $userQuery->where(['id' => 1])->onePopulate(); $profile = $user->getProfile(); $orders = $user->getOrders(); ``` + +Also see [Using Dependency Injection With Active Record Model](docs/using-di.md). + +Back to [README](../README.md) diff --git a/docs/using-di.md b/docs/using-di.md new file mode 100644 index 000000000..f1f2891b0 --- /dev/null +++ b/docs/using-di.md @@ -0,0 +1,101 @@ +# Using Dependency Injection With Active Record + +Using [dependency injection](https://github.com/yiisoft/di) in the Active Record model allows to inject dependencies +into the model and use them in the model methods. + +To create an Active Record model with dependency injection, you need to use +a [factory](https://github.com/yiisoft/factory) that will create an instance of the model and inject the dependencies +into it. + +## Define The Active Record Model + +Yii Active Record provides a `FactoryTrait` trait that allows to use the factory with the Active Record class. + +```php +use Yiisoft\ActiveRecord\ActiveQueryInterface; +use Yiisoft\ActiveRecord\ActiveRecord; +use Yiisoft\ActiveRecord\Trait\FactoryTrait; + +#[\AllowDynamicProperties] +final class User extends ActiveRecord +{ + use FactoryTrait; + + public function __construct(private MyService $myService) + { + } + + public function getTableName(): string + { + return '{{%user}}'; + } + + public function relationQuery(string $name): ActiveQueryInterface + { + return match ($name) { + 'profile' => $this->hasOne(Profile::class, ['id' => 'profile_id']), + 'orders' => $this->hasMany(Order::class, ['user_id' => 'id']), + default => parent::relationQuery($name), + }; + } + + public function getProfile(): Profile|null + { + return $this->relation('profile'); + } + + /** @return Order[] */ + public function getOrders(): array + { + return $this->relation('orders'); + } +} +``` + +When you use dependency injection in the Active Record model, you need to create the Active Record instance using +the factory. + +```php +/** @var \Yiisoft\Factory\Factory $factory */ +$user = $factory->create(User::class); +``` + +To create `ActiveQuery` instance you also need to use the factory to create the Active Record model. + +```php +$userQuery = new ActiveQuery($factory->create(User::class)->withFactory($factory)); +``` + +## Factory Parameter In The Constructor + +Optionally, you can define the factory parameter in the constructor of the Active Record class. + +```php +use Yiisoft\ActiveRecord\ActiveQueryInterface; +use Yiisoft\ActiveRecord\ActiveRecord; +use Yiisoft\ActiveRecord\Trait\FactoryTrait; + +#[\AllowDynamicProperties] +final class User extends ActiveRecord +{ + use FactoryTrait; + + public function __construct(Factory $factory, private MyService $myService) + { + $this->factory = $factory; + } + + public function getMyService(): MyService + { + return $this->myService; + } +} +``` + +This will allow to create the `ActiveQuery` instance without calling `ActiveRecord::withFactory()` method. + +```php +$userQuery = new ActiveQuery($factory->create(User::class)); +``` + +Back to [Create Active Record Model](docs/create-model.md) diff --git a/src/Trait/FactoryTrait.php b/src/Trait/FactoryTrait.php new file mode 100644 index 000000000..ec2e736e4 --- /dev/null +++ b/src/Trait/FactoryTrait.php @@ -0,0 +1,59 @@ +factory = $factory; + return $new; + } + + public function instantiateQuery(string|ActiveRecordInterface|Closure $arClass): ActiveQueryInterface + { + if (!isset($this->factory)) { + return new ActiveQuery($arClass); + } + + if (is_string($arClass)) { + if (method_exists($arClass, 'withFactory')) { + return new ActiveQuery( + fn (): ActiveRecordInterface => $this->factory->create($arClass)->withFactory($this->factory) + ); + } + + return new ActiveQuery(fn (): ActiveRecordInterface => $this->factory->create($arClass)); + } + + if ($arClass instanceof ActiveRecordInterface && method_exists($arClass, 'withFactory')) { + return new ActiveQuery( + $arClass->withFactory($this->factory) + ); + } + + return new ActiveQuery($arClass); + } +} diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 54126b82a..490d7bdf3 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests; +use ArgumentCountError; use DivisionByZeroError; use ReflectionException; use Yiisoft\ActiveRecord\ActiveQuery; @@ -14,6 +15,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerClosureField; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerForArrayable; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerWithAlias; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerWithFactory; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CustomerWithCustomConnection; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Dog; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Item; @@ -22,6 +24,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Order; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItem; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItemWithNullFK; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithFactory; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\Assert; use Yiisoft\Db\Exception\Exception; @@ -30,9 +33,12 @@ use Yiisoft\Db\Exception\InvalidConfigException; use Yiisoft\Db\Exception\UnknownPropertyException; use Yiisoft\Db\Query\Query; +use Yiisoft\Factory\Factory; abstract class ActiveRecordTest extends TestCase { + abstract protected function createFactory(): Factory; + public function testStoreNull(): void { $this->checkFixture($this->db(), 'null_values', true); @@ -985,4 +991,89 @@ public function testWithCustomConnection(): void ConnectionProvider::remove('custom'); } + + public function testWithFactory(): void + { + $this->checkFixture($this->db(), 'order'); + + $factory = $this->createFactory(); + + $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); + $order = $orderQuery->with('customerWithFactory')->findOne(2); + + $this->assertInstanceOf(OrderWithFactory::class, $order); + $this->assertTrue($order->isRelationPopulated('customerWithFactory')); + $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactory()); + } + + public function testWithFactoryClosureRelation(): void + { + $this->checkFixture($this->db(), 'order'); + + $factory = $this->createFactory(); + + $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); + $order = $orderQuery->findOne(2); + + $this->assertInstanceOf(OrderWithFactory::class, $order); + $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactoryClosure()); + } + + public function testWithFactoryInstanceRelation(): void + { + $this->checkFixture($this->db(), 'order'); + + $factory = $this->createFactory(); + + $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); + $order = $orderQuery->findOne(2); + + $this->assertInstanceOf(OrderWithFactory::class, $order); + $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactoryInstance()); + } + + public function testWithFactoryLazyRelation(): void + { + $this->checkFixture($this->db(), 'order'); + + $factory = $this->createFactory(); + + $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); + $order = $orderQuery->findOne(2); + + $this->assertInstanceOf(OrderWithFactory::class, $order); + $this->assertFalse($order->isRelationPopulated('customerWithFactory')); + $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactory()); + } + + public function testWithFactoryWithConstructor(): void + { + $this->checkFixture($this->db(), 'order'); + + $factory = $this->createFactory(); + + $customerQuery = new ActiveQuery($factory->create(CustomerWithFactory::class)); + $customer = $customerQuery->findOne(2); + + $this->assertInstanceOf(CustomerWithFactory::class, $customer); + $this->assertFalse($customer->isRelationPopulated('ordersWithFactory')); + $this->assertInstanceOf(OrderWithFactory::class, $customer->getOrdersWithFactory()[0]); + } + + public function testWithFactoryNonInitiated(): void + { + $this->checkFixture($this->db(), 'order'); + + $orderQuery = new ActiveQuery(OrderWithFactory::class); + $order = $orderQuery->findOne(2); + + $customer = $order->getCustomer(); + + $this->assertInstanceOf(Customer::class, $customer); + + $this->expectException(ArgumentCountError::class); + $this->expectExceptionMessage('Too few arguments to function'); + + $customer = $order->getCustomerWithFactory(); + } } diff --git a/tests/Driver/Mssql/ActiveRecordTest.php b/tests/Driver/Mssql/ActiveRecordTest.php index 39ad1fa00..a3d738ca8 100644 --- a/tests/Driver/Mssql/ActiveRecordTest.php +++ b/tests/Driver/Mssql/ActiveRecordTest.php @@ -9,6 +9,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\TestTriggerAlert; use Yiisoft\ActiveRecord\Tests\Support\MssqlHelper; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Factory\Factory; final class ActiveRecordTest extends \Yiisoft\ActiveRecord\Tests\ActiveRecordTest { @@ -17,6 +18,11 @@ protected function createConnection(): ConnectionInterface return (new MssqlHelper())->createConnection(); } + protected function createFactory(): Factory + { + return (new MssqlHelper())->createFactory($this->db()); + } + public function testSaveWithTrigger(): void { $this->checkFixture($this->db(), 'test_trigger'); diff --git a/tests/Driver/Mysql/ActiveRecordTest.php b/tests/Driver/Mysql/ActiveRecordTest.php index c4ecfdd21..308e6ea6a 100644 --- a/tests/Driver/Mysql/ActiveRecordTest.php +++ b/tests/Driver/Mysql/ActiveRecordTest.php @@ -10,6 +10,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; use Yiisoft\ActiveRecord\Tests\Support\MysqlHelper; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Factory\Factory; final class ActiveRecordTest extends \Yiisoft\ActiveRecord\Tests\ActiveRecordTest { @@ -18,6 +19,11 @@ protected function createConnection(): ConnectionInterface return (new MysqlHelper())->createConnection(); } + protected function createFactory(): Factory + { + return (new MysqlHelper())->createFactory($this->db()); + } + public function testCastValues(): void { $this->checkFixture($this->db(), 'type'); diff --git a/tests/Driver/Oracle/ActiveRecordTest.php b/tests/Driver/Oracle/ActiveRecordTest.php index 4f82fc7ef..e8595a342 100644 --- a/tests/Driver/Oracle/ActiveRecordTest.php +++ b/tests/Driver/Oracle/ActiveRecordTest.php @@ -10,6 +10,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\OracleHelper; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Factory\Factory; final class ActiveRecordTest extends \Yiisoft\ActiveRecord\Tests\ActiveRecordTest { @@ -18,6 +19,11 @@ protected function createConnection(): ConnectionInterface return (new OracleHelper())->createConnection(); } + protected function createFactory(): Factory + { + return (new OracleHelper())->createFactory($this->db()); + } + public function testCastValues(): void { $this->markTestSkipped('Cant bind floats without support from a custom PDO driver.'); diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index df91df2c9..c6f1f37da 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -21,6 +21,7 @@ use Yiisoft\Db\Expression\Expression; use Yiisoft\Db\Expression\JsonExpression; use Yiisoft\Db\Pgsql\Schema as SchemaPgsql; +use Yiisoft\Factory\Factory; final class ActiveRecordTest extends \Yiisoft\ActiveRecord\Tests\ActiveRecordTest { @@ -29,6 +30,11 @@ protected function createConnection(): ConnectionInterface return (new PgsqlHelper())->createConnection(); } + protected function createFactory(): Factory + { + return (new PgsqlHelper())->createFactory($this->db()); + } + public function testDefaultValues(): void { $this->checkFixture($this->db(), 'type'); diff --git a/tests/Driver/Sqlite/ActiveRecordTest.php b/tests/Driver/Sqlite/ActiveRecordTest.php index f9da98db3..d47fc396a 100644 --- a/tests/Driver/Sqlite/ActiveRecordTest.php +++ b/tests/Driver/Sqlite/ActiveRecordTest.php @@ -9,6 +9,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; use Yiisoft\ActiveRecord\Tests\Support\SqliteHelper; use Yiisoft\Db\Connection\ConnectionInterface; +use Yiisoft\Factory\Factory; final class ActiveRecordTest extends \Yiisoft\ActiveRecord\Tests\ActiveRecordTest { @@ -17,6 +18,11 @@ protected function createConnection(): ConnectionInterface return (new SqliteHelper())->createConnection(); } + protected function createFactory(): Factory + { + return (new SqliteHelper())->createFactory($this->db()); + } + public function testExplicitPkOnAutoIncrement(): void { $this->checkFixture($this->db(), 'customer', true); diff --git a/tests/Stubs/ActiveRecord/CustomerWithFactory.php b/tests/Stubs/ActiveRecord/CustomerWithFactory.php new file mode 100644 index 000000000..1c2b46c86 --- /dev/null +++ b/tests/Stubs/ActiveRecord/CustomerWithFactory.php @@ -0,0 +1,34 @@ +factory = $factory; + } + + public function relationQuery(string $name): ActiveQueryInterface + { + return match ($name) { + 'ordersWithFactory' => $this->hasMany(OrderWithFactory::class, ['customer_id' => 'id']), + default => parent::relationQuery($name), + }; + } + + /** @return OrderWithFactory[] */ + public function getOrdersWithFactory(): array + { + return $this->relation('ordersWithFactory'); + } +} diff --git a/tests/Stubs/ActiveRecord/OrderWithFactory.php b/tests/Stubs/ActiveRecord/OrderWithFactory.php new file mode 100644 index 000000000..026ca94ab --- /dev/null +++ b/tests/Stubs/ActiveRecord/OrderWithFactory.php @@ -0,0 +1,42 @@ + $this->hasOne(CustomerWithFactory::class, ['id' => 'customer_id']), + 'customerWithFactoryClosure' => $this->hasOne( + fn () => $this->factory->create(CustomerWithFactory::class), ['id' => 'customer_id'] + ), + 'customerWithFactoryInstance' => $this->hasOne( + $this->factory->create(CustomerWithFactory::class), ['id' => 'customer_id'] + ), + default => parent::relationQuery($name), + }; + } + + public function getCustomerWithFactory(): CustomerWithFactory|null + { + return $this->relation('customerWithFactory'); + } + + public function getCustomerWithFactoryClosure(): CustomerWithFactory|null + { + return $this->relation('customerWithFactoryClosure'); + } + + public function getCustomerWithFactoryInstance(): CustomerWithFactory|null + { + return $this->relation('customerWithFactoryInstance'); + } +} diff --git a/tests/Support/ConnectionHelper.php b/tests/Support/ConnectionHelper.php index 2b9a07dd5..df0e89527 100644 --- a/tests/Support/ConnectionHelper.php +++ b/tests/Support/ConnectionHelper.php @@ -14,19 +14,12 @@ abstract class ConnectionHelper { - protected Factory $factory; - - public function createARFactory(ConnectionInterface $db): ActiveRecordFactory - { - return new ActiveRecordFactory($this->createFactory($db)); - } - protected function createSchemaCache(): SchemaCache { return new SchemaCache(new ArrayCache()); } - private function createFactory(ConnectionInterface $db): Factory + public function createFactory(ConnectionInterface $db): Factory { $container = new Container(ContainerConfig::create()->withDefinitions([ConnectionInterface::class => $db])); return new Factory($container, [ConnectionInterface::class => $db]);