From f09ccde48d5e3f9efcc6433cd27c075cd3e2cbc2 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Mon, 16 Mar 2026 16:12:27 +0700 Subject: [PATCH 1/4] Add `ActiveRecord::instantiate()` method --- docs/create-model.md | 29 ++++++----------------------- src/AbstractActiveRecord.php | 5 +++++ src/ActiveQuery.php | 2 +- src/ActiveRecordInterface.php | 7 +++++++ src/Trait/EventsTrait.php | 2 +- tests/ActiveRecordTest.php | 29 +++++++++++++++++------------ 6 files changed, 37 insertions(+), 37 deletions(-) diff --git a/docs/create-model.md b/docs/create-model.md index 58cfd90d3..77dc78475 100644 --- a/docs/create-model.md +++ b/docs/create-model.md @@ -231,34 +231,17 @@ final class User extends ActiveRecord public ?int $id; public function __construct( - public ?string $username = null, - public ?string $email = null, + public string $username, + public string $email, public string $status = 'active', ) {} } ``` -### Limitations - -When using the constructor, you should either specify default values or `null` for the arguments, or avoid using the static -`ActiveRecord::query()` method. It will not work correctly. Instead, create a new model instance and create a new query -object by calling the `createQuery()` method on the model instance. - -```php -// If the constructor arguments do not have default values -$user = new User('admin', 'admin@example.net', 'active'); -/** @var Yiisoft\ActiveRecord\ActiveQueryInterface $query */ -$query = $user->createQuery(); -``` - -Then you can use the active query object as usual, for example: - -```php -$users = $query->where(['status' => 'active'])->all(); -``` - -Also, if the constructor arguments do not have default values, you cannot use `RepositoryTrait`, because it uses static -`ActiveRecord::query()` method. +> [!IMPORTANT] +> When using `ActiveRecord::query()` method to create a query or `RepositoryTrait` methods, the constructor is not called. +> If you need to call the constructor, override the `ActiveRecord::instantiate()` method to create a new instance with +> calling the constructor. For example, `return new static();`. ## Relations diff --git a/src/AbstractActiveRecord.php b/src/AbstractActiveRecord.php index 878693f6b..bb7905472 100644 --- a/src/AbstractActiveRecord.php +++ b/src/AbstractActiveRecord.php @@ -243,6 +243,11 @@ public function insert(?array $properties = null): void $this->insertInternal($properties); } + public static function instantiate(): static + { + return (new ReflectionClass(static::class))->newInstanceWithoutConstructor(); + } + public function isChanged(): bool { return !empty($this->newValues()); diff --git a/src/ActiveQuery.php b/src/ActiveQuery.php index b18b94507..8990e64f0 100644 --- a/src/ActiveQuery.php +++ b/src/ActiveQuery.php @@ -162,7 +162,7 @@ final public function __construct( ) { $this->model = $modelClass instanceof ActiveRecordInterface ? $modelClass - : new $modelClass(); + : $modelClass::instantiate(); parent::__construct($this->model->db()); } diff --git a/src/ActiveRecordInterface.php b/src/ActiveRecordInterface.php index fcf37768a..6caa03274 100644 --- a/src/ActiveRecordInterface.php +++ b/src/ActiveRecordInterface.php @@ -375,6 +375,13 @@ public function hasOne(self|string $modelClass, array $link): ActiveQueryInterfa */ public function insert(?array $properties = null): void; + /** + * Creates a new instance of the active record class. + * The method is used by {@see ActiveQuery} class and {@see EventsTrait} trait when an active record model passed + * as a string class name. Usually, it happens when calling {@see ActiveRecordInterface::query()} method. + */ + public static function instantiate(): static; + /** * Checks if any property returned by {@see ActiveRecordInterface::propertyNames()} method has changed. * A new active record instance is considered changed if any property has been set including default values. diff --git a/src/Trait/EventsTrait.php b/src/Trait/EventsTrait.php index 48a86c985..ca233509a 100644 --- a/src/Trait/EventsTrait.php +++ b/src/Trait/EventsTrait.php @@ -85,7 +85,7 @@ public static function query(ActiveRecordInterface|string|null $modelClass = nul { $model = $modelClass instanceof ActiveRecordInterface ? $modelClass - : new ($modelClass ?? static::class)(); + : ($modelClass !== null ? $modelClass::instantiate() : static::instantiate()); $eventDispatcher = EventDispatcherProvider::get($model::class); $eventDispatcher->dispatch($event = new BeforeCreateQuery($model)); diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index fc8e07f6f..bc4e9aab6 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -31,6 +31,7 @@ use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\NullValues; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Order; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItem; +use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItemWithConstructor; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItemWithNullFK; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithConstructor; use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithFactory; @@ -1070,10 +1071,9 @@ public function testWithFactoryNonInitiated(): void $this->assertInstanceOf(Customer::class, $customer); - $this->expectException(ArgumentCountError::class); - $this->expectExceptionMessage('Too few arguments to function'); + $customer = $order->getCustomerWithFactory(); - $order->getCustomerWithFactory(); + $this->assertInstanceOf(Customer::class, $customer); } public function testSerialization(): void @@ -1942,26 +1942,31 @@ public function testGetAllWithHasOneAndArrayValue(): void public function testWithConstructorQuery(): void { - $this->expectException(ArgumentCountError::class); - $this->expectExceptionMessage('Too few arguments to function'); + $orders = OrderWithConstructor::query()->all(); + + $this->assertCount(3, $orders); + + $orderItems = OrderItemWithConstructor::query()->all(); - OrderWithConstructor::query()->all(); + $this->assertCount(6, $orderItems); } public function testWithConstructorRelations(): void { - $this->expectException(ArgumentCountError::class); - $this->expectExceptionMessage('Too few arguments to function'); + $orderItems = (new OrderWithConstructor(1))->createQuery()->findByPk(1)->getOrderItems(); - (new OrderWithConstructor(1))->createQuery()->findByPk(1)->getOrderItems(); + $this->assertCount(2, $orderItems); } public function testWithConstructorRepositoryTrait(): void { - $this->expectException(ArgumentCountError::class); - $this->expectExceptionMessage('Too few arguments to function'); + $orders = OrderWithConstructor::findAll(); + + $this->assertCount(3, $orders); + + $order = OrderWithConstructor::findByPk(1); - OrderWithConstructor::findAll(); + $this->assertSame(1, $order->getId()); } public function testWithConstructorNewInstance(): void From 5ffe3a124afedef5edda28b8dc1686ed8a5e30fe Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 17 Mar 2026 11:14:43 +0700 Subject: [PATCH 2/4] Remove `composer-root-version` parameter --- .github/workflows/composer-require-checker.yml | 1 - .github/workflows/db-mssql.yml | 1 - .github/workflows/db-mysql.yml | 1 - .github/workflows/db-oracle.yml | 1 - .github/workflows/db-pgsql.yml | 1 - .github/workflows/db-sqlite.yml | 1 - .github/workflows/mutation.yml | 1 - .github/workflows/rector-cs.yml | 1 - .github/workflows/static.yml | 1 - 9 files changed, 9 deletions(-) diff --git a/.github/workflows/composer-require-checker.yml b/.github/workflows/composer-require-checker.yml index 6843ebf3a..2f8aece62 100644 --- a/.github/workflows/composer-require-checker.yml +++ b/.github/workflows/composer-require-checker.yml @@ -28,4 +28,3 @@ jobs: ['8.1', '8.2', '8.3', '8.4', '8.5'] required-packages: >- ['db'] - composer-root-version: 2.0.0 diff --git a/.github/workflows/db-mssql.yml b/.github/workflows/db-mssql.yml index dd54ba2c6..0050a3eee 100644 --- a/.github/workflows/db-mssql.yml +++ b/.github/workflows/db-mssql.yml @@ -80,7 +80,6 @@ jobs: - name: Install required yiisoft/db and yiisoft/db-mssql uses: yiisoft/actions/install-packages@master with: - composer-root-version: 2.0.0 packages: >- ['db', 'db-mssql'] diff --git a/.github/workflows/db-mysql.yml b/.github/workflows/db-mysql.yml index 9c3347cf1..3426142de 100644 --- a/.github/workflows/db-mysql.yml +++ b/.github/workflows/db-mysql.yml @@ -71,7 +71,6 @@ jobs: - name: Install required yiisoft/db and yiisoft/db-mysql uses: yiisoft/actions/install-packages@master with: - composer-root-version: 2.0.0 packages: >- ['db', 'db-mysql'] diff --git a/.github/workflows/db-oracle.yml b/.github/workflows/db-oracle.yml index 9fad99abc..d548168ab 100644 --- a/.github/workflows/db-oracle.yml +++ b/.github/workflows/db-oracle.yml @@ -79,7 +79,6 @@ jobs: - name: Install required yiisoft/db and yiisoft/db-oracle uses: yiisoft/actions/install-packages@master with: - composer-root-version: 2.0.0 packages: >- ['db', 'db-oracle'] diff --git a/.github/workflows/db-pgsql.yml b/.github/workflows/db-pgsql.yml index 27ecda971..02b28d480 100644 --- a/.github/workflows/db-pgsql.yml +++ b/.github/workflows/db-pgsql.yml @@ -69,7 +69,6 @@ jobs: - name: Install required yiisoft/db and yiisoft/db-pgsql uses: yiisoft/actions/install-packages@master with: - composer-root-version: 2.0.1 packages: >- ['db', 'db-pgsql'] diff --git a/.github/workflows/db-sqlite.yml b/.github/workflows/db-sqlite.yml index 94639165f..bd0b4722c 100644 --- a/.github/workflows/db-sqlite.yml +++ b/.github/workflows/db-sqlite.yml @@ -63,7 +63,6 @@ jobs: - name: Install required yiisoft/db and yiisoft/db-sqlite uses: yiisoft/actions/install-packages@master with: - composer-root-version: 2.0.0 packages: >- ['db', 'db-sqlite'] diff --git a/.github/workflows/mutation.yml b/.github/workflows/mutation.yml index a9768d508..6912041ba 100644 --- a/.github/workflows/mutation.yml +++ b/.github/workflows/mutation.yml @@ -66,7 +66,6 @@ jobs: - name: Install required yiisoft/db and yiisoft/db-pgsql uses: yiisoft/actions/install-packages@master with: - composer-root-version: 2.0.1 packages: >- ['db', 'db-pgsql'] diff --git a/.github/workflows/rector-cs.yml b/.github/workflows/rector-cs.yml index 6502076ac..3982a9c75 100644 --- a/.github/workflows/rector-cs.yml +++ b/.github/workflows/rector-cs.yml @@ -27,4 +27,3 @@ jobs: php: '8.1' required-packages: >- ['db'] - composer-root-version: 2.0.0 diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5f8179f04..287f4ac96 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -28,4 +28,3 @@ jobs: ['8.1', '8.2', '8.3', '8.4'] required-packages: >- ['db'] - composer-root-version: 2.0.0 From a0633439ca16cd8092f260c82ebdb0003eabf8a0 Mon Sep 17 00:00:00 2001 From: Tigrov Date: Tue, 31 Mar 2026 13:53:57 +0700 Subject: [PATCH 3/4] Improve doc [skip ci] --- docs/create-model.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/create-model.md b/docs/create-model.md index 77dc78475..df1c37781 100644 --- a/docs/create-model.md +++ b/docs/create-model.md @@ -239,9 +239,9 @@ final class User extends ActiveRecord ``` > [!IMPORTANT] -> When using `ActiveRecord::query()` method to create a query or `RepositoryTrait` methods, the constructor is not called. -> If you need to call the constructor, override the `ActiveRecord::instantiate()` method to create a new instance with -> calling the constructor. For example, `return new static();`. +> When calling `ActiveRecord::query()`, `ActiveRecord::instantiate()` or methods from `RepositoryTrait`, +> the constructor is not invoked. If you need the constructor to run, override the `ActiveRecord::instantiate()` method +> and return a new instance that calls the constructor. For example, `return new static();`. ## Relations From 8aabb446b9115c4ee1170890d887ccae4de558d9 Mon Sep 17 00:00:00 2001 From: vjik <525501+vjik@users.noreply.github.com> Date: Sun, 3 May 2026 11:46:01 +0000 Subject: [PATCH 4/4] Apply PHP CS Fixer and Rector changes (CI) --- tests/ActiveRecordTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/ActiveRecordTest.php b/tests/ActiveRecordTest.php index b2163165a..f51af6752 100644 --- a/tests/ActiveRecordTest.php +++ b/tests/ActiveRecordTest.php @@ -4,7 +4,6 @@ namespace Yiisoft\ActiveRecord\Tests; -use ArgumentCountError; use DateTimeImmutable; use InvalidArgumentException; use LogicException;