Skip to content
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

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

## 1.0.0 December 09, 2025

Expand Down
34 changes: 4 additions & 30 deletions docs/create-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -203,11 +203,6 @@ use Yiisoft\ActiveRecord\Trait\MagicPropertiesTrait;
final class User extends ActiveRecord
{
use MagicPropertiesTrait;

public function tableName(): string
{
return '{{%user}}';
}
}
```

Expand All @@ -233,37 +228,16 @@ use Yiisoft\ActiveRecord\ActiveRecord;
**/
final class User extends ActiveRecord
{
public ?int $id;

public function __construct(
public ?int $id = null,
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(1, '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.

## Relations

To define relations, use the `ActiveRecordInterface::relationQuery()` method. This method should return an instance of
Expand Down
1 change: 1 addition & 0 deletions rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,6 @@
'tests/Stubs/ActiveRecord/Category.php',
'tests/Stubs/ActiveRecord/Customer.php',
'tests/Stubs/ActiveRecord/Order.php',
'tests/Stubs/ActiveRecord/OrderWithConstructor.php',
],
]);
3 changes: 2 additions & 1 deletion src/ActiveQuery.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use Closure;
use InvalidArgumentException;
use ReflectionClass;
use ReflectionException;
use Throwable;
use Yiisoft\ActiveRecord\Internal\ArArrayHelper;
Expand Down Expand Up @@ -162,7 +163,7 @@ final public function __construct(
) {
$this->model = $modelClass instanceof ActiveRecordInterface
? $modelClass
: new $modelClass();
: (new ReflectionClass($modelClass))->newInstanceWithoutConstructor();

parent::__construct($this->model->db());
}
Expand Down
15 changes: 5 additions & 10 deletions src/Trait/EventsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Yiisoft\ActiveRecord\Trait;

use Closure;
use ReflectionClass;
use Yiisoft\ActiveRecord\ActiveQueryInterface;
use Yiisoft\ActiveRecord\ActiveRecordInterface;
use Yiisoft\ActiveRecord\Event\AfterCreateQuery;
Expand All @@ -23,8 +23,6 @@
use Yiisoft\ActiveRecord\Event\BeforeUpsert;
use Yiisoft\ActiveRecord\Event\EventDispatcherProvider;

use function is_string;

/**
* Trait to implement event dispatching for ActiveRecord.
*
Expand Down Expand Up @@ -84,14 +82,11 @@ public function populateRecord(array|object $data): static
return $this;
}

public static function query(ActiveRecordInterface|Closure|string|null $modelClass = null): ActiveQueryInterface
public static function query(ActiveRecordInterface|string|null $modelClass = null): ActiveQueryInterface
{
$model = match (true) {
$modelClass === null => new static(),
is_string($modelClass) => new $modelClass(),
$modelClass instanceof ActiveRecordInterface => $modelClass,
default => ($modelClass)(),
};
$model = $modelClass instanceof ActiveRecordInterface
? $modelClass
: (new ReflectionClass($modelClass ?? static::class))->newInstanceWithoutConstructor();

$eventDispatcher = EventDispatcherProvider::get($model::class);
$eventDispatcher->dispatch($event = new BeforeCreateQuery($model));
Expand Down
44 changes: 40 additions & 4 deletions tests/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@

namespace Yiisoft\ActiveRecord\Tests;

use ArgumentCountError;
use DivisionByZeroError;
use InvalidArgumentException;
use LogicException;
Expand Down Expand Up @@ -32,6 +31,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\OrderWithConstructor;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\OrderWithFactory;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Profile;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Promotion;
Expand Down Expand Up @@ -1069,10 +1069,9 @@ public function testWithFactoryNonInitiated(): void

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

$this->expectException(ArgumentCountError::class);
$this->expectExceptionMessage('Too few arguments to function');

$customer = $order->getCustomerWithFactory();

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

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

public function testWithConstructorQuery(): void
{
/** @var OrderWithConstructor[] $orders */
$orders = OrderWithConstructor::query()->all();

$this->assertCount(3, $orders);
}

public function testWithConstructorRelations(): void
{
$orderItems = OrderWithConstructor::query()->findByPk(1)->getOrderItems();
$this->assertCount(2, $orderItems);
}

public function testWithConstructorRepositoryTrait(): void
{
$this->assertCount(3, OrderWithConstructor::findAll());
$this->assertInstanceOf(OrderWithConstructor::class, OrderWithConstructor::findByPk(1));
}

public function testWithConstructorNewInstance(): void
{
$this->reloadFixtureAfterTest();

$newOrder = new OrderWithConstructor(1);

$this->assertTrue($newOrder->isNew());
$newOrder->save();
$this->assertFalse($newOrder->isNew());
$this->assertSame(4, $newOrder->getId());
$this->assertNotNull($newOrder->getCreatedAt());
$this->assertNotNull($newOrder->getUpdatedAt());
$this->assertNull($newOrder->getDeletedAt());
$this->assertSame(1, $newOrder->delete());
$this->assertNotNull($newOrder->getDeletedAt());
}

abstract protected function createFactory(): Factory;
}
8 changes: 0 additions & 8 deletions tests/EventsTraitTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
use Yiisoft\ActiveRecord\Event\BeforeUpdate;
use Yiisoft\ActiveRecord\Event\BeforeUpsert;
use Yiisoft\ActiveRecord\Event\EventDispatcherProvider;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Category;
use Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\CategoryEventsModel;
use Yiisoft\Test\Support\EventDispatcher\SimpleEventDispatcher;

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

public function testQueryWithClosureModelClass(): void
{
$query = CategoryEventsModel::query(fn() => new Category());

$this->assertInstanceOf(Category::class, $query->getModel());
}

public function testSaveWithEventPrevention(): void
{
EventDispatcherProvider::set(
Expand Down
96 changes: 96 additions & 0 deletions tests/Stubs/ActiveRecord/OrderItemWithConstructor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord;

use Yiisoft\ActiveRecord\ActiveQuery;
use Yiisoft\ActiveRecord\ActiveQueryInterface;
use Yiisoft\ActiveRecord\ActiveRecord;

/**
* Class OrderItem.
*/
final class OrderItemWithConstructor extends ActiveRecord
{
public function __construct(
protected int $order_id,
protected int $item_id,
protected int $quantity,
protected float $subtotal,
) {}

public function tableName(): string
{
return '{{%order_item}}';
}

public function getOrderId(): int
{
return $this->order_id;
}

public function getItemId(): int
{
return $this->item_id;
}

public function getQuantity(): int
{
return $this->quantity;
}

public function getSubtotal(): float
{
return $this->subtotal;
}

public function setOrderId(int $orderId): void
{
$this->set('order_id', $orderId);
}

public function setItemId(int $itemId): void
{
$this->set('item_id', $itemId);
}

public function setQuantity(int $quantity): void
{
$this->quantity = $quantity;
}

public function setSubtotal(float $subtotal): void
{
$this->subtotal = $subtotal;
}

public function relationQuery(string $name): ActiveQueryInterface
{
return match ($name) {
'order' => $this->getOrderQuery(),
'item' => $this->getItemQuery(),
default => parent::relationQuery($name),
};
}

public function getOrder(): ?OrderWithConstructor
{
return $this->relation('order');
}

public function getOrderQuery(): ActiveQuery
{
return $this->hasOne(OrderWithConstructor::class, ['id' => 'order_id']);
}

public function getItem(): ?Item
{
return $this->relation('item');
}

public function getItemQuery(): ActiveQuery
{
return $this->hasOne(Item::class, ['id' => 'item_id']);
}
}
Loading
Loading