Skip to content

Commit

Permalink
Refactor MagicRelationsTrait (#325)
Browse files Browse the repository at this point in the history
  • Loading branch information
Tigrov authored May 20, 2024
1 parent b4e4db5 commit 39b55c4
Show file tree
Hide file tree
Showing 4 changed files with 48 additions and 34 deletions.
71 changes: 42 additions & 29 deletions src/Trait/MagicRelationsTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@

namespace Yiisoft\ActiveRecord\Trait;

use Error;
use ReflectionException;
use ReflectionMethod;
use Yiisoft\ActiveRecord\ActiveQueryInterface;
use Yiisoft\ActiveRecord\ActiveRecordInterface;
use Yiisoft\Db\Exception\InvalidArgumentException;

use function is_a;
use function lcfirst;
use function method_exists;
use function substr;
Expand All @@ -23,13 +23,21 @@
trait MagicRelationsTrait
{
/**
* Returns the relation object with the specified name.
* @inheritdoc
*
* A relation is defined by a getter method which returns an {@see ActiveQueryInterface} object.
* A relation is defined by a getter method which has prefix `get` and suffix `Query` and returns an object
* implementing the {@see ActiveQueryInterface}. Normally this would be a relational {@see ActiveQuery} object.
*
* It can be declared in either the Active Record class itself or one of its behaviors.
* For example, a relation named `orders` is defined using the following getter method:
*
* @param string $name the relation name, e.g. `orders` for a relation defined via `getOrders()` method
* ```php
* public function getOrders(): ActiveQueryInterface
* {
* return $this->hasMany(Order::class, ['customer_id' => 'id']);
* }
* ```
*
* @param string $name The relation name, for example `orders` for a relation defined via `getOrdersQuery()` method
* (case-sensitive).
* @param bool $throwException whether to throw exception if the relation does not exist.
*
Expand All @@ -43,42 +51,47 @@ public function relationQuery(string $name, bool $throwException = true): Active
{
$getter = 'get' . ucfirst($name);

try {
/** the relation could be defined in a behavior */
$relation = $this->$getter();
} catch (Error) {
if ($throwException) {
throw new InvalidArgumentException(static::class . ' has no relation named "' . $name . '".');
if (!method_exists($this, $getter)) {
if (!$throwException) {
return null;
}

return null;
throw new InvalidArgumentException(static::class . ' has no relation named "' . $name . '".');
}

if (!$relation instanceof ActiveQueryInterface) {
if ($throwException) {
throw new InvalidArgumentException(static::class . ' has no relation named "' . $name . '".');
$method = new ReflectionMethod($this, $getter);
$type = $method->getReturnType();

if (
$type === null
|| !is_a('\\' . $type->getName(), ActiveQueryInterface::class, true)
) {
if (!$throwException) {
return null;
}

return null;
}
$typeName = $type === null ? 'mixed' : $type->getName();

if (method_exists($this, $getter)) {
/** relation name is case sensitive, trying to validate it when the relation is defined within this class */
$method = new ReflectionMethod($this, $getter);
$realName = lcfirst(substr($method->getName(), 3));
throw new InvalidArgumentException(
'Relation query method "' . static::class . '::' . $getter . '()" should return type "'
. ActiveQueryInterface::class . '", but returns "' . $typeName . '" type.'
);
}

if ($realName !== $name) {
if ($throwException) {
throw new InvalidArgumentException(
'Relation names are case sensitive. ' . static::class
. " has a relation named \"$realName\" instead of \"$name\"."
);
}
/** relation name is case sensitive, trying to validate it when the relation is defined within this class */
$realName = lcfirst(substr($method->getName(), 3));

if ($realName !== $name) {
if (!$throwException) {
return null;
}

throw new InvalidArgumentException(
'Relation names are case sensitive. ' . static::class
. " has a relation named \"$realName\" instead of \"$name\"."
);
}

return $relation;
return $this->$getter();
}
}
3 changes: 2 additions & 1 deletion tests/ActiveQueryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2355,7 +2355,8 @@ public function testGetRelationInvalidArgumentExceptionHasNoRelationNamed(): voi

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage(
'Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer has no relation named "item"'
'Relation query method "Yiisoft\ActiveRecord\Tests\Stubs\ActiveRecord\Customer::getItem()" should return'
. ' type "Yiisoft\ActiveRecord\ActiveQueryInterface", but returns "void" type.'
);
$query->relationQuery('item');
}
Expand Down
4 changes: 2 additions & 2 deletions tests/Stubs/ActiveRecord/Customer.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,13 +109,13 @@ public function setOrdersReadOnly(): void
{
}

public function getOrderItems2()
public function getOrderItems2(): ActiveQuery
{
return $this->hasMany(OrderItem::class, ['order_id' => 'id'])
->via('ordersNoOrder');
}

public function getItems2()
public function getItems2(): ActiveQuery
{
return $this->hasMany(Item::class, ['id' => 'item_id'])
->via('orderItems2');
Expand Down
4 changes: 2 additions & 2 deletions tests/Stubs/ActiveRecord/Order.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,12 @@ public function activeAttributes(): array
];
}

public function getOrderItemsFor8()
public function getOrderItemsFor8(): ActiveQuery
{
return $this->hasMany(OrderItemWithNullFK::class, ['order_id' => 'id'])->andOnCondition(['subtotal' => 8.0]);
}

public function getItemsFor8()
public function getItemsFor8(): ActiveQuery
{
return $this->hasMany(Item::class, ['id' => 'item_id'])->via('orderItemsFor8');
}
Expand Down

0 comments on commit 39b55c4

Please sign in to comment.