From 8e37b1b63d47b1e24d4d8e1ed4befe59804da621 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Mon, 16 Mar 2026 16:41:28 +0700 Subject: [PATCH 1/7] Add `ActiveRecordFactory` and refactor `FactoryTrait` --- docs/traits/factory.md | 34 +------- docs/using-di.md | 67 ++++++++------- src/ActiveRecordFactory.php | 37 ++++++++ src/Trait/FactoryTrait.php | 39 ++------- tests/ActiveRecordTest.php | 86 ++++++++++++++----- tests/Driver/Mssql/ActiveRecordTest.php | 1 + tests/Driver/Mysql/ActiveRecordTest.php | 1 + tests/Driver/Oracle/ActiveRecordTest.php | 1 + tests/Driver/Pgsql/ActiveRecordTest.php | 1 + tests/Driver/Sqlite/ActiveRecordTest.php | 1 + .../ActiveRecord/CustomerWithFactory.php | 8 +- tests/Stubs/ActiveRecord/OrderWithFactory.php | 14 ++- tests/Support/ConnectionHelper.php | 2 +- tests/Support/MyService.php | 8 ++ 14 files changed, 172 insertions(+), 128 deletions(-) create mode 100644 src/ActiveRecordFactory.php create mode 100644 tests/Support/MyService.php diff --git a/docs/traits/factory.md b/docs/traits/factory.md index 7bc08ab52..54d6acdea 100644 --- a/docs/traits/factory.md +++ b/docs/traits/factory.md @@ -7,51 +7,25 @@ This is required for [Using Dependency Injection With Active Record](../using-di The following method is provided by the `FactoryTrait`: -- `withFactory()` clones the current instance of Active Record then sets the factory for the new instance and returns - the new instance. +- `instantiate()` creates a new instance of the model with initialized dependencies using [yiisoft/factory](https://github.com/yiisoft/factory). ## Usage ```php use Yiisoft\ActiveRecord\ActiveRecord; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Trait\FactoryTrait; -use Yiisoft\Factory\Factory; final class User extends ActiveRecord { use FactoryTrait; - public function __construct(Factory $factory, private MyService $myService) - { - $this->factory = $factory; - } + public function __construct(private MyService $myService) {} } -$user = $factory->create(User::class); // returns a new User instance with an initialized `Factory` and `MyService` instances. +$user = User::instantiate(); // returns a new User instance with an initialized `MyService` instance. ``` -If the `$factory` property is initialized, then the defined relations will be created using this factory. - -## Limitations - -When using `FactoryTrait`, you should not use the static `ActiveRecord::query()` method. It will not work correctly. -Instead, create a new instance of the model using the factory and create a new query object by calling the -`createQuery()` method on the model instance. - -```php -$user = $factory->create(User::class); -/** @var Yiisoft\ActiveRecord\ActiveQueryInterface $query */ -$query = $user->createQuery(); -``` - -Then you can use the active query object as usual, for example: - -```php -$users = $query->where(['is_active' => true])->all(); -``` - -Also, you cannot use `RepositoryTrait` with `FactoryTrait`, because it uses static `ActiveRecord::query()` method. - ## See also - [Using Dependency Injection With Active Record](../using-di.md); diff --git a/docs/using-di.md b/docs/using-di.md index 4e2b06c52..4d762e2b1 100644 --- a/docs/using-di.md +++ b/docs/using-di.md @@ -1,49 +1,56 @@ # Using Dependency Injection With Active Record Using [dependency injection](https://github.com/yiisoft/di) in the Active Record model allows injecting dependencies -into the model and use them in the model methods. +into the model and using 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 +## Define the Factory for Active Record + +To use dependency injection with Active Record, you need to define the factory in `ActiveRecordFactory` class using one +of the following ways: -Yii Active Record provides a [FactoryTrait](traits/factory.md) trait that allows to use the factory with the Active Record class. +### Using the bootstrap configuration + +Add the following code to the configuration file, for example, in `config/common/bootstrap.php`: ```php -use Yiisoft\ActiveRecord\ActiveQueryInterface; -use Yiisoft\ActiveRecord\ActiveRecord; -use Yiisoft\ActiveRecord\Trait\FactoryTrait; +use Psr\Container\ContainerInterface; +use Yiisoft\ActiveRecord\ActiveRecordFactory; +use Yiisoft\Factory\Factory; -#[\AllowDynamicProperties] -final class User extends ActiveRecord -{ - use FactoryTrait; - - public function __construct(private MyService $myService) - { +return [ + static function (ContainerInterface $container): void { + ActiveRecordFactory::setFactory($container->get(Factory::class)); } -} +]; ``` -When you use dependency injection in the Active Record model, you need to create the Active Record instance using -the factory. +### Using DI container autowiring -```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. +You can set the factory for Active Record using the DI container autowiring. ```php -$userQuery = new ActiveQuery($factory->create(User::class)->withFactory($factory)); +use Psr\Http\Message\ResponseInterface; +use Yiisoft\ActiveRecord\ActiveRecordFactory; +use Yiisoft\Factory\Factory; + +final class SomeController +{ + public function someAction(Factory $factory): ResponseInterface + { + ActiveRecordFactory::setFactory($factory); + + // ... + } +} ``` -## Factory Parameter In The Constructor +## Define The Active Record Model -Optionally, you can define the factory parameter in the constructor of the Active Record class. +Yii Active Record provides a [FactoryTrait](traits/factory.md) trait that allows using the factory with the Active Record class. ```php use Yiisoft\ActiveRecord\ActiveQueryInterface; @@ -55,17 +62,15 @@ final class User extends ActiveRecord { use FactoryTrait; - public function __construct(Factory $factory, private MyService $myService) - { - $this->factory = $factory; - } + public function __construct(private MyService $myService) {} } ``` -This will allow creating the `ActiveQuery` instance without calling `ActiveRecord::withFactory()` method. +Now you can create the Active Record instance or `ActiveQuery` instance using the factory. ```php -$userQuery = new ActiveQuery($factory->create(User::class)); +$user = User::instantiate(); +$userQuery = User::query(); ``` Back to [Create Active Record Model](create-model.md) diff --git a/src/ActiveRecordFactory.php b/src/ActiveRecordFactory.php new file mode 100644 index 000000000..3436448ac --- /dev/null +++ b/src/ActiveRecordFactory.php @@ -0,0 +1,37 @@ +create($className); + } + + private static function getFactory(string $className): Factory + { + return self::$factories[$className] + ?? self::$factories[self::DEFAULT] + ?? throw new InvalidArgumentException("Factory for class '$className' not found"); + } +} diff --git a/src/Trait/FactoryTrait.php b/src/Trait/FactoryTrait.php index f14845ddd..d70221212 100644 --- a/src/Trait/FactoryTrait.php +++ b/src/Trait/FactoryTrait.php @@ -4,46 +4,21 @@ namespace Yiisoft\ActiveRecord\Trait; -use Yiisoft\ActiveRecord\ActiveQueryInterface; -use Yiisoft\ActiveRecord\ActiveRecordInterface; -use Yiisoft\Factory\Factory; - -use function is_string; -use function method_exists; +use Yiisoft\ActiveRecord\ActiveRecordFactory; /** - * Trait to add factory support to ActiveRecord. + * Trait to add factory support to an active record model by using {@see ActiveRecordFactory} class. * - * @see AbstractActiveRecord::createQuery() + * @see AbstractActiveRecord::instantiate() */ trait FactoryTrait { - private Factory $factory; - /** - * Set the factory to use for creating new instances. + * Creates a new instance of the active record class. */ - public function withFactory(Factory $factory): static - { - $new = clone $this; - $new->factory = $factory; - return $new; - } - - public function createQuery(ActiveRecordInterface|string|null $modelClass = null): ActiveQueryInterface + public static function instantiate(): static { - if (!isset($this->factory)) { - return parent::createQuery($modelClass); - } - - $model = is_string($modelClass) - ? $this->factory->create($modelClass) - : $modelClass ?? $this; - - if (method_exists($model, 'withFactory')) { - return parent::createQuery($model->withFactory($this->factory)); - } - - return parent::createQuery($model); + /** @var static */ + return ActiveRecordFactory::create(static::class); } } diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index bc4e9aab6..5a6bbb328 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -9,8 +9,10 @@ use InvalidArgumentException; use LogicException; use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\Attributes\Depends; use PHPUnit\Framework\Attributes\TestWith; use Yiisoft\ActiveRecord\ActiveQuery; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Event\AfterDelete; use Yiisoft\ActiveRecord\Event\EventDispatcherProvider; use Yiisoft\ActiveRecord\Internal\ArArrayHelper; @@ -45,6 +47,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\UuidPromotion; use Yiisoft\ActiveRecord\Tests\Support\DbHelper; use Yiisoft\ActiveRecord\Tests\Support\ModelFactory; +use Yiisoft\ActiveRecord\Tests\Support\MyService; use Yiisoft\ActiveRecord\UnknownPropertyException; use Yiisoft\Db\Connection\ConnectionProvider; use Yiisoft\Db\Exception\Exception; @@ -1004,64 +1007,98 @@ public function testWithCustomConnection(): void ConnectionProvider::remove('custom'); } - public function testWithFactory(): void + public function testWithNotInitializedFactory(): void { - $factory = $this->createFactory(); + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Factory for class '" . OrderWithFactory::class . "' not found"); - $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); - $order = $orderQuery->with('customerWithFactory')->findByPk(2); + OrderWithFactory::query()->with('customerWithFactory')->findByPk(2); + } + + #[Depends('testWithNotInitializedFactory')] + public function testWithFactoryInstantiate(): void + { + $this->initFactory(); + + $order = OrderWithFactory::instantiate(); $this->assertInstanceOf(OrderWithFactory::class, $order); - $this->assertTrue($order->isRelationPopulated('customerWithFactory')); - $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactory()); + $this->assertInstanceOf(MyService::class, $order->service); } - public function testWithFactoryInstanceRelation(): void + #[Depends('testWithNotInitializedFactory')] + public function testWithFactoryQuery(): void { - $factory = $this->createFactory(); + $this->initFactory(); - $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); - $order = $orderQuery->findByPk(2); + $order = OrderWithFactory::query()->with('customerWithFactory')->findByPk(2); $this->assertInstanceOf(OrderWithFactory::class, $order); - $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactoryInstance()); + $this->assertInstanceOf(MyService::class, $order->service); + $this->assertTrue($order->isRelationPopulated('customerWithFactory')); + $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactory()); } + #[Depends('testWithNotInitializedFactory')] public function testWithFactoryRelationWithoutFactory(): void { - $factory = $this->createFactory(); + $this->initFactory(); - $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); - $order = $orderQuery->findByPk(2); + $order = OrderWithFactory::query()->findByPk(2); $this->assertInstanceOf(OrderWithFactory::class, $order); $this->assertInstanceOf(Customer::class, $order->getCustomer()); } + #[Depends('testWithNotInitializedFactory')] public function testWithFactoryLazyRelation(): void { - $factory = $this->createFactory(); + $this->initFactory(); - $orderQuery = new ActiveQuery($factory->create(OrderWithFactory::class)->withFactory($factory)); - $order = $orderQuery->findByPk(2); + $order = OrderWithFactory::query()->findByPk(2); $this->assertInstanceOf(OrderWithFactory::class, $order); $this->assertFalse($order->isRelationPopulated('customerWithFactory')); $this->assertInstanceOf(CustomerWithFactory::class, $order->getCustomerWithFactory()); } - public function testWithFactoryWithConstructor(): void + #[Depends('testWithNotInitializedFactory')] + public function testWithFactoryRelationWithConstructor(): void { - $factory = $this->createFactory(); + $this->initFactory(); - $customerQuery = new ActiveQuery($factory->create(CustomerWithFactory::class)); - $customer = $customerQuery->findByPk(2); + $customer = CustomerWithFactory::query()->findByPk(2); $this->assertInstanceOf(CustomerWithFactory::class, $customer); $this->assertFalse($customer->isRelationPopulated('ordersWithFactory')); $this->assertInstanceOf(OrderWithFactory::class, $customer->getOrdersWithFactory()[0]); } + #[Depends('testWithNotInitializedFactory')] + public function testWithConcreteFactory(): void + { + $factory = $this->createFactory(); + ActiveRecordFactory::setFactory($factory); + + $newFactory = $this->createFactory()->withDefinitions([ + MyService::class => [ + '__construct()' => ['name' => 'new factory'], + ], + ]); + + ActiveRecordFactory::setFactory($newFactory, CustomerWithFactory::class); + + $order = OrderWithFactory::instantiate(); + + $this->assertInstanceOf(MyService::class, $order->service); + $this->assertSame('default', $order->service->name); + + $customer = CustomerWithFactory::instantiate(); + + $this->assertInstanceOf(MyService::class, $customer->service); + $this->assertSame('new factory', $customer->service->name); + } + public function testWithFactoryNonInitiated(): void { $orderQuery = OrderWithFactory::query(); @@ -1986,5 +2023,12 @@ public function testWithConstructorNewInstance(): void $this->assertNotNull($newOrder->getDeletedAt()); } + protected function initFactory(): void + { + $factory = $this->createFactory(); + + ActiveRecordFactory::setFactory($factory); + } + abstract protected function createFactory(): Factory; } diff --git a/tests/Driver/Mssql/ActiveRecordTest.php b/tests/Driver/Mssql/ActiveRecordTest.php index 8666e2c3b..35188c9f0 100644 --- a/tests/Driver/Mssql/ActiveRecordTest.php +++ b/tests/Driver/Mssql/ActiveRecordTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Mssql; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\TestTrigger; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\TestTriggerAlert; use Yiisoft\ActiveRecord\Tests\Support\MssqlHelper; diff --git a/tests/Driver/Mysql/ActiveRecordTest.php b/tests/Driver/Mysql/ActiveRecordTest.php index 92ff30150..94c184ccc 100644 --- a/tests/Driver/Mysql/ActiveRecordTest.php +++ b/tests/Driver/Mysql/ActiveRecordTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Mysql; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Driver\Mysql\Stubs\Type; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Beta; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; diff --git a/tests/Driver/Oracle/ActiveRecordTest.php b/tests/Driver/Oracle/ActiveRecordTest.php index 71034c368..7e46a3506 100644 --- a/tests/Driver/Oracle/ActiveRecordTest.php +++ b/tests/Driver/Oracle/ActiveRecordTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Oracle; use PHPUnit\Framework\Attributes\TestWith; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Driver\Oracle\Stubs\Customer; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\OracleHelper; diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index 0857c6103..4b06e725e 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -5,6 +5,7 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Pgsql; use PHPUnit\Framework\Attributes\DataProvider; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Internal\ArArrayHelper; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Item; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Promotion; diff --git a/tests/Driver/Sqlite/ActiveRecordTest.php b/tests/Driver/Sqlite/ActiveRecordTest.php index 46a6b01fa..610cbc6c9 100644 --- a/tests/Driver/Sqlite/ActiveRecordTest.php +++ b/tests/Driver/Sqlite/ActiveRecordTest.php @@ -4,6 +4,7 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Sqlite; +use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Beta; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; use Yiisoft\ActiveRecord\Tests\Support\SqliteHelper; diff --git a/tests/Stubs/ActiveRecord/CustomerWithFactory.php b/tests/Stubs/ActiveRecord/CustomerWithFactory.php index 97ff2f9c0..3b3864704 100644 --- a/tests/Stubs/ActiveRecord/CustomerWithFactory.php +++ b/tests/Stubs/ActiveRecord/CustomerWithFactory.php @@ -5,6 +5,7 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord; use Yiisoft\ActiveRecord\ActiveQueryInterface; +use Yiisoft\ActiveRecord\Tests\Support\MyService; use Yiisoft\ActiveRecord\Trait\FactoryTrait; use Yiisoft\Factory\Factory; @@ -12,10 +13,9 @@ final class CustomerWithFactory extends Customer { use FactoryTrait; - public function __construct(Factory $factory) - { - $this->factory = $factory; - } + public function __construct( + public readonly MyService $service, + ) {} public function relationQuery(string $name): ActiveQueryInterface { diff --git a/tests/Stubs/ActiveRecord/OrderWithFactory.php b/tests/Stubs/ActiveRecord/OrderWithFactory.php index 40224aa09..23b9f5add 100644 --- a/tests/Stubs/ActiveRecord/OrderWithFactory.php +++ b/tests/Stubs/ActiveRecord/OrderWithFactory.php @@ -5,20 +5,21 @@ namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord; use Yiisoft\ActiveRecord\ActiveQueryInterface; +use Yiisoft\ActiveRecord\Tests\Support\MyService; use Yiisoft\ActiveRecord\Trait\FactoryTrait; final class OrderWithFactory extends Order { use FactoryTrait; + public function __construct( + public readonly MyService $service, + ) {} + public function relationQuery(string $name): ActiveQueryInterface { return match ($name) { 'customerWithFactory' => $this->hasOne(CustomerWithFactory::class, ['id' => 'customer_id']), - 'customerWithFactoryInstance' => $this->hasOne( - $this->factory->create(CustomerWithFactory::class), - ['id' => 'customer_id'], - ), default => parent::relationQuery($name), }; } @@ -27,9 +28,4 @@ public function getCustomerWithFactory(): ?CustomerWithFactory { return $this->relation('customerWithFactory'); } - - public function getCustomerWithFactoryInstance(): ?CustomerWithFactory - { - return $this->relation('customerWithFactoryInstance'); - } } diff --git a/tests/Support/ConnectionHelper.php b/tests/Support/ConnectionHelper.php index 0307de6a1..2ba0cda01 100644 --- a/tests/Support/ConnectionHelper.php +++ b/tests/Support/ConnectionHelper.php @@ -16,7 +16,7 @@ abstract class ConnectionHelper public function createFactory(ConnectionInterface $db): Factory { $container = new Container(ContainerConfig::create()->withDefinitions([ConnectionInterface::class => $db])); - return new Factory($container, [ConnectionInterface::class => $db]); + return new Factory($container); } protected function createSchemaCache(): SchemaCache diff --git a/tests/Support/MyService.php b/tests/Support/MyService.php new file mode 100644 index 000000000..d5f100efa --- /dev/null +++ b/tests/Support/MyService.php @@ -0,0 +1,8 @@ + Date: Wed, 18 Mar 2026 10:33:12 +0700 Subject: [PATCH 2/7] Improve --- docs/using-di.md | 8 +- src/ActiveRecordFactory.php | 82 +++++++- tests/ActiveRecordFactoryTest.php | 187 ++++++++++++++++++ tests/ActiveRecordTest.php | 8 +- tests/Driver/Mssql/ActiveRecordTest.php | 1 - tests/Driver/Mysql/ActiveRecordTest.php | 1 - tests/Driver/Oracle/ActiveRecordTest.php | 1 - tests/Driver/Pgsql/ActiveRecordTest.php | 1 - tests/Driver/Sqlite/ActiveRecordTest.php | 1 - .../ActiveRecord/CustomerWithFactory.php | 1 - tests/Support/MyService.php | 2 +- 11 files changed, 271 insertions(+), 22 deletions(-) create mode 100644 tests/ActiveRecordFactoryTest.php diff --git a/docs/using-di.md b/docs/using-di.md index 4d762e2b1..0114238ce 100644 --- a/docs/using-di.md +++ b/docs/using-di.md @@ -23,11 +23,15 @@ use Yiisoft\Factory\Factory; return [ static function (ContainerInterface $container): void { - ActiveRecordFactory::setFactory($container->get(Factory::class)); + ActiveRecordFactory::set(new Factory($container)); + // or `ActiveRecordFactory::set($container->get(Factory::class))` if the factory is defined in the container. } ]; ``` +> [!NOTE] +> The factory can be represented as `Factory` or `StrictFactory` class instance. + ### Using DI container autowiring You can set the factory for Active Record using the DI container autowiring. @@ -41,7 +45,7 @@ final class SomeController { public function someAction(Factory $factory): ResponseInterface { - ActiveRecordFactory::setFactory($factory); + ActiveRecordFactory::set($factory); // ... } diff --git a/src/ActiveRecordFactory.php b/src/ActiveRecordFactory.php index 3436448ac..643d66385 100644 --- a/src/ActiveRecordFactory.php +++ b/src/ActiveRecordFactory.php @@ -4,8 +4,9 @@ namespace Yiisoft\ActiveRecord; -use Yiisoft\Factory\Factory; use InvalidArgumentException; +use Yiisoft\Factory\Factory; +use Yiisoft\Factory\StrictFactory; /** * ActiveRecordFactory is a factory class for creating active record instances. @@ -15,23 +16,86 @@ final class ActiveRecordFactory { private const DEFAULT = ''; - /** @var Factory[] $factories */ + /** @var (Factory|StrictFactory)[] $factories */ private static array $factories = []; - public static function setFactory(Factory $factory, string $className = self::DEFAULT): void + /** + * Returns all factories. If the default factory is set, it will be returned with an empty string key. + * + * @return (Factory|StrictFactory)[] + */ + public static function all(): array + { + return self::$factories; + } + + /** + * Clear all registered factories. + */ + public static function clear(): void { - self::$factories[$className] = $factory; + self::$factories = []; } + /** + * Creates an active record instance. + * + * @param class-string $className The class name of the active record to be created. + */ public static function create(string $className): ActiveRecordInterface { - return self::getFactory($className)->create($className); + return self::get($className)->create($className); + } + + /** + * Returns the factory for the given class name or the default factory if none is found. + * + * @param ?class-string $className The class name of the active record to be checked + * or `null` to get the default factory. + */ + public static function get(?string $className = null): Factory|StrictFactory + { + if ($className !== null) { + return self::$factories[$className] + ?? self::$factories[self::DEFAULT] + ?? throw new InvalidArgumentException("Factory for class '$className' not found"); + } + + return self::$factories[self::DEFAULT] + ?? throw new InvalidArgumentException("Default factory not found"); + } + + /** + * Checks if a factory for the given class name exists. + * + * @param ?class-string $className The class name of the active record to be checked + * or `null` to check default factory. + */ + public static function has(?string $className = null): bool + { + return isset(self::$factories[$className ?? self::DEFAULT]); + } + + /** + * Removes a factory by name. + * + * @param ?class-string $className The class name of the active record to be removed + * or `null` to remove default factory. + */ + public static function remove(?string $className = null): void + { + unset(self::$factories[$className ?? self::DEFAULT]); } - private static function getFactory(string $className): Factory + /** + * Sets a factory by name. + * + * @param Factory|StrictFactory $factory The factory to be set. + * @param ?class-string $className The class name of the active record to be set + * or `null` to set default factory. + */ + public static function set(Factory|StrictFactory $factory, ?string $className = null): void { - return self::$factories[$className] - ?? self::$factories[self::DEFAULT] - ?? throw new InvalidArgumentException("Factory for class '$className' not found"); + self::$factories[$className ?? self::DEFAULT] = $factory; } } diff --git a/tests/ActiveRecordFactoryTest.php b/tests/ActiveRecordFactoryTest.php new file mode 100644 index 000000000..46020499e --- /dev/null +++ b/tests/ActiveRecordFactoryTest.php @@ -0,0 +1,187 @@ +assertSame($factory, ActiveRecordFactory::get()); + } + + public function testSetAndGetWithClassName(): void + { + $factory = new Factory(); + ActiveRecordFactory::set($factory); + + $newFactory = new Factory(); + $className = Order::class; + ActiveRecordFactory::set($newFactory, $className); + + $this->assertNotSame($factory, $newFactory); + $this->assertSame($newFactory, ActiveRecordFactory::get($className)); + } + + public function testGetFallbackToDefault(): void + { + $factory = new Factory(); + ActiveRecordFactory::set($factory); + $className = Order::class; + + $this->assertSame($factory, ActiveRecordFactory::get($className)); + } + + public function testGetThrowsExceptionWhenNotFound(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Default factory not found"); + ActiveRecordFactory::get(); + } + + public function testGetWithClassNameThrowsExceptionWhenNotFound(): void + { + $className = Order::class; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Factory for class '$className' not found"); + + ActiveRecordFactory::get($className); + } + + public function testHas(): void + { + $this->assertFalse(ActiveRecordFactory::has()); + + ActiveRecordFactory::set(new Factory()); + + $this->assertTrue(ActiveRecordFactory::has()); + + $className = Order::class; + + $this->assertFalse(ActiveRecordFactory::has($className)); + + ActiveRecordFactory::set(new Factory(), $className); + + $this->assertTrue(ActiveRecordFactory::has($className)); + } + + public function testRemove(): void + { + $className = Order::class; + + ActiveRecordFactory::set(new Factory()); + ActiveRecordFactory::set(new Factory(), $className); + + ActiveRecordFactory::remove($className); + + $this->assertFalse(ActiveRecordFactory::has($className)); + $this->assertTrue(ActiveRecordFactory::has()); + + ActiveRecordFactory::remove(); + + $this->assertFalse(ActiveRecordFactory::has()); + } + + public function testClear(): void + { + $className = Order::class; + + ActiveRecordFactory::set(new Factory()); + ActiveRecordFactory::set(new Factory(), $className); + + ActiveRecordFactory::clear(); + + $this->assertFalse(ActiveRecordFactory::has()); + $this->assertFalse(ActiveRecordFactory::has($className)); + $this->assertEmpty(ActiveRecordFactory::all()); + } + + public function testAll(): void + { + $factory1 = new Factory(); + $factory2 = new Factory(); + $className = Order::class; + + ActiveRecordFactory::set($factory1); + ActiveRecordFactory::set($factory2, $className); + + $all = ActiveRecordFactory::all(); + $this->assertCount(2, $all); + $this->assertSame($factory1, $all['']); + $this->assertSame($factory2, $all[$className]); + } + + public function testCreate(): void + { + $className = OrderWithFactory::class; + + $factory = new Factory(null, [ + MyService::class => [ + '__construct()' => ['custom'], + ], + ]); + + ActiveRecordFactory::set($factory); + + $order = ActiveRecordFactory::create($className); + $this->assertInstanceOf($className, $order); + $this->assertSame('custom', $order->service->name); + } + + public function testSetAndGetWithStrictFactory(): void + { + $factory = new StrictFactory([]); + ActiveRecordFactory::set($factory); + + $this->assertSame($factory, ActiveRecordFactory::get()); + } + + public function testCreateWithStrictFactory(): void + { + $className = OrderWithFactory::class; + + $factory = new StrictFactory([ + MyService::class => [ + '__construct()' => ['custom'], + ], + ]); + ActiveRecordFactory::set($factory); + + $order = ActiveRecordFactory::create($className); + $this->assertInstanceOf($className, $order); + $this->assertSame('strict', $order->service->name); + } + + public function testCreateWithStrictFactoryThrowsExceptionWhenNotFound(): void + { + $className = OrderWithFactory::class; + $factory = new StrictFactory([]); + ActiveRecordFactory::set($factory); + + $this->expectException(NotFoundException::class); + $this->expectExceptionMessage("No definition or class found or resolvable for $className."); + + ActiveRecordFactory::create($className); + } +} diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index 5a6bbb328..d8905d7ad 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -1077,8 +1077,7 @@ public function testWithFactoryRelationWithConstructor(): void #[Depends('testWithNotInitializedFactory')] public function testWithConcreteFactory(): void { - $factory = $this->createFactory(); - ActiveRecordFactory::setFactory($factory); + $this->initFactory(); $newFactory = $this->createFactory()->withDefinitions([ MyService::class => [ @@ -1086,7 +1085,7 @@ public function testWithConcreteFactory(): void ], ]); - ActiveRecordFactory::setFactory($newFactory, CustomerWithFactory::class); + ActiveRecordFactory::set($newFactory, CustomerWithFactory::class); $order = OrderWithFactory::instantiate(); @@ -2027,7 +2026,8 @@ protected function initFactory(): void { $factory = $this->createFactory(); - ActiveRecordFactory::setFactory($factory); + ActiveRecordFactory::clear(); + ActiveRecordFactory::set($factory); } abstract protected function createFactory(): Factory; diff --git a/tests/Driver/Mssql/ActiveRecordTest.php b/tests/Driver/Mssql/ActiveRecordTest.php index 35188c9f0..8666e2c3b 100644 --- a/tests/Driver/Mssql/ActiveRecordTest.php +++ b/tests/Driver/Mssql/ActiveRecordTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Mssql; -use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\TestTrigger; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\TestTriggerAlert; use Yiisoft\ActiveRecord\Tests\Support\MssqlHelper; diff --git a/tests/Driver/Mysql/ActiveRecordTest.php b/tests/Driver/Mysql/ActiveRecordTest.php index 94c184ccc..92ff30150 100644 --- a/tests/Driver/Mysql/ActiveRecordTest.php +++ b/tests/Driver/Mysql/ActiveRecordTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Mysql; -use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Driver\Mysql\Stubs\Type; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Beta; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; diff --git a/tests/Driver/Oracle/ActiveRecordTest.php b/tests/Driver/Oracle/ActiveRecordTest.php index 7e46a3506..71034c368 100644 --- a/tests/Driver/Oracle/ActiveRecordTest.php +++ b/tests/Driver/Oracle/ActiveRecordTest.php @@ -5,7 +5,6 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Oracle; use PHPUnit\Framework\Attributes\TestWith; -use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Driver\Oracle\Stubs\Customer; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Type; use Yiisoft\ActiveRecord\Tests\Support\OracleHelper; diff --git a/tests/Driver/Pgsql/ActiveRecordTest.php b/tests/Driver/Pgsql/ActiveRecordTest.php index 4b06e725e..0857c6103 100644 --- a/tests/Driver/Pgsql/ActiveRecordTest.php +++ b/tests/Driver/Pgsql/ActiveRecordTest.php @@ -5,7 +5,6 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Pgsql; use PHPUnit\Framework\Attributes\DataProvider; -use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Internal\ArArrayHelper; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Item; use Yiisoft\ActiveRecord\Tests\Driver\Pgsql\Stubs\Promotion; diff --git a/tests/Driver/Sqlite/ActiveRecordTest.php b/tests/Driver/Sqlite/ActiveRecordTest.php index 610cbc6c9..46a6b01fa 100644 --- a/tests/Driver/Sqlite/ActiveRecordTest.php +++ b/tests/Driver/Sqlite/ActiveRecordTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\ActiveRecord\Tests\Driver\Sqlite; -use Yiisoft\ActiveRecord\ActiveRecordFactory; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Beta; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer; use Yiisoft\ActiveRecord\Tests\Support\SqliteHelper; diff --git a/tests/Stubs/ActiveRecord/CustomerWithFactory.php b/tests/Stubs/ActiveRecord/CustomerWithFactory.php index 3b3864704..9d30f7206 100644 --- a/tests/Stubs/ActiveRecord/CustomerWithFactory.php +++ b/tests/Stubs/ActiveRecord/CustomerWithFactory.php @@ -7,7 +7,6 @@ use Yiisoft\ActiveRecord\ActiveQueryInterface; use Yiisoft\ActiveRecord\Tests\Support\MyService; use Yiisoft\ActiveRecord\Trait\FactoryTrait; -use Yiisoft\Factory\Factory; final class CustomerWithFactory extends Customer { diff --git a/tests/Support/MyService.php b/tests/Support/MyService.php index d5f100efa..10c60e25e 100644 --- a/tests/Support/MyService.php +++ b/tests/Support/MyService.php @@ -4,5 +4,5 @@ final class MyService { - public function __construct(public string $name = 'default') {} + public function __construct(public readonly string $name = 'default') {} } From 3179d68462f3b0523a05f136653e25cccf6905c1 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 18 Mar 2026 10:42:28 +0700 Subject: [PATCH 3/7] Fix psalm --- src/ActiveRecordFactory.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ActiveRecordFactory.php b/src/ActiveRecordFactory.php index 643d66385..7f2918c78 100644 --- a/src/ActiveRecordFactory.php +++ b/src/ActiveRecordFactory.php @@ -44,6 +44,7 @@ public static function clear(): void */ public static function create(string $className): ActiveRecordInterface { + /** @var ActiveRecordInterface */ return self::get($className)->create($className); } From 75697e7f6eb7b801c3ef80c31facaf2a24a5b165 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Wed, 18 Mar 2026 14:35:01 +0700 Subject: [PATCH 4/7] Update tests --- tests/ActiveRecordFactoryTest.php | 5 +++-- tests/Driver/Mssql/ActiveRecordFactoryTest.php | 9 +++++++++ tests/Driver/Mysql/ActiveRecordFactoryTest.php | 9 +++++++++ tests/Driver/Oracle/ActiveRecordFactoryTest.php | 9 +++++++++ tests/Driver/Pgsql/ActiveRecordFactoryTest.php | 9 +++++++++ tests/Driver/Sqlite/ActiveRecordFactoryTest.php | 9 +++++++++ 6 files changed, 48 insertions(+), 2 deletions(-) create mode 100644 tests/Driver/Mssql/ActiveRecordFactoryTest.php create mode 100644 tests/Driver/Mysql/ActiveRecordFactoryTest.php create mode 100644 tests/Driver/Oracle/ActiveRecordFactoryTest.php create mode 100644 tests/Driver/Pgsql/ActiveRecordFactoryTest.php create mode 100644 tests/Driver/Sqlite/ActiveRecordFactoryTest.php diff --git a/tests/ActiveRecordFactoryTest.php b/tests/ActiveRecordFactoryTest.php index 46020499e..7a341c2de 100644 --- a/tests/ActiveRecordFactoryTest.php +++ b/tests/ActiveRecordFactoryTest.php @@ -14,7 +14,7 @@ use Yiisoft\Factory\NotFoundException; use Yiisoft\Factory\StrictFactory; -final class ActiveRecordFactoryTest extends TestCase +abstract class ActiveRecordFactoryTest extends TestCase { protected function tearDown(): void { @@ -162,8 +162,9 @@ public function testCreateWithStrictFactory(): void $className = OrderWithFactory::class; $factory = new StrictFactory([ + $className => [], MyService::class => [ - '__construct()' => ['custom'], + '__construct()' => ['strict'], ], ]); ActiveRecordFactory::set($factory); diff --git a/tests/Driver/Mssql/ActiveRecordFactoryTest.php b/tests/Driver/Mssql/ActiveRecordFactoryTest.php new file mode 100644 index 000000000..978120f0a --- /dev/null +++ b/tests/Driver/Mssql/ActiveRecordFactoryTest.php @@ -0,0 +1,9 @@ + Date: Wed, 18 Mar 2026 14:37:53 +0700 Subject: [PATCH 5/7] Update tests --- tests/Support/MyService.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Support/MyService.php b/tests/Support/MyService.php index 10c60e25e..cb9a0be8d 100644 --- a/tests/Support/MyService.php +++ b/tests/Support/MyService.php @@ -1,5 +1,7 @@ Date: Thu, 19 Mar 2026 12:24:45 +0700 Subject: [PATCH 6/7] Simplify `ActiveRecordFactory` --- src/ActiveRecordFactory.php | 52 +++++------------- tests/ActiveRecordFactoryTest.php | 88 +++++++------------------------ 2 files changed, 33 insertions(+), 107 deletions(-) diff --git a/src/ActiveRecordFactory.php b/src/ActiveRecordFactory.php index 7f2918c78..86fa7cffe 100644 --- a/src/ActiveRecordFactory.php +++ b/src/ActiveRecordFactory.php @@ -19,16 +19,6 @@ final class ActiveRecordFactory /** @var (Factory|StrictFactory)[] $factories */ private static array $factories = []; - /** - * Returns all factories. If the default factory is set, it will be returned with an empty string key. - * - * @return (Factory|StrictFactory)[] - */ - public static function all(): array - { - return self::$factories; - } - /** * Clear all registered factories. */ @@ -48,24 +38,6 @@ public static function create(string $className): ActiveRecordInterface return self::get($className)->create($className); } - /** - * Returns the factory for the given class name or the default factory if none is found. - * - * @param ?class-string $className The class name of the active record to be checked - * or `null` to get the default factory. - */ - public static function get(?string $className = null): Factory|StrictFactory - { - if ($className !== null) { - return self::$factories[$className] - ?? self::$factories[self::DEFAULT] - ?? throw new InvalidArgumentException("Factory for class '$className' not found"); - } - - return self::$factories[self::DEFAULT] - ?? throw new InvalidArgumentException("Default factory not found"); - } - /** * Checks if a factory for the given class name exists. * @@ -77,17 +49,6 @@ public static function has(?string $className = null): bool return isset(self::$factories[$className ?? self::DEFAULT]); } - /** - * Removes a factory by name. - * - * @param ?class-string $className The class name of the active record to be removed - * or `null` to remove default factory. - */ - public static function remove(?string $className = null): void - { - unset(self::$factories[$className ?? self::DEFAULT]); - } - /** * Sets a factory by name. * @@ -99,4 +60,17 @@ public static function set(Factory|StrictFactory $factory, ?string $className = { self::$factories[$className ?? self::DEFAULT] = $factory; } + + /** + * Returns the factory for the given class name or the default factory if none is found. + * + * @param class-string $className The class name of the active record to be checked + * or `null` to get the default factory. + */ + private static function get(string $className): Factory|StrictFactory + { + return self::$factories[$className] + ?? self::$factories[self::DEFAULT] + ?? throw new InvalidArgumentException("Factory for class '$className' not found"); + } } diff --git a/tests/ActiveRecordFactoryTest.php b/tests/ActiveRecordFactoryTest.php index 7a341c2de..599044153 100644 --- a/tests/ActiveRecordFactoryTest.php +++ b/tests/ActiveRecordFactoryTest.php @@ -22,51 +22,24 @@ protected function tearDown(): void parent::tearDown(); } - public function testSetAndGet(): void + public function testSet(): void { - $factory = new Factory(); - ActiveRecordFactory::set($factory); - - $this->assertSame($factory, ActiveRecordFactory::get()); - } - - public function testSetAndGetWithClassName(): void - { - $factory = new Factory(); - ActiveRecordFactory::set($factory); + $this->assertFalse(ActiveRecordFactory::has()); - $newFactory = new Factory(); - $className = Order::class; - ActiveRecordFactory::set($newFactory, $className); + ActiveRecordFactory::set(new Factory()); - $this->assertNotSame($factory, $newFactory); - $this->assertSame($newFactory, ActiveRecordFactory::get($className)); + $this->assertTrue(ActiveRecordFactory::has()); } - public function testGetFallbackToDefault(): void + public function testSetWithClassName(): void { - $factory = new Factory(); - ActiveRecordFactory::set($factory); $className = Order::class; - $this->assertSame($factory, ActiveRecordFactory::get($className)); - } - - public function testGetThrowsExceptionWhenNotFound(): void - { - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Default factory not found"); - ActiveRecordFactory::get(); - } - - public function testGetWithClassNameThrowsExceptionWhenNotFound(): void - { - $className = Order::class; + $this->assertFalse(ActiveRecordFactory::has($className)); - $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage("Factory for class '$className' not found"); + ActiveRecordFactory::set(new Factory(), $className); - ActiveRecordFactory::get($className); + $this->assertTrue(ActiveRecordFactory::has($className)); } public function testHas(): void @@ -86,22 +59,6 @@ public function testHas(): void $this->assertTrue(ActiveRecordFactory::has($className)); } - public function testRemove(): void - { - $className = Order::class; - - ActiveRecordFactory::set(new Factory()); - ActiveRecordFactory::set(new Factory(), $className); - - ActiveRecordFactory::remove($className); - - $this->assertFalse(ActiveRecordFactory::has($className)); - $this->assertTrue(ActiveRecordFactory::has()); - - ActiveRecordFactory::remove(); - - $this->assertFalse(ActiveRecordFactory::has()); - } public function testClear(): void { @@ -114,23 +71,8 @@ public function testClear(): void $this->assertFalse(ActiveRecordFactory::has()); $this->assertFalse(ActiveRecordFactory::has($className)); - $this->assertEmpty(ActiveRecordFactory::all()); } - public function testAll(): void - { - $factory1 = new Factory(); - $factory2 = new Factory(); - $className = Order::class; - - ActiveRecordFactory::set($factory1); - ActiveRecordFactory::set($factory2, $className); - - $all = ActiveRecordFactory::all(); - $this->assertCount(2, $all); - $this->assertSame($factory1, $all['']); - $this->assertSame($factory2, $all[$className]); - } public function testCreate(): void { @@ -149,12 +91,22 @@ public function testCreate(): void $this->assertSame('custom', $order->service->name); } - public function testSetAndGetWithStrictFactory(): void + public function testCreateWithClassNameThrowsExceptionWhenNotFound(): void + { + $className = Order::class; + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage("Factory for class '$className' not found"); + + ActiveRecordFactory::create(Order::class); + } + + public function testSetWithStrictFactory(): void { $factory = new StrictFactory([]); ActiveRecordFactory::set($factory); - $this->assertSame($factory, ActiveRecordFactory::get()); + $this->assertTrue(ActiveRecordFactory::has()); } public function testCreateWithStrictFactory(): void From 3cc2213f72f233c8b0b7344577e9eaafab0e6271 Mon Sep 17 00:00:00 2001 From: Sergei Tigrov Date: Thu, 19 Mar 2026 15:11:02 +0700 Subject: [PATCH 7/7] Apply from suggestions [skip ci] Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/ActiveRecordFactory.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ActiveRecordFactory.php b/src/ActiveRecordFactory.php index 86fa7cffe..3c884a18b 100644 --- a/src/ActiveRecordFactory.php +++ b/src/ActiveRecordFactory.php @@ -64,8 +64,7 @@ public static function set(Factory|StrictFactory $factory, ?string $className = /** * Returns the factory for the given class name or the default factory if none is found. * - * @param class-string $className The class name of the active record to be checked - * or `null` to get the default factory. + * @param class-string $className The class name of the active record to be checked. */ private static function get(string $className): Factory|StrictFactory {