diff --git a/CHANGELOG.md b/CHANGELOG.md index 5422afe..c683ca8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Enh #112: Explicitly import constants in "use" section (@mspirkov) - Enh #113: Remove unnecessary files from Composer package (@mspirkov) +- Enh #114: Improve array definition config parsing and add benchmarks (@samdark) ## 3.4.1 December 02, 2025 diff --git a/composer.json b/composer.json index f7bb066..c48c4f5 100644 --- a/composer.json +++ b/composer.json @@ -33,11 +33,15 @@ "bamarni/composer-bin-plugin": "^1.8.3", "friendsofphp/php-cs-fixer": "^3.69", "maglnet/composer-require-checker": "^4.7.1", + "phpbench/phpbench": "^1.4.1", "phpunit/phpunit": "^10.5.45", "rector/rector": "^2.4.2", "spatie/phpunit-watcher": "^1.24", "yiisoft/test-support": "^3.0.1" }, + "suggest": { + "phpbench/phpbench": "To run benchmarks." + }, "autoload": { "psr-4": { "Yiisoft\\Definitions\\": "src" diff --git a/phpbench.json b/phpbench.json new file mode 100644 index 0000000..5254f28 --- /dev/null +++ b/phpbench.json @@ -0,0 +1,12 @@ +{ + "runner.bootstrap": "vendor/autoload.php", + "runner.path": "tests/Benchmark", + "runner.retry_threshold": 3, + "report.outputs": { + "csv_file": { + "extends": "delimited", + "delimiter": ",", + "file": "benchmarks.csv" + } + } +} diff --git a/src/ArrayDefinition.php b/src/ArrayDefinition.php index 0aa7a94..1e65b86 100644 --- a/src/ArrayDefinition.php +++ b/src/ArrayDefinition.php @@ -13,15 +13,15 @@ use Yiisoft\Definitions\Helpers\ArrayDefinitionHelper; use Yiisoft\Definitions\Helpers\DefinitionExtractor; use Yiisoft\Definitions\Helpers\DefinitionResolver; -use Yiisoft\Definitions\Tests\Unit\Helpers\DefinitionValidatorTest; use function array_key_exists; use function call_user_func_array; -use function count; use function gettype; use function is_array; use function is_string; use function sprintf; +use function strpos; +use function substr; /** * Builds an object by array config. @@ -185,19 +185,18 @@ private static function getMethodsAndPropertiesFromConfig(array $config): array $methodsAndProperties = []; foreach ($config as $key => $value) { - if ($key === self::CONSTRUCTOR) { + if ($key === self::CONSTRUCTOR || $key === self::CLASS_NAME) { continue; } - /** - * @infection-ignore-all Explode limit does not affect the result. - * - * @see DefinitionValidatorTest::testIncorrectMethodName() - */ - if (count($methodArray = explode('()', $key, 2)) === 2) { - $methodsAndProperties[$key] = [self::TYPE_METHOD, $methodArray[0], $value]; - } elseif (count($propertyArray = explode('$', $key)) === 2) { - $methodsAndProperties[$key] = [self::TYPE_PROPERTY, $propertyArray[1], $value]; + $position = strpos($key, '()'); + if ($position !== false && $position !== 0 && strpos($key, '()', $position + 2) === false) { + $methodsAndProperties[$key] = [self::TYPE_METHOD, substr($key, 0, $position), $value]; + continue; + } + + if (($key[0] ?? '') === '$') { + $methodsAndProperties[$key] = [self::TYPE_PROPERTY, substr($key, 1), $value]; } } diff --git a/tests/Benchmark/DefinitionStorageBench.php b/tests/Benchmark/DefinitionStorageBench.php new file mode 100644 index 0000000..c0dd3de --- /dev/null +++ b/tests/Benchmark/DefinitionStorageBench.php @@ -0,0 +1,196 @@ + + */ + private array $definitions = []; + + private DefinitionStorage $explicitStorage; + private DefinitionStorage $resolvableStorage; + private DefinitionStorage $unresolvableStorage; + private DefinitionStorage $delegateStorage; + private DefinitionStorage $strictStorage; + + public function before(): void + { + $this->indexes = []; + $this->definitions = [ + EngineInterface::class => EngineMarkOne::class, + ColorInterface::class => ColorPink::class, + ]; + + for ($i = 0; $i < self::SERVICE_COUNT; $i++) { + $this->indexes[] = $i; + $this->definitions["service$i"] = Car::class; + } + + $this->explicitStorage = new DefinitionStorage($this->definitions); + $this->resolvableStorage = new DefinitionStorage([ + EngineInterface::class => EngineMarkOne::class, + ]); + $this->unresolvableStorage = new DefinitionStorage(); + $this->delegateStorage = new DefinitionStorage(); + $this->delegateStorage->setDelegateContainer(new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + ])); + $this->strictStorage = new DefinitionStorage([], true); + + $this->resolvableStorage->has(Car::class); + $this->delegateStorage->has(Car::class); + } + + /** + * Measures construction with a typical services map. + * + * @Groups({"construct", "storage"}) + */ + public function benchConstruct(): void + { + new DefinitionStorage($this->definitions); + } + + /** + * Measures positive lookups of explicitly configured service IDs. + * + * @Groups({"lookup", "storage", "cold"}) + */ + public function benchSequentialExplicitLookupsColdStorage(): void + { + $storage = new DefinitionStorage($this->definitions); + + foreach ($this->indexes as $index) { + $storage->has("service$index"); + } + } + + /** + * Measures positive lookups against a reused definitions storage. + * + * @Groups({"lookup", "storage", "warm", "typical"}) + */ + public function benchSequentialExplicitLookupsWarmStorage(): void + { + foreach ($this->indexes as $index) { + $this->explicitStorage->has("service$index"); + } + } + + /** + * Measures positive autowiring checks for an object graph. + * + * @Groups({"lookup", "storage", "autowire", "typical"}) + * @Revs(1000) + */ + public function benchResolvableObjectGraphColdStorage(): void + { + $storage = new DefinitionStorage([ + EngineInterface::class => EngineMarkOne::class, + ]); + + $storage->has(Car::class); + } + + /** + * Measures positive autowiring checks for an object graph against reused storage. + * + * @Groups({"lookup", "storage", "autowire", "warm", "typical"}) + * @Revs(10000) + */ + public function benchResolvableObjectGraphWarmStorage(): void + { + $this->resolvableStorage->has(Car::class); + } + + /** + * Measures negative autowiring checks for a missing dependency. + * + * @Groups({"lookup", "storage", "autowire", "typical"}) + * @Revs(1000) + */ + public function benchUnresolvableObjectGraphColdStorage(): void + { + $storage = new DefinitionStorage(); + + $storage->has(Car::class); + } + + /** + * Measures negative autowiring checks for a missing dependency against reused storage. + * + * @Groups({"lookup", "storage", "autowire", "warm", "typical"}) + * @Revs(1000) + */ + public function benchUnresolvableObjectGraphWarmStorage(): void + { + $this->unresolvableStorage->has(Car::class); + } + + /** + * Measures fallback through a delegate container when a dependency is not explicitly defined. + * + * @Groups({"lookup", "storage", "delegate", "typical"}) + * @Revs(1000) + */ + public function benchResolvableObjectGraphWithDelegateColdStorage(): void + { + $storage = new DefinitionStorage(); + $storage->setDelegateContainer(new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + ])); + + $storage->has(Car::class); + } + + /** + * Measures fallback through a delegate container against reused storage. + * + * @Groups({"lookup", "storage", "delegate", "warm", "typical"}) + * @Revs(10000) + */ + public function benchResolvableObjectGraphWithDelegateWarmStorage(): void + { + $this->delegateStorage->has(Car::class); + } + + /** + * Measures strict mode misses where class names are not implicitly resolvable. + * + * @Groups({"lookup", "storage", "strict"}) + */ + public function benchStrictModeClassMisses(): void + { + foreach ($this->indexes as $_index) { + $this->strictStorage->has(EngineMarkOne::class); + } + } +} diff --git a/tests/Benchmark/NormalizerBench.php b/tests/Benchmark/NormalizerBench.php new file mode 100644 index 0000000..6e65550 --- /dev/null +++ b/tests/Benchmark/NormalizerBench.php @@ -0,0 +1,287 @@ + + */ + private array $mixedDefinitions = []; + + public function before(): void + { + $this->classDefinitions = []; + $this->referenceDefinitions = []; + $this->arrayDefinitions = []; + $this->callableDefinitions = []; + $this->objectDefinitions = []; + $this->mixedDefinitions = []; + + for ($i = 0; $i < self::DEFINITION_COUNT; $i++) { + $this->classDefinitions[] = self::CLASS_DEFINITIONS[$i % count(self::CLASS_DEFINITIONS)]; + $this->referenceDefinitions[] = "service.$i"; + + $arrayDefinition = [ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ]; + $this->arrayDefinitions[] = $arrayDefinition; + $this->callableDefinitions[] = CarFactory::create(...); + $objectDefinition = new stdClass(); + $this->objectDefinitions[] = $objectDefinition; + + $type = $i % 6; + if ($type === 0) { + $this->mixedDefinitions[] = [$this->classDefinitions[$i], null]; + } elseif ($type === 1) { + $this->mixedDefinitions[] = [$this->referenceDefinitions[$i], null]; + } elseif ($type === 2) { + $this->mixedDefinitions[] = [$arrayDefinition, null]; + } elseif ($type === 3) { + $this->mixedDefinitions[] = [[ + '__construct()' => ['Yii Phone', '3.0'], + '$dev' => true, + 'addApp()' => ['Browser', '7'], + ], Phone::class]; + } elseif ($type === 4) { + $this->mixedDefinitions[] = [CarFactory::create(...), null]; + } else { + $this->mixedDefinitions[] = [$objectDefinition, null]; + } + } + + $this->clearNormalizerCache(); + $this->warmUpNormalizer($this->classDefinitions); + $this->warmUpNormalizer($this->referenceDefinitions); + $this->warmUpNormalizer($this->objectDefinitions); + $this->normalizeMixedDefinitions(); + } + + /** + * @Groups({"class", "warm"}) + */ + public function benchWarmClassDefinitions(): void + { + foreach ($this->classDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"class", "cold"}) + */ + public function benchColdClassDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->classDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"reference", "warm"}) + */ + public function benchWarmReferenceDefinitions(): void + { + foreach ($this->referenceDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"reference", "cold"}) + */ + public function benchColdReferenceDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->referenceDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"definition", "cold", "typical"}) + */ + public function benchArrayDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->arrayDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"factory", "cold", "typical"}) + */ + public function benchCallableDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->callableDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"value", "warm"}) + */ + public function benchWarmObjectDefinitions(): void + { + foreach ($this->objectDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"value", "cold"}) + */ + public function benchColdObjectDefinitions(): void + { + $this->clearNormalizerCache(); + + foreach ($this->objectDefinitions as $definition) { + Normalizer::normalize($definition); + } + } + + /** + * @Groups({"definition", "cold", "typical"}) + */ + public function benchArrayDefinitionsWithInferredClass(): void + { + $this->clearNormalizerCache(); + + foreach ($this->arrayDefinitions as $definition) { + unset($definition['class']); + Normalizer::normalize($definition, Car::class); + } + } + + /** + * @Groups({"mixed", "cold", "typical"}) + */ + public function benchMixedApplicationDefinitionsCold(): void + { + $this->clearNormalizerCache(); + $this->normalizeMixedDefinitions(); + } + + /** + * @Groups({"mixed", "warm", "typical"}) + */ + public function benchMixedApplicationDefinitionsWarm(): void + { + $this->normalizeMixedDefinitions(); + } + + /** + * @param iterable $definitions + */ + private function warmUpNormalizer(iterable $definitions): void + { + foreach ($definitions as $definition) { + Normalizer::normalize($definition); + } + } + + private function normalizeMixedDefinitions(): void + { + foreach ($this->mixedDefinitions as [$definition, $class]) { + Normalizer::normalize($definition, $class); + } + } + + private function clearNormalizerCache(): void + { + if (method_exists(Normalizer::class, 'clearCache')) { + Normalizer::clearCache(); + } + } +} diff --git a/tests/Benchmark/TypicalUsageBench.php b/tests/Benchmark/TypicalUsageBench.php new file mode 100644 index 0000000..fa8e4d5 --- /dev/null +++ b/tests/Benchmark/TypicalUsageBench.php @@ -0,0 +1,172 @@ + + */ + private array $mixedDefinitions = []; + + public function before(): void + { + $this->container = new SimpleContainer([ + EngineInterface::class => new EngineMarkOne(), + ColorInterface::class => new ColorPink(), + CarFactory::class => new CarFactory(), + ]); + + $this->objectGraphDefinition = ArrayDefinition::fromConfig([ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ]); + + $this->methodsAndPropertiesDefinition = ArrayDefinition::fromConfig([ + 'class' => Phone::class, + '__construct()' => [ + 'name' => 'Yii Phone', + 'version' => '3.0', + 'colors' => ['black', 'white'], + ], + '$dev' => true, + '$codeName' => 'Radar', + 'addApp()' => ['Browser', '7'], + 'setId()' => ['42'], + ]); + + $this->staticFactoryDefinition = new CallableDefinition(CarFactory::create(...)); + $this->serviceFactoryDefinition = new CallableDefinition([CarFactory::class, 'createWithColor']); + $this->reference = Reference::to(EngineInterface::class); + $this->mixedDefinitions = []; + + for ($i = 0; $i < self::MIXED_DEFINITION_COUNT; $i++) { + $type = $i % 5; + if ($type === 0) { + $this->mixedDefinitions[] = ArrayDefinition::fromConfig([ + 'class' => Car::class, + '__construct()' => [Reference::to(EngineInterface::class)], + 'setColor()' => [Reference::to(ColorInterface::class)], + ]); + } elseif ($type === 1) { + $this->mixedDefinitions[] = ArrayDefinition::fromConfig([ + 'class' => Phone::class, + '__construct()' => [ + 'name' => 'Yii Phone', + 'version' => '3.0', + 'colors' => ['black', 'white'], + ], + '$dev' => true, + 'addApp()' => ['Browser', '7'], + ]); + } elseif ($type === 2) { + $this->mixedDefinitions[] = new CallableDefinition(CarFactory::create(...)); + } elseif ($type === 3) { + $this->mixedDefinitions[] = new CallableDefinition([CarFactory::class, 'createWithColor']); + } else { + $this->mixedDefinitions[] = Reference::to(EngineInterface::class); + } + } + } + + /** + * Measures explicit array definitions with constructor, setter, and reference resolution. + * + * @Groups({"definition", "lookup", "typical"}) + * @Revs(1000) + */ + public function benchArrayDefinitionObjectGraph(): void + { + $this->objectGraphDefinition->resolve($this->container); + } + + /** + * Measures constructor arguments, public property assignment, and method calls. + * + * @Groups({"definition", "lookup", "typical"}) + * @Revs(1000) + */ + public function benchArrayDefinitionMethodsAndProperties(): void + { + $this->methodsAndPropertiesDefinition->resolve($this->container); + } + + /** + * Measures a static callable factory with autowired arguments. + * + * @Groups({"factory", "lookup", "typical"}) + * @Revs(1000) + */ + public function benchStaticFactoryDefinition(): void + { + $this->staticFactoryDefinition->resolve($this->container); + } + + /** + * Measures a callable factory resolved as a service before invocation. + * + * @Groups({"factory", "lookup", "typical"}) + * @Revs(1000) + */ + public function benchServiceFactoryDefinition(): void + { + $this->serviceFactoryDefinition->resolve($this->container); + } + + /** + * Measures direct reference resolution against a PSR container. + * + * @Groups({"reference", "lookup", "typical"}) + * @Revs(10000) + */ + public function benchReferenceResolution(): void + { + $this->reference->resolve($this->container); + } + + /** + * Measures a mixed batch of array, callable, and reference definitions against one reused container. + * + * @Groups({"mixed", "lookup", "typical"}) + * @Revs(1000) + */ + public function benchMixedDefinitionResolution(): void + { + foreach ($this->mixedDefinitions as $definition) { + $definition->resolve($this->container); + } + } +} diff --git a/tests/Unit/ArrayDefinitionTest.php b/tests/Unit/ArrayDefinitionTest.php index 539f811..b109009 100644 --- a/tests/Unit/ArrayDefinitionTest.php +++ b/tests/Unit/ArrayDefinitionTest.php @@ -36,6 +36,54 @@ public function testClass(): void self::assertInstanceOf(Phone::class, $definition->resolve($container)); } + public function testPreparedDataWithArguments(): void + { + $definition = ArrayDefinition::fromPreparedData(Phone::class, ['Yii Phone']); + + self::assertSame(Phone::class, $definition->getClass()); + self::assertSame(['Yii Phone'], $definition->getConstructorArguments()); + self::assertSame([], $definition->getMethodsAndProperties()); + } + + public function testConfigWithIgnoredKey(): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Phone::class, + 'ignored' => true, + ]); + + self::assertSame([], $definition->getMethodsAndProperties()); + } + + public function testConfigWithDollarInMiddleIsIgnored(): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Phone::class, + 'ignored$dev' => true, + ]); + + self::assertSame([], $definition->getMethodsAndProperties()); + } + + public static function dataConfigWithInvalidMethodKey(): array + { + return [ + ['addApp()hm()'], + ['()addApp'], + ]; + } + + #[DataProvider('dataConfigWithInvalidMethodKey')] + public function testConfigWithInvalidMethodKeyIsIgnored(string $key): void + { + $definition = ArrayDefinition::fromConfig([ + ArrayDefinition::CLASS_NAME => Phone::class, + $key => ['Browser'], + ]); + + self::assertSame([], $definition->getMethodsAndProperties()); + } + public static function dataConstructor(): array { return [ diff --git a/tests/Unit/CallableDefinitionTest.php b/tests/Unit/CallableDefinitionTest.php index 42fc65d..2fd6b5a 100644 --- a/tests/Unit/CallableDefinitionTest.php +++ b/tests/Unit/CallableDefinitionTest.php @@ -11,6 +11,9 @@ use Yiisoft\Definitions\Tests\Support\CarFactory; use Yiisoft\Definitions\Tests\Support\ColorInterface; use Yiisoft\Definitions\Tests\Support\ColorPink; +use Yiisoft\Definitions\Tests\Support\EngineInterface; +use Yiisoft\Definitions\Tests\Support\EngineMarkOne; +use Yiisoft\Definitions\Tests\Support\EngineMarkTwo; use Yiisoft\Test\Support\Container\SimpleContainer; final class CallableDefinitionTest extends TestCase @@ -33,6 +36,30 @@ public function testDynamicCallable(): void $this->assertInstanceOf(ColorPink::class, $car->getColor()); } + public function testDynamicCallableUsesResolvedObjectSignature(): void + { + $definition = new CallableDefinition([EngineAwareCallableFactory::class, 'create']); + + /** @var Car $car */ + $car = $definition->resolve( + new SimpleContainer([ + EngineAwareCallableFactory::class => new EngineAwareCallableFactory(), + EngineInterface::class => new EngineMarkOne(), + ]), + ); + + $this->assertInstanceOf(EngineMarkOne::class, $car->getEngine()); + + /** @var Car $car */ + $car = $definition->resolve( + new SimpleContainer([ + EngineAwareCallableFactory::class => new EngineLessCallableFactory(), + ]), + ); + + $this->assertInstanceOf(EngineMarkTwo::class, $car->getEngine()); + } + public function testNonExistsClass(): void { $definition = new CallableDefinition(['NonExistsClass', 'run']); @@ -42,3 +69,19 @@ public function testNonExistsClass(): void $definition->resolve(new SimpleContainer()); } } + +final class EngineAwareCallableFactory +{ + public function create(EngineInterface $engine): Car + { + return new Car($engine); + } +} + +final class EngineLessCallableFactory +{ + public function create(): Car + { + return new Car(new EngineMarkTwo()); + } +} diff --git a/tests/Unit/DefinitionStorageTest.php b/tests/Unit/DefinitionStorageTest.php index bef95b6..b43ca61 100644 --- a/tests/Unit/DefinitionStorageTest.php +++ b/tests/Unit/DefinitionStorageTest.php @@ -79,11 +79,22 @@ public function testExplicitDefinitionIsNotChecked(): void $this->assertSame([], $storage->getBuildStack()); } + public function testExplicitDefinitionClearsPreviousBuildStack(): void + { + $storage = new DefinitionStorage(['existing' => 'anything']); + + $this->assertFalse($storage->has(NonExisting::class)); + $this->assertNotSame([], $storage->getBuildStack()); + + $this->assertTrue($storage->has('existing')); + $this->assertSame([], $storage->getBuildStack()); + } + public function testNonExistingService(): void { $storage = new DefinitionStorage([]); - $this->assertFalse($storage->has(NonExisitng::class)); - $this->assertSame([NonExisitng::class], $storage->getBuildStack()); + $this->assertFalse($storage->has(NonExisting::class)); + $this->assertSame([NonExisting::class], $storage->getBuildStack()); } public function testServiceWithNonExistingDependency(): void @@ -159,8 +170,8 @@ public function testEmptyDelegateContainer(): void $container = new SimpleContainer([]); $storage = new DefinitionStorage([]); $storage->setDelegateContainer($container); - $this->assertFalse($storage->has(\NonExisitng::class)); - $this->assertSame([\NonExisitng::class], $storage->getBuildStack()); + $this->assertFalse($storage->has(\NonExisting::class)); + $this->assertSame([\NonExisting::class], $storage->getBuildStack()); } public function testServiceWithNonExistingUnionTypes(): void diff --git a/tests/Unit/Helpers/DefinitionValidatorTest.php b/tests/Unit/Helpers/DefinitionValidatorTest.php index bae7e3e..0dd0135 100644 --- a/tests/Unit/Helpers/DefinitionValidatorTest.php +++ b/tests/Unit/Helpers/DefinitionValidatorTest.php @@ -38,6 +38,13 @@ public function testIntegerKeyOfArray(): void ]); } + public function testCallable(): void + { + DefinitionValidator::validate(CarFactory::create(...)); + + $this->expectNotToPerformAssertions(); + } + public static function dataInvalidClass(): array { return [ diff --git a/tests/Unit/Helpers/NormalizerTest.php b/tests/Unit/Helpers/NormalizerTest.php index 05b2e24..102ed6e 100644 --- a/tests/Unit/Helpers/NormalizerTest.php +++ b/tests/Unit/Helpers/NormalizerTest.php @@ -7,9 +7,11 @@ use PHPUnit\Framework\TestCase; use stdClass; use Yiisoft\Definitions\ArrayDefinition; +use Yiisoft\Definitions\CallableDefinition; use Yiisoft\Definitions\Exception\InvalidConfigException; use Yiisoft\Definitions\Helpers\Normalizer; use Yiisoft\Definitions\Reference; +use Yiisoft\Definitions\Tests\Support\CarFactory; use Yiisoft\Definitions\Tests\Support\ColorPink; use Yiisoft\Definitions\Tests\Support\GearBox; use Yiisoft\Definitions\ValueDefinition; @@ -35,6 +37,85 @@ public function testClass(): void $this->assertSame([], $definition->getMethodsAndProperties()); } + public function testSameClass(): void + { + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize(ColorPink::class, ColorPink::class); + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame(ColorPink::class, $definition->getClass()); + } + + public function testClassRepeatedly(): void + { + Normalizer::normalize(GearBox::class); + + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize(GearBox::class); + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame(GearBox::class, $definition->getClass()); + } + + public function testLowercaseClass(): void + { + $class = 'lowercaseautoloadeddefinitiontest' . str_replace('.', '', uniqid('', true)); + $autoload = static function (string $autoloadedClass) use ($class): void { + if ($autoloadedClass === $class) { + eval('class ' . $class . ' {}'); + } + }; + + spl_autoload_register($autoload); + try { + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize($class); + } finally { + spl_autoload_unregister($autoload); + } + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame($class, $definition->getClass()); + } + + public function testReferenceRepeatedly(): void + { + Normalizer::normalize('engine'); + + $this->assertInstanceOf(Reference::class, Normalizer::normalize('engine')); + } + + public function testReferenceWithClassRepeatedly(): void + { + Normalizer::normalize('engine-with-class', GearBox::class); + + $this->assertInstanceOf(Reference::class, Normalizer::normalize('engine-with-class')); + } + + public function testReferenceWithClassDoesNotPreventLaterClassDetection(): void + { + $class = 'contextreferencethenautoloadeddefinitiontest' . str_replace('.', '', uniqid('', true)); + + $this->assertInstanceOf(Reference::class, Normalizer::normalize($class, GearBox::class)); + + $autoload = static function (string $autoloadedClass) use ($class): void { + if ($autoloadedClass === $class) { + eval('class ' . $class . ' {}'); + } + }; + + spl_autoload_register($autoload); + try { + /** @var ArrayDefinition $definition */ + $definition = Normalizer::normalize($class); + } finally { + spl_autoload_unregister($autoload); + } + + $this->assertInstanceOf(ArrayDefinition::class, $definition); + $this->assertSame($class, $definition->getClass()); + } + public function testArray(): void { /** @var ArrayDefinition $definition */ @@ -51,6 +132,20 @@ public function testArray(): void $this->assertSame([], $definition->getMethodsAndProperties()); } + public function testFirstClassCallable(): void + { + $definition = Normalizer::normalize(CarFactory::create(...)); + + $this->assertInstanceOf(CallableDefinition::class, $definition); + } + + public function testObjectCallableArray(): void + { + $definition = Normalizer::normalize([new CarFactory(), 'createWithColor']); + + $this->assertInstanceOf(CallableDefinition::class, $definition); + } + public function testReadyObject(): void { $container = new SimpleContainer();