diff --git a/src/Attribute/ObjectType/Slots.php b/src/Attribute/ObjectType/Slots.php index c5ee76b..fc32bb8 100644 --- a/src/Attribute/ObjectType/Slots.php +++ b/src/Attribute/ObjectType/Slots.php @@ -14,6 +14,7 @@ use Pinto\Slots\Build; use Pinto\Slots\Definition; use Pinto\Slots\NoDefaultValue; +use Pinto\Slots\Origin; use Pinto\Slots\RenameSlots; use Pinto\Slots\Slot; use Pinto\Slots\SlotList; @@ -78,7 +79,7 @@ public function __construct( $r = new \ReflectionClass($slot); if ($r->implementsInterface(\UnitEnum::class)) { foreach ($slot::cases() as $case) { - $this->slots->add(new Slot(name: $case)); + $this->slots->add(new Slot(name: $case, origin: Origin\EnumCase::createFromEnum($case))); } } @@ -86,7 +87,12 @@ public function __construct( continue; } $this->slots->add( - $slot instanceof Slot ? $slot : new Slot(name: $slot) + $slot instanceof Slot + ? $slot + : new Slot(name: $slot, origin: match (true) { + \is_string($slot) => Origin\StaticallyDefined::create(data: $slot), + $slot instanceof \UnitEnum => Origin\EnumCase::createFromEnum($slot), + }), ); } } @@ -163,7 +169,6 @@ public function getDefinition(ObjectListInterface $case, \Reflector $r): mixed foreach ($parametersFrom->getParameters() as $rParam) { $paramType = $rParam->getType(); if ($paramType instanceof \ReflectionNamedType || $paramType instanceof \ReflectionUnionType) { - // @todo use the type @ $paramType->getName() $args = ['name' => $rParam->getName()]; // Default should only be set if there is a default. if ($rParam->isDefaultValueAvailable()) { @@ -174,6 +179,8 @@ public function getDefinition(ObjectListInterface $case, \Reflector $r): mixed $args['fillValueFromThemeObjectClassPropertyWhenEmpty'] = $rParam->name; } + $args['origin'] = Origin\Parameter::fromReflection($rParam); + $slots[] = new Slot(...$args); } } diff --git a/src/Slots/Origin/EnumCase.php b/src/Slots/Origin/EnumCase.php new file mode 100644 index 0000000..0b308d3 --- /dev/null +++ b/src/Slots/Origin/EnumCase.php @@ -0,0 +1,35 @@ +name + ); + } + + public function enumCase(): \UnitEnum + { + try { + // @phpstan-ignore return.type + return \constant($this->className . '::' . $this->case); + } catch (\Error) { + throw new \InvalidArgumentException($this->className . '::' . $this->case . ' does not exist.'); + } + } +} diff --git a/src/Slots/Origin/Parameter.php b/src/Slots/Origin/Parameter.php new file mode 100644 index 0000000..3a9c726 --- /dev/null +++ b/src/Slots/Origin/Parameter.php @@ -0,0 +1,41 @@ +getName(), + functionName: $r->getDeclaringFunction()->getName(), + className: $r->getDeclaringClass()?->getName() ?? throw new \LogicException('unhandled'), + ); + } + + public function parameterReflection(): \ReflectionParameter + { + try { + return new \ReflectionParameter( + [$this->className, $this->functionName], + $this->parameterName, + ); + } catch (\ReflectionException|\InvalidArgumentException $e) { + throw new \InvalidArgumentException(previous: $e); + } + } +} diff --git a/src/Slots/Origin/StaticallyDefined.php b/src/Slots/Origin/StaticallyDefined.php new file mode 100644 index 0000000..c051141 --- /dev/null +++ b/src/Slots/Origin/StaticallyDefined.php @@ -0,0 +1,28 @@ +data; + } +} diff --git a/src/Slots/Slot.php b/src/Slots/Slot.php index d866caa..075f7c6 100644 --- a/src/Slots/Slot.php +++ b/src/Slots/Slot.php @@ -10,6 +10,7 @@ final class Slot public function __construct( public readonly \UnitEnum|string $name, + public readonly Origin\Parameter|Origin\StaticallyDefined|Origin\EnumCase $origin = new Origin\StaticallyDefined(), string $useNamedParameters = self::useNamedParameters, public readonly mixed $defaultValue = new NoDefaultValue(), public readonly ?string $fillValueFromThemeObjectClassPropertyWhenEmpty = null, diff --git a/tests/PintoSlotsOriginsTest.php b/tests/PintoSlotsOriginsTest.php new file mode 100644 index 0000000..3a6ebd6 --- /dev/null +++ b/tests/PintoSlotsOriginsTest.php @@ -0,0 +1,69 @@ +enumCase()); + } + + /** + * Test a serialised origin, but since gone/renamed, etc. + */ + public function testEnumCaseNonExistent(): void + { + $constructor = (new \ReflectionClass(Origin\EnumCase::class))->getConstructor() ?? throw new \LogicException('impossible'); + $origin = (new \ReflectionClass(Origin\EnumCase::class))->newInstanceWithoutConstructor(); + $constructor->setAccessible(true); + $constructor->invokeArgs($origin, ['class name', 'case name']); + + static::expectException(\InvalidArgumentException::class); + static::expectExceptionMessage('class name::case name does not exist'); + $origin->enumCase(); + } + + /** + * @see Origin\StaticallyDefined + */ + public function testStaticallyDefined(): void + { + $origin = Origin\StaticallyDefined::create('foo'); + static::assertEquals('foo', $origin->data()); + } + + /** + * @see Origin\StaticallyDefined + */ + public function testParameterReflection(): void + { + $origin = Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBasic::class, '__construct'], 'text')); + static::assertEquals('text', $origin->parameterReflection()->getName()); + } + + /** + * Test a serialised origin, but since gone/renamed, etc. + */ + public function testParameterReflectionNonExistent(): void + { + $constructor = (new \ReflectionClass(Origin\Parameter::class))->getConstructor() ?? throw new \LogicException('impossible'); + $origin = (new \ReflectionClass(Origin\Parameter::class))->newInstanceWithoutConstructor(); + $constructor->setAccessible(true); + $constructor->invokeArgs($origin, ['parameter name', '__construct', PintoObjectSlotsBasic::class]); + + static::expectException(\InvalidArgumentException::class); + $origin->parameterReflection(); + } +} diff --git a/tests/PintoSlotsTest.php b/tests/PintoSlotsTest.php index 192b14c..7164200 100644 --- a/tests/PintoSlotsTest.php +++ b/tests/PintoSlotsTest.php @@ -11,6 +11,8 @@ use Pinto\tests\fixtures\Etc\SlotEnum; use Pinto\tests\fixtures\Lists; use Pinto\tests\fixtures\Lists\PintoListSlots; +use Pinto\tests\fixtures\Objects\Faulty\PintoObjectSlotsBindPromotedPublicWithDefinedSlots; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsAttributeOnMethod; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsBasic; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsBindPromotedPublic; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsBindPromotedPublicNonConstructor; @@ -19,7 +21,13 @@ use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsByInheritanceGrandParent; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsExplicit; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsExplicitEnumClass; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsExplicitEnums; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsExplicitIgnoresReflection; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsFromList; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsFromListCase; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsFromListMethodSpecified; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsMissingSlotValue; +use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsMissingSlotValueWithDefault; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsRenameChild; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsRenameParent; use Pinto\tests\fixtures\Objects\Slots\PintoObjectSlotsSetInvalidSlot; @@ -64,7 +72,7 @@ public function testSlotsExplicit(): void public function testSlotsExplicitEnums(): void { - $object = new fixtures\Objects\Slots\PintoObjectSlotsExplicitEnums(); + $object = new PintoObjectSlotsExplicitEnums(); $build = $object(); static::assertInstanceOf(Slots\Build::class, $build); static::assertEquals('Slot One', $build->pintoGet(SlotEnum::Slot1)); @@ -85,9 +93,9 @@ public function testSlotsExplicitEnumClass(): void static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: SlotEnum::Slot1), - new Slots\Slot(name: SlotEnum::Slot2), - new Slots\Slot(name: SlotEnum::Slot3), + new Slots\Slot(name: SlotEnum::Slot1, origin: Slots\Origin\EnumCase::createFromEnum(SlotEnum::Slot1)), + new Slots\Slot(name: SlotEnum::Slot2, origin: Slots\Origin\EnumCase::createFromEnum(SlotEnum::Slot2)), + new Slots\Slot(name: SlotEnum::Slot3, origin: Slots\Origin\EnumCase::createFromEnum(SlotEnum::Slot3)), ]), $slotsDefinition->slots); } @@ -100,10 +108,10 @@ public function testPintoObjectSlotsBindPromotedPublic(): void static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'aPublic', fillValueFromThemeObjectClassPropertyWhenEmpty: 'aPublic'), - new Slots\Slot(name: 'aPublicAndSetInInvoker', fillValueFromThemeObjectClassPropertyWhenEmpty: 'aPublicAndSetInInvoker'), - new Slots\Slot(name: 'aPrivate'), - new Slots\Slot(name: 'unionType', fillValueFromThemeObjectClassPropertyWhenEmpty: 'unionType'), + new Slots\Slot(name: 'aPublic', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBindPromotedPublic::class, '__construct'], 'aPublic')), fillValueFromThemeObjectClassPropertyWhenEmpty: 'aPublic'), + new Slots\Slot(name: 'aPublicAndSetInInvoker', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBindPromotedPublic::class, '__construct'], 'aPublicAndSetInInvoker')), fillValueFromThemeObjectClassPropertyWhenEmpty: 'aPublicAndSetInInvoker'), + new Slots\Slot(name: 'aPrivate', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBindPromotedPublic::class, '__construct'], 'aPrivate'))), + new Slots\Slot(name: 'unionType', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBindPromotedPublic::class, '__construct'], 'unionType')), fillValueFromThemeObjectClassPropertyWhenEmpty: 'unionType'), ]), $slotsDefinition->slots); $object = new PintoObjectSlotsBindPromotedPublic('the public', 'public but also overridden in invoker', 'the private', 42.0); @@ -139,12 +147,12 @@ public function testPintoObjectSlotsBindPromotedPublicWithDefinedSlots(): void { static::expectException(\Pinto\Exception\PintoThemeDefinition::class); static::expectExceptionMessage('Slots must use reflection (no explicitly defined `$slots`) when promoted properties bind is on.'); - \Pinto\ObjectType\ObjectTypeDiscovery::definitionForThemeObject(fixtures\Objects\Faulty\PintoObjectSlotsBindPromotedPublicWithDefinedSlots::class, Lists\PintoFaultyList::PintoObjectSlotsBindPromotedPublicWithDefinedSlots, definitionDiscovery: new \Pinto\DefinitionDiscovery()); + \Pinto\ObjectType\ObjectTypeDiscovery::definitionForThemeObject(PintoObjectSlotsBindPromotedPublicWithDefinedSlots::class, Lists\PintoFaultyList::PintoObjectSlotsBindPromotedPublicWithDefinedSlots, definitionDiscovery: new \Pinto\DefinitionDiscovery()); } public function testSlotsExplicitIgnoresReflection(): void { - $object = new fixtures\Objects\Slots\PintoObjectSlotsExplicitIgnoresReflection('Should be ignored', 999); + $object = new PintoObjectSlotsExplicitIgnoresReflection('Should be ignored', 999); $build = $object(); static::assertInstanceOf(Slots\Build::class, $build); static::assertEquals('Some text', $build->pintoGet('text')); @@ -175,7 +183,7 @@ public function testSlotsNoConstructor(): void public function testSlotsBuildMissingValueWithDefault(): void { - $object = new fixtures\Objects\Slots\PintoObjectSlotsMissingSlotValueWithDefault('Foo!'); + $object = new PintoObjectSlotsMissingSlotValueWithDefault('Foo!'); $build = $object(); static::assertInstanceOf(Slots\Build::class, $build); static::assertEquals('Foo!', $build->pintoGet('text')); @@ -186,20 +194,20 @@ public function testSlotsBuildMissingValueWithDefault(): void public function testDefinitionsSlotsAttrOnObject(): void { $themeDefinitions = PintoListSlots::definitions(new \Pinto\DefinitionDiscovery()); - static::assertCount(8, $themeDefinitions); + static::assertCount(9, $themeDefinitions); $slotsDefinition = $themeDefinitions[PintoListSlots::Slots]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'text'), - new Slots\Slot(name: 'number', defaultValue: 3), + new Slots\Slot(name: 'text', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBasic::class, '__construct'], 'text'))), + new Slots\Slot(name: 'number', defaultValue: 3, origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsBasic::class, '__construct'], 'number'))), ]), $slotsDefinition->slots); $slotsDefinition = $themeDefinitions[PintoListSlots::SlotsAttributeOnMethod]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'foo', defaultValue: null), - new Slots\Slot(name: 'arr', defaultValue: []), + new Slots\Slot(name: 'foo', defaultValue: null, origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsAttributeOnMethod::class, 'create'], 'foo'))), + new Slots\Slot(name: 'arr', defaultValue: [], origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsAttributeOnMethod::class, 'create'], 'arr'))), ]), $slotsDefinition->slots); } @@ -211,8 +219,8 @@ public function testDefinitionsSlotsAttrOnList(): void $slotsDefinition = $themeDefinitions[Lists\PintoListSlotsOnEnum::SlotsOnEnum]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'fooFromList'), - new Slots\Slot(name: 'number', defaultValue: 4), + new Slots\Slot(name: 'fooFromList', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsFromList::class, '__construct'], 'fooFromList'))), + new Slots\Slot(name: 'number', defaultValue: 4, origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsFromList::class, '__construct'], 'number'))), ]), $slotsDefinition->slots); } @@ -224,7 +232,7 @@ public function testDefinitionsSlotsAttrOnListCase(): void $slotsDefinition = $themeDefinitions[Lists\PintoListSlotsOnEnumCase::SlotsOnEnumCase]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'fooFromListCase'), + new Slots\Slot(name: 'fooFromListCase', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsFromListCase::class, '__construct'], 'fooFromListCase'))), ]), $slotsDefinition->slots); } @@ -239,7 +247,7 @@ public function testDefinitionsSlotsAttrByInheritance(): void $slotsDefinition = $themeDefinitions[Lists\PintoListSlotsByInheritance::SlotsByInheritanceChild]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'fooFromGrandParent'), + new Slots\Slot(name: 'fooFromGrandParent', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsByInheritanceGrandParent::class, '__construct'], 'fooFromGrandParent'))), ]), $slotsDefinition->slots); } @@ -275,8 +283,8 @@ public function testDefinitionsSlotsAttrByInheritanceModifiedSlots(): void $slotsDefinition = $themeDefinitions[Lists\PintoListSlotsByInheritance::PintoObjectSlotsByInheritanceChildModifySlots]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'fooFromGrandParent'), - new Slots\Slot(name: 'new_slot'), + new Slots\Slot(name: 'fooFromGrandParent', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsByInheritanceGrandParent::class, '__construct'], 'fooFromGrandParent'))), + new Slots\Slot(name: 'new_slot', origin: new Slots\Origin\StaticallyDefined()), ]), $slotsDefinition->slots); } @@ -299,7 +307,7 @@ public function testDefinitionsSlotsAttrOnListMethodSpecified(): void $slotsDefinition = $themeDefinitions[Lists\PintoListSlotsOnEnumMethodSpecified::SlotsOnEnumMethodSpecified]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'create', defaultValue: 'from method specified on enum #[Slots]'), + new Slots\Slot(name: 'create', defaultValue: 'from method specified on enum #[Slots]', origin: Slots\Origin\Parameter::fromReflection(new \ReflectionParameter([PintoObjectSlotsFromListMethodSpecified::class, 'create'], 'create'))), ]), $slotsDefinition->slots); } @@ -311,8 +319,8 @@ public function testSlotAttribute(): void ]); static::assertEquals([ - new Slots\Slot(name: 'foo'), - new Slots\Slot(name: 'bar'), + new Slots\Slot(name: 'foo', origin: Slots\Origin\StaticallyDefined::create(data: null)), + new Slots\Slot(name: 'bar', origin: Slots\Origin\StaticallyDefined::create(data: 'bar')), ], $attr->slots->toArray()); } @@ -320,7 +328,7 @@ public function testSlotNamedParameters(): void { static::expectException(\LogicException::class); static::expectExceptionMessage('Using this attribute without named parameters is not supported.'); - new Slots\Slot('slotname', '', 'defaultvalue'); + new Slots\Slot('slotname', origin: Slots\Origin\StaticallyDefined::create(data: 'slotname'), useNamedParameters: 'defaultvalue'); } public function testSlotAttributeNamedParameters(): void @@ -345,9 +353,9 @@ public function testRenameSlots(): void $slotsDefinition = $themeDefinitions[Lists\PintoListSlotsRename::SlotsRenameChild]; static::assertInstanceOf(Slots\Definition::class, $slotsDefinition); static::assertEquals(new SlotList([ - new Slots\Slot(name: 'slotFromParentUnrenamed'), - new Slots\Slot(name: 'stringFromParentThatWillBeRenamed'), - new Slots\Slot(name: SlotEnum::Slot1), + new Slots\Slot(name: 'slotFromParentUnrenamed', origin: Slots\Origin\StaticallyDefined::create(data: 'slotFromParentUnrenamed')), + new Slots\Slot(name: 'stringFromParentThatWillBeRenamed', origin: Slots\Origin\StaticallyDefined::create(data: 'stringFromParentThatWillBeRenamed')), + new Slots\Slot(name: SlotEnum::Slot1, origin: Slots\Origin\EnumCase::createFromEnum(SlotEnum::Slot1)), ]), $slotsDefinition->slots); $expectedRenameSlots = Slots\RenameSlots::create(); diff --git a/tests/fixtures/Lists/PintoListSlots.php b/tests/fixtures/Lists/PintoListSlots.php index 2e609b8..0d5556b 100644 --- a/tests/fixtures/Lists/PintoListSlots.php +++ b/tests/fixtures/Lists/PintoListSlots.php @@ -22,6 +22,9 @@ enum PintoListSlots implements ObjectListInterface #[Definition(Slots\PintoObjectSlotsMissingSlotValue::class)] case SlotMissingValue; + #[Definition(Slots\PintoObjectSlotsValidationFailurePhpType::class)] + case PintoObjectSlotsValidationFailurePhpType; + #[Definition(Slots\PintoObjectSlotsMissingSlotValueWithDefault::class)] case PintoObjectSlotsMissingSlotValueWithDefault; diff --git a/tests/fixtures/Objects/Slots/PintoObjectSlotsValidationFailurePhpType.php b/tests/fixtures/Objects/Slots/PintoObjectSlotsValidationFailurePhpType.php new file mode 100644 index 0000000..329ed52 --- /dev/null +++ b/tests/fixtures/Objects/Slots/PintoObjectSlotsValidationFailurePhpType.php @@ -0,0 +1,55 @@ +pintoBuild(function (Build $build): Build { + return $build + ->set('number', 'Foo') + ; + }); + } + + private function pintoMapping(): PintoMapping + { + return new PintoMapping( + enumClasses: [], + enums: [ + static::class => [PintoListSlots::class, PintoListSlots::PintoObjectSlotsValidationFailurePhpType->name], + ], + definitions: [ + static::class => new Slots\Definition(new Slots\SlotList([ + new Slots\Slot(name: 'number', defaultValue: null), + ])), + ], + buildInvokers: [ + static::class => '__invoke', + ], + types: [static::class => ObjectType\Slots::class], + lsbFactoryCanonicalObjectClasses: [], + ); + } +}