diff --git a/src/Illuminate/Database/Eloquent/Model.php b/src/Illuminate/Database/Eloquent/Model.php index 1c80875b3825..b94022ff87ec 100644 --- a/src/Illuminate/Database/Eloquent/Model.php +++ b/src/Illuminate/Database/Eloquent/Model.php @@ -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; @@ -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) { @@ -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) { // } diff --git a/src/Illuminate/Reflection/Reflector.php b/src/Illuminate/Reflection/Reflector.php index ab3f48c86dc0..37da90b17499 100644 --- a/src/Illuminate/Reflection/Reflector.php +++ b/src/Illuminate/Reflection/Reflector.php @@ -2,6 +2,7 @@ namespace Illuminate\Support; +use LogicException; use ReflectionAttribute; use ReflectionClass; use ReflectionEnum; @@ -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 $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. * diff --git a/src/Illuminate/Support/Traits/ReadsClassAttributes.php b/src/Illuminate/Support/Traits/ReadsClassAttributes.php index f631a2c689c6..8ecc1e8a02fb 100644 --- a/src/Illuminate/Support/Traits/ReadsClassAttributes.php +++ b/src/Illuminate/Support/Traits/ReadsClassAttributes.php @@ -3,6 +3,8 @@ namespace Illuminate\Support\Traits; use Exception; +use Illuminate\Support\Reflector; +use LogicException; use ReflectionClass; trait ReadsClassAttributes @@ -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) { @@ -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) { // } diff --git a/tests/Database/DatabaseEloquentModelAttributesTest.php b/tests/Database/DatabaseEloquentModelAttributesTest.php index 5622bbb66a03..94e4d12fcc0e 100644 --- a/tests/Database/DatabaseEloquentModelAttributesTest.php +++ b/tests/Database/DatabaseEloquentModelAttributesTest.php @@ -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 @@ -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 @@ -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; +} diff --git a/tests/Support/Fixtures/ClassesWithAttributes.php b/tests/Support/Fixtures/ClassesWithAttributes.php index baed29f97fd3..012b39924f31 100644 --- a/tests/Support/Fixtures/ClassesWithAttributes.php +++ b/tests/Support/Fixtures/ClassesWithAttributes.php @@ -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; +} diff --git a/tests/Support/SupportReadsClassAttributesTest.php b/tests/Support/SupportReadsClassAttributesTest.php new file mode 100644 index 000000000000..4287aedd35f9 --- /dev/null +++ b/tests/Support/SupportReadsClassAttributesTest.php @@ -0,0 +1,128 @@ +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; +} diff --git a/tests/Support/SupportReflectorTest.php b/tests/Support/SupportReflectorTest.php index 67e438361d8f..0f3a6ee42118 100644 --- a/tests/Support/SupportReflectorTest.php +++ b/tests/Support/SupportReflectorTest.php @@ -8,6 +8,7 @@ use Illuminate\Support\Testing\Fakes\BusFake; use Illuminate\Support\Testing\Fakes\MailFake; use Illuminate\Support\Testing\Fakes\PendingMailFake; +use LogicException; use PHPUnit\Framework\TestCase; use ReflectionClass; @@ -132,6 +133,34 @@ public function testGetClassAttribute() $this->assertSame('quick', Reflector::getClassAttribute(Fixtures\ChildClass::class, Fixtures\StrAttr::class, true)->string); $this->assertSame('lazy', Reflector::getClassAttribute(Fixtures\ParentClass::class, Fixtures\StrAttr::class)->string); } + + public function testGetClassAttributeFromTraits() + { + require_once __DIR__.'/Fixtures/ClassesWithAttributes.php'; + + $this->assertNull(Reflector::getClassAttributeFromTraits( + new ReflectionClass(Fixtures\ChildClass::class), + Fixtures\StrAttr::class + )); + + $this->assertSame('from_alpha', Reflector::getClassAttributeFromTraits( + new ReflectionClass(Fixtures\ClassWithSingleTraitAttribute::class), + Fixtures\StrAttr::class + )->string); + + $this->assertSame('same', Reflector::getClassAttributeFromTraits( + new ReflectionClass(Fixtures\ClassWithIdenticalTraitAttributes::class), + Fixtures\StrAttr::class + )->string); + + $this->expectException(LogicException::class); + $this->expectExceptionMessage('declare conflicting values'); + + Reflector::getClassAttributeFromTraits( + new ReflectionClass(Fixtures\ClassWithConflictingTraitAttributes::class), + Fixtures\StrAttr::class + ); + } } class A