diff --git a/docs/create-model.md b/docs/create-model.md index 58cfd90d3..df1c37781 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 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 diff --git a/src/AbstractActiveRecord.php b/src/AbstractActiveRecord.php index 9be9d6a16..129599704 100644 --- a/src/AbstractActiveRecord.php +++ b/src/AbstractActiveRecord.php @@ -245,6 +245,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 c68bb9098..6cef60bac 100644 --- a/src/ActiveRecordInterface.php +++ b/src/ActiveRecordInterface.php @@ -374,6 +374,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 b53f3844a..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; @@ -34,6 +33,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; @@ -974,10 +974,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 @@ -1846,26 +1845,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