Skip to content

Commit

Permalink
Using Dependency Injection With Active Record (#370)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored Jul 4, 2024
1 parent cd71945 commit e9708de
Show file tree
Hide file tree
Showing 15 changed files with 378 additions and 11 deletions.
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
3 changes: 2 additions & 1 deletion composer-require-checker.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
]
}
2 changes: 2 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand All @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions docs/create-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
96 changes: 96 additions & 0 deletions docs/using-di.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# 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;
}
}
```

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)
59 changes: 59 additions & 0 deletions src/Trait/FactoryTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

declare(strict_types=1);

namespace Yiisoft\ActiveRecord\Trait;

use Closure;
use Yiisoft\ActiveRecord\ActiveQuery;
use Yiisoft\ActiveRecord\ActiveQueryInterface;
use Yiisoft\ActiveRecord\ActiveRecordInterface;
use Yiisoft\Factory\Factory;

use function is_string;
use function method_exists;

/**
* Trait to add factory support to ActiveRecord.
*
* @see AbstractActiveRecord::instantiateQuery()
*/
trait FactoryTrait
{
private Factory $factory;

/**
* Set the factory to use for creating new instances.
*/
public function withFactory(Factory $factory): static
{
$new = clone $this;
$new->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);
}
}
104 changes: 104 additions & 0 deletions tests/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Yiisoft\ActiveRecord\Tests;

use ArgumentCountError;
use DivisionByZeroError;
use ReflectionException;
use Yiisoft\ActiveRecord\ActiveQuery;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -985,4 +991,102 @@ 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 testWithFactoryRelationWithoutFactory(): 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(Customer::class, $order->getCustomer());
}

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();
}
}
6 changes: 6 additions & 0 deletions tests/Driver/Mssql/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions tests/Driver/Mysql/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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');
Expand Down
6 changes: 6 additions & 0 deletions tests/Driver/Oracle/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand All @@ -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.');
Expand Down
Loading

0 comments on commit e9708de

Please sign in to comment.