Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Using Dependency Injection With Active Record #370

Merged
merged 3 commits into from
Jul 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading