Skip to content
Closed
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
15 changes: 7 additions & 8 deletions src/Illuminate/Database/Eloquent/Model.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
use Illuminate\Database\Eloquent\Relations\Pivot;
use Illuminate\Support\Arr;
use Illuminate\Support\Collection as BaseCollection;
use Illuminate\Support\Reflector;
use Illuminate\Support\Str;
use Illuminate\Support\Stringable as SupportStringable;
use Illuminate\Support\Traits\ForwardsCalls;
Expand Down Expand Up @@ -2674,6 +2675,8 @@ public function broadcastChannel()
* @param string|null $property
* @param string|null $class
* @return mixed
*
* @throws \LogicException
*/
protected static function resolveClassAttribute(string $attributeClass, ?string $property = null, ?string $class = null)
{
Expand All @@ -2697,16 +2700,12 @@ protected static function resolveClassAttribute(string $attributeClass, ?string
return static::$classAttributes[$cacheKey] = $property ? $instance->{$property} : $instance;
}

foreach ($reflection->getTraits() as $trait) {
$attributes = $trait->getAttributes($attributeClass);

if (count($attributes) > 0) {
$instance = $attributes[0]->newInstance();

return static::$classAttributes[$cacheKey] = $property ? $instance->{$property} : $instance;
}
if (! is_null($instance = Reflector::getClassAttributeFromTraits($reflection, $attributeClass))) {
return static::$classAttributes[$cacheKey] = $property ? $instance->{$property} : $instance;
}
} while ($reflection = $reflection->getParentClass());
} catch (LogicException $e) {
throw $e;
} catch (Exception) {
//
}
Expand Down
50 changes: 50 additions & 0 deletions src/Illuminate/Reflection/Reflector.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Illuminate\Support;

use LogicException;
use ReflectionAttribute;
use ReflectionClass;
use ReflectionEnum;
Expand Down Expand Up @@ -101,6 +102,55 @@ public static function getClassAttributes($objectOrClass, $attribute, $includePa
return $includeParents ? new Collection($attributes) : array_first($attributes);
}

/**
* Get the given attribute from traits used by the class.
*
* @template TAttribute of object
*
* @param \ReflectionClass $reflectionClass
* @param class-string<TAttribute> $attribute
* @return TAttribute|null
*
* @throws \LogicException
*/
public static function getClassAttributeFromTraits(ReflectionClass $reflectionClass, $attribute)
{
$matches = [];

foreach ($reflectionClass->getTraits() as $trait) {
$attributes = $trait->getAttributes($attribute);

if ($attributes !== []) {
$matches[$trait->getName()] = $attributes[0]->newInstance();
}
}

if ($matches === []) {
return null;
}

$resolved = null;

foreach ($matches as $instance) {
if (is_null($resolved)) {
$resolved = $instance;

continue;
}

if ($resolved != $instance) {
throw new LogicException(sprintf(
'Could not resolve [%s] attribute on [%s]: traits [%s] declare conflicting values.',
$attribute,
$reflectionClass->getName(),
implode(', ', array_keys($matches))
));
}
}

return $resolved;
}

/**
* Get the class name of the given parameter's type, if possible.
*
Expand Down
16 changes: 9 additions & 7 deletions src/Illuminate/Support/Traits/ReadsClassAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace Illuminate\Support\Traits;

use Exception;
use Illuminate\Support\Reflector;
use LogicException;
use ReflectionClass;

trait ReadsClassAttributes
Expand Down Expand Up @@ -57,6 +59,8 @@ protected function extractAttributeValue($instance)
* @param class-string $attributeClass
* @param \ReflectionClass|null $declaringClass
* @return object|null
*
* @throws \LogicException
*/
protected function getAttributeInstance($target, string $attributeClass, ?ReflectionClass &$declaringClass = null)
{
Expand All @@ -72,16 +76,14 @@ protected function getAttributeInstance($target, string $attributeClass, ?Reflec
return $attributes[0]->newInstance();
}

foreach ($reflection->getTraits() as $trait) {
$attributes = $trait->getAttributes($attributeClass);

if (count($attributes) > 0) {
$declaringClass = $reflection;
if (! is_null($instance = Reflector::getClassAttributeFromTraits($reflection, $attributeClass))) {
$declaringClass = $reflection;

return $attributes[0]->newInstance();
}
return $instance;
}
} while ($reflection = $reflection->getParentClass());
} catch (LogicException $e) {
throw $e;
} catch (Exception) {
//
}
Expand Down
59 changes: 59 additions & 0 deletions tests/Database/DatabaseEloquentModelAttributesTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use Illuminate\Database\Eloquent\Attributes\WithoutIncrementing;
use Illuminate\Database\Eloquent\Attributes\WithoutTimestamps;
use Illuminate\Database\Eloquent\Model;
use LogicException;
use PHPUnit\Framework\TestCase;

class DatabaseEloquentModelAttributesTest extends TestCase
Expand Down Expand Up @@ -478,6 +479,28 @@ public function test_class_attribute_takes_precedence_over_trait(): void

$this->assertSame('primary', $model->getConnectionName());
}

public function test_identical_trait_attributes_are_allowed(): void
{
$model = new ModelWithIdenticalTraitTableAttributes;

$this->assertSame('shared_table', $model->getTable());
}

public function test_conflicting_trait_attributes_throw_logic_exception(): void
{
$this->expectException(LogicException::class);
$this->expectExceptionMessage('declare conflicting values');

new ModelWithConflictingTraitTableAttributes;
}

public function test_class_attribute_takes_precedence_over_conflicting_traits(): void
{
$model = new ModelOverridingConflictingTraitTableAttributes;

$this->assertSame('from_class', $model->getTable());
}
}

enum ConnectionUnitEnum
Expand Down Expand Up @@ -831,3 +854,39 @@ class ModelOverridingTraitConnectionAttribute extends Model
{
use TraitUsingConnectionAttribute;
}

#[Table(name: 'from_alpha')]
trait TraitCarryingAlphaTableAttribute
{
}

#[Table(name: 'from_beta')]
trait TraitCarryingBetaTableAttribute
{
}

#[Table(name: 'shared_table')]
trait TraitCarryingSharedTableAttributeA
{
}

#[Table(name: 'shared_table')]
trait TraitCarryingSharedTableAttributeB
{
}

class ModelWithConflictingTraitTableAttributes extends Model
{
use TraitCarryingAlphaTableAttribute, TraitCarryingBetaTableAttribute;
}

class ModelWithIdenticalTraitTableAttributes extends Model
{
use TraitCarryingSharedTableAttributeA, TraitCarryingSharedTableAttributeB;
}

#[Table(name: 'from_class')]
class ModelOverridingConflictingTraitTableAttributes extends Model
{
use TraitCarryingAlphaTableAttribute, TraitCarryingBetaTableAttribute;
}
35 changes: 35 additions & 0 deletions tests/Support/Fixtures/ClassesWithAttributes.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,38 @@ class ParentClass
class ChildClass extends ParentClass
{
}

#[StrAttr('from_alpha')]
trait TraitWithAlphaStrAttr
{
}

#[StrAttr('from_beta')]
trait TraitWithBetaStrAttr
{
}

#[StrAttr('same')]
trait TraitWithSameStrAttrA
{
}

#[StrAttr('same')]
trait TraitWithSameStrAttrB
{
}

class ClassWithConflictingTraitAttributes
{
use TraitWithAlphaStrAttr, TraitWithBetaStrAttr;
}

class ClassWithIdenticalTraitAttributes
{
use TraitWithSameStrAttrA, TraitWithSameStrAttrB;
}

class ClassWithSingleTraitAttribute
{
use TraitWithAlphaStrAttr;
}
128 changes: 128 additions & 0 deletions tests/Support/SupportReadsClassAttributesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace Illuminate\Tests\Support;

use Attribute;
use Illuminate\Support\Traits\ReadsClassAttributes;
use LogicException;
use PHPUnit\Framework\TestCase;

class SupportReadsClassAttributesTest extends TestCase
{
public function test_trait_attribute_is_resolved()
{
$reader = new class
{
use ReadsClassAttributes;

public function read($target)
{
return $this->getAttributeValue($target, ReadsClassAttributesTestAttr::class, 'value');
}
};

$this->assertSame('from_trait', $reader->read(new ReadsClassAttributesTestWithTrait));
}

public function test_identical_trait_attributes_are_allowed()
{
$reader = new class
{
use ReadsClassAttributes;

public function read($target)
{
return $this->getAttributeValue($target, ReadsClassAttributesTestAttr::class, 'value');
}
};

$this->assertSame('same', $reader->read(new ReadsClassAttributesTestWithIdenticalTraits));
}

public function test_conflicting_trait_attributes_throw_logic_exception()
{
$reader = new class
{
use ReadsClassAttributes;

public function read($target)
{
return $this->getAttributeValue($target, ReadsClassAttributesTestAttr::class, 'value');
}
};

$this->expectException(LogicException::class);
$this->expectExceptionMessage('declare conflicting values');

$reader->read(new ReadsClassAttributesTestWithConflictingTraits);
}

public function test_class_attribute_takes_precedence_over_conflicting_traits()
{
$reader = new class
{
use ReadsClassAttributes;

public function read($target)
{
return $this->getAttributeValue($target, ReadsClassAttributesTestAttr::class, 'value');
}
};

$this->assertSame('from_class', $reader->read(new ReadsClassAttributesTestOverridingConflictingTraits));
}
}

#[Attribute(Attribute::TARGET_CLASS)]
class ReadsClassAttributesTestAttr
{
public function __construct(public string $value)
{
}
}

#[ReadsClassAttributesTestAttr('from_alpha')]
trait ReadsClassAttributesTestAlphaTrait
{
}

#[ReadsClassAttributesTestAttr('from_beta')]
trait ReadsClassAttributesTestBetaTrait
{
}

#[ReadsClassAttributesTestAttr('same')]
trait ReadsClassAttributesTestSameTraitA
{
}

#[ReadsClassAttributesTestAttr('same')]
trait ReadsClassAttributesTestSameTraitB
{
}

#[ReadsClassAttributesTestAttr('from_trait')]
trait ReadsClassAttributesTestSingleTrait
{
}

class ReadsClassAttributesTestWithTrait
{
use ReadsClassAttributesTestSingleTrait;
}

class ReadsClassAttributesTestWithIdenticalTraits
{
use ReadsClassAttributesTestSameTraitA, ReadsClassAttributesTestSameTraitB;
}

class ReadsClassAttributesTestWithConflictingTraits
{
use ReadsClassAttributesTestAlphaTrait, ReadsClassAttributesTestBetaTrait;
}

#[ReadsClassAttributesTestAttr('from_class')]
class ReadsClassAttributesTestOverridingConflictingTraits
{
use ReadsClassAttributesTestAlphaTrait, ReadsClassAttributesTestBetaTrait;
}
Loading
Loading