Skip to content

Commit f022dff

Browse files
authored
Instantiate AR model with constructor (#538)
1 parent 9ceeefd commit f022dff

9 files changed

Lines changed: 277 additions & 53 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44

55
- Bug #527: Fix PHPDoc tags `@see` (@mspirkov)
66
- Enh #532, #533: Remove unnecessary files from Composer package (@mspirkov)
7+
- Enh #538: It is now possible to instantiate AR model with constructor (@Tigrov, @olegbaturin)
8+
- Bug #538: Remove `Closure` type from parameter `$modelClass` of `EventsTrait::query()` method (@Tigrov)
79

810
## 1.0.0 December 09, 2025
911

docs/create-model.md

Lines changed: 4 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -203,11 +203,6 @@ use Yiisoft\ActiveRecord\Trait\MagicPropertiesTrait;
203203
final class User extends ActiveRecord
204204
{
205205
use MagicPropertiesTrait;
206-
207-
public function tableName(): string
208-
{
209-
return '{{%user}}';
210-
}
211206
}
212207
```
213208

@@ -233,37 +228,16 @@ use Yiisoft\ActiveRecord\ActiveRecord;
233228
**/
234229
final class User extends ActiveRecord
235230
{
231+
public ?int $id;
232+
236233
public function __construct(
237-
public ?int $id = null,
238-
public ?string $username = null,
239-
public ?string $email = null,
234+
public string $username,
235+
public string $email,
240236
public string $status = 'active',
241237
) {}
242238
}
243239
```
244240

245-
### Limitations
246-
247-
When using the constructor, you should either specify default values or `null` for the arguments, or avoid using the static
248-
`ActiveRecord::query()` method. It will not work correctly. Instead, create a new model instance and create a new query
249-
object by calling the `createQuery()` method on the model instance.
250-
251-
```php
252-
// If the constructor arguments do not have default values
253-
$user = new User(1, 'admin', 'admin@example.net', 'active');
254-
/** @var Yiisoft\ActiveRecord\ActiveQueryInterface $query */
255-
$query = $user->createQuery();
256-
```
257-
258-
Then you can use the active query object as usual, for example:
259-
260-
```php
261-
$users = $query->where(['status' => 'active'])->all();
262-
```
263-
264-
Also, if the constructor arguments do not have default values, you cannot use `RepositoryTrait`, because it uses static
265-
`ActiveRecord::query()` method.
266-
267241
## Relations
268242

269243
To define relations, use the `ActiveRecordInterface::relationQuery()` method. This method should return an instance of

rector.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@
2424
'tests/Stubs/ActiveRecord/Category.php',
2525
'tests/Stubs/ActiveRecord/Customer.php',
2626
'tests/Stubs/ActiveRecord/Order.php',
27+
'tests/Stubs/ActiveRecord/OrderWithConstructor.php',
2728
],
2829
]);

src/ActiveQuery.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Closure;
88
use InvalidArgumentException;
9+
use ReflectionClass;
910
use ReflectionException;
1011
use Throwable;
1112
use Yiisoft\ActiveRecord\Internal\ArArrayHelper;
@@ -162,7 +163,7 @@ final public function __construct(
162163
) {
163164
$this->model = $modelClass instanceof ActiveRecordInterface
164165
? $modelClass
165-
: new $modelClass();
166+
: (new ReflectionClass($modelClass))->newInstanceWithoutConstructor();
166167

167168
parent::__construct($this->model->db());
168169
}

src/Trait/EventsTrait.php

Lines changed: 5 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Yiisoft\ActiveRecord\Trait;
66

7-
use Closure;
7+
use ReflectionClass;
88
use Yiisoft\ActiveRecord\ActiveQueryInterface;
99
use Yiisoft\ActiveRecord\ActiveRecordInterface;
1010
use Yiisoft\ActiveRecord\Event\AfterCreateQuery;
@@ -23,8 +23,6 @@
2323
use Yiisoft\ActiveRecord\Event\BeforeUpsert;
2424
use Yiisoft\ActiveRecord\Event\EventDispatcherProvider;
2525

26-
use function is_string;
27-
2826
/**
2927
* Trait to implement event dispatching for ActiveRecord.
3028
*
@@ -84,14 +82,11 @@ public function populateRecord(array|object $data): static
8482
return $this;
8583
}
8684

87-
public static function query(ActiveRecordInterface|Closure|string|null $modelClass = null): ActiveQueryInterface
85+
public static function query(ActiveRecordInterface|string|null $modelClass = null): ActiveQueryInterface
8886
{
89-
$model = match (true) {
90-
$modelClass === null => new static(),
91-
is_string($modelClass) => new $modelClass(),
92-
$modelClass instanceof ActiveRecordInterface => $modelClass,
93-
default => ($modelClass)(),
94-
};
87+
$model = $modelClass instanceof ActiveRecordInterface
88+
? $modelClass
89+
: (new ReflectionClass($modelClass ?? static::class))->newInstanceWithoutConstructor();
9590

9691
$eventDispatcher = EventDispatcherProvider::get($model::class);
9792
$eventDispatcher->dispatch($event = new BeforeCreateQuery($model));

tests/ActiveRecordTest.php

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
namespace Yiisoft\ActiveRecord\Tests;
66

7-
use ArgumentCountError;
87
use DivisionByZeroError;
98
use InvalidArgumentException;
109
use LogicException;
@@ -32,6 +31,7 @@
3231
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Order;
3332
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItem;
3433
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderItemWithNullFK;
34+
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithConstructor;
3535
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithFactory;
3636
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Profile;
3737
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Promotion;
@@ -1069,10 +1069,9 @@ public function testWithFactoryNonInitiated(): void
10691069

10701070
$this->assertInstanceOf(Customer::class, $customer);
10711071

1072-
$this->expectException(ArgumentCountError::class);
1073-
$this->expectExceptionMessage('Too few arguments to function');
1074-
10751072
$customer = $order->getCustomerWithFactory();
1073+
1074+
$this->assertInstanceOf(Customer::class, $customer);
10761075
}
10771076

10781077
public function testSerialization(): void
@@ -1939,5 +1938,42 @@ public function testGetAllWithHasOneAndArrayValue(): void
19391938
$this->assertNull($promotions[1]->relation('singleItem'));
19401939
}
19411940

1941+
public function testWithConstructorQuery(): void
1942+
{
1943+
/** @var OrderWithConstructor[] $orders */
1944+
$orders = OrderWithConstructor::query()->all();
1945+
1946+
$this->assertCount(3, $orders);
1947+
}
1948+
1949+
public function testWithConstructorRelations(): void
1950+
{
1951+
$orderItems = OrderWithConstructor::query()->findByPk(1)->getOrderItems();
1952+
$this->assertCount(2, $orderItems);
1953+
}
1954+
1955+
public function testWithConstructorRepositoryTrait(): void
1956+
{
1957+
$this->assertCount(3, OrderWithConstructor::findAll());
1958+
$this->assertInstanceOf(OrderWithConstructor::class, OrderWithConstructor::findByPk(1));
1959+
}
1960+
1961+
public function testWithConstructorNewInstance(): void
1962+
{
1963+
$this->reloadFixtureAfterTest();
1964+
1965+
$newOrder = new OrderWithConstructor(1);
1966+
1967+
$this->assertTrue($newOrder->isNew());
1968+
$newOrder->save();
1969+
$this->assertFalse($newOrder->isNew());
1970+
$this->assertSame(4, $newOrder->getId());
1971+
$this->assertNotNull($newOrder->getCreatedAt());
1972+
$this->assertNotNull($newOrder->getUpdatedAt());
1973+
$this->assertNull($newOrder->getDeletedAt());
1974+
$this->assertSame(1, $newOrder->delete());
1975+
$this->assertNotNull($newOrder->getDeletedAt());
1976+
}
1977+
19421978
abstract protected function createFactory(): Factory;
19431979
}

tests/EventsTraitTest.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
use Yiisoft\ActiveRecord\Event\BeforeUpdate;
1212
use Yiisoft\ActiveRecord\Event\BeforeUpsert;
1313
use Yiisoft\ActiveRecord\Event\EventDispatcherProvider;
14-
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Category;
1514
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CategoryEventsModel;
1615
use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher;
1716

@@ -110,13 +109,6 @@ static function (object $event) use ($customQuery): void {
110109
$this->assertSame($customQuery, $query);
111110
}
112111

113-
public function testQueryWithClosureModelClass(): void
114-
{
115-
$query = CategoryEventsModel::query(fn() => new Category());
116-
117-
$this->assertInstanceOf(Category::class, $query->getModel());
118-
}
119-
120112
public function testSaveWithEventPrevention(): void
121113
{
122114
EventDispatcherProvider::set(
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord;
6+
7+
use Yiisoft\ActiveRecord\ActiveQuery;
8+
use Yiisoft\ActiveRecord\ActiveQueryInterface;
9+
use Yiisoft\ActiveRecord\ActiveRecord;
10+
11+
/**
12+
* Class OrderItem.
13+
*/
14+
final class OrderItemWithConstructor extends ActiveRecord
15+
{
16+
public function __construct(
17+
protected int $order_id,
18+
protected int $item_id,
19+
protected int $quantity,
20+
protected float $subtotal,
21+
) {}
22+
23+
public function tableName(): string
24+
{
25+
return '{{%order_item}}';
26+
}
27+
28+
public function getOrderId(): int
29+
{
30+
return $this->order_id;
31+
}
32+
33+
public function getItemId(): int
34+
{
35+
return $this->item_id;
36+
}
37+
38+
public function getQuantity(): int
39+
{
40+
return $this->quantity;
41+
}
42+
43+
public function getSubtotal(): float
44+
{
45+
return $this->subtotal;
46+
}
47+
48+
public function setOrderId(int $orderId): void
49+
{
50+
$this->set('order_id', $orderId);
51+
}
52+
53+
public function setItemId(int $itemId): void
54+
{
55+
$this->set('item_id', $itemId);
56+
}
57+
58+
public function setQuantity(int $quantity): void
59+
{
60+
$this->quantity = $quantity;
61+
}
62+
63+
public function setSubtotal(float $subtotal): void
64+
{
65+
$this->subtotal = $subtotal;
66+
}
67+
68+
public function relationQuery(string $name): ActiveQueryInterface
69+
{
70+
return match ($name) {
71+
'order' => $this->getOrderQuery(),
72+
'item' => $this->getItemQuery(),
73+
default => parent::relationQuery($name),
74+
};
75+
}
76+
77+
public function getOrder(): ?OrderWithConstructor
78+
{
79+
return $this->relation('order');
80+
}
81+
82+
public function getOrderQuery(): ActiveQuery
83+
{
84+
return $this->hasOne(OrderWithConstructor::class, ['id' => 'order_id']);
85+
}
86+
87+
public function getItem(): ?Item
88+
{
89+
return $this->relation('item');
90+
}
91+
92+
public function getItemQuery(): ActiveQuery
93+
{
94+
return $this->hasOne(Item::class, ['id' => 'item_id']);
95+
}
96+
}

0 commit comments

Comments
 (0)