diff --git a/.gitignore b/.gitignore index 19982ea..37f1cd6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,7 @@ composer.lock -vendor \ No newline at end of file +vendor +.idea +.php_cs.cache +.phpcs.cache +data +test/result \ No newline at end of file diff --git a/.travis.yml b/.travis.yml index 4bc5f9a..a7d81cc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,8 +30,10 @@ install: - stty cols 120 && composer show script: - - if [[ $TEST_COVERAGE == 'true' ]]; then composer test-coverage ; else composer test ; fi - - if [[ $CS_CHECK == 'true' ]]; then composer cs-check ; fi + - if [[ $CS_CHECK == 'true' ]]; then composer 'cs-check' ; fi + - if [[ $TEST_COVERAGE == 'true' ]]; then composer 'test-unit-coverage' ; else composer 'test-unit' ; fi + - if [[ $TEST_COVERAGE == 'true' ]]; then composer 'test-component-coverage' ; else composer 'test-component' ; fi + - if [[ $TEST_COVERAGE == 'true' ]]; then composer 'test-integration-coverage' ; else composer 'test-integration' ; fi after_script: - if [[ $TEST_COVERAGE == 'true' ]]; then vendor/bin/php-coveralls -v ; fi diff --git a/composer.json b/composer.json index 7a2c093..ebe39ce 100644 --- a/composer.json +++ b/composer.json @@ -5,11 +5,15 @@ "keywords": [], "require": { "php": "^7.2", - "phpstan/phpdoc-parser": "^0.3.0", - "roave/better-reflection": "^3.0" + "phpstan/phpdoc-parser": "^0.3.0" }, "require-dev": { - "phpunit/phpunit": "^7.2" + "friendsofphp/php-cs-fixer": "^2.12", + "phpstan/phpstan": "^0.10.2", + "phpstan/phpstan-phpunit": "^0.10.0", + "phpstan/phpstan-strict-rules": "^0.10.1", + "phpunit/phpunit": "^7.2", + "slevomat/coding-standard": "^4.6" }, "autoload": { "psr-4": { @@ -25,10 +29,35 @@ "sort-packages": true }, "scripts": { - "check": [ - "@test" + "test-unit": "phpunit -c phpunit.xml --testsuite unit --colors=always ", + "test-unit-coverage": "phpunit -c phpunit.xml --testsuite unit --coverage-php test/result/unit.cov --colors=never", + "test-component": "phpunit -c phpunit.xml --testsuite component --colors=always ", + "test-component-coverage": "phpunit -c phpunit.xml --testsuite component --coverage-php test/result/component.cov --colors=never", + "test-integration": "phpunit -c phpunit.xml --testsuite integration --colors=always ", + "test-integration-coverage": "phpunit -c phpunit.xml --testsuite integration --coverage-php test/result/integration.cov --colors=never", + "cs-phpstan": [ + "phpstan analyse --level=max --ansi --no-progress -c phpstan.neon src" ], - "test": "phpunit --colors=always", - "test-coverage": "phpunit --colors=always --coverage-clover clover.xml" + "cs-phpcs": [ + "phpcs --standard=ruleset.xml --extensions=php -v --ignore=.config.php src test --cache=.phpcs.cache" + ], + "cs-phpcs-fix": [ + "phpcbf --standard=ruleset.xml --extensions=php --ignore=.config.php src test --cache=.phpcs.cache" + ], + "cs-php-cs-fixer": [ + "php-cs-fixer fix src --allow-risky yes --diff --show-progress dots --verbose --dry-run" + ], + "cs-php-cs-fixer-fix": [ + "php-cs-fixer fix src --allow-risky yes --diff --show-progress dots --verbose" + ], + "cs-check": [ + "@cs-phpcs", + "@cs-php-cs-fixer", + "@cs-phpstan" + ], + "cs-fix": [ + "@cs-phpcs-fix", + "@cs-php-cs-fixer-fix" + ] } } diff --git a/phpstan-test.neon b/phpstan-test.neon new file mode 100644 index 0000000..f496094 --- /dev/null +++ b/phpstan-test.neon @@ -0,0 +1,6 @@ +parameters: + tmpDir: data/cache/phpstan-test +includes: + - vendor/phpstan/phpstan-phpunit/extension.neon + - vendor/phpstan/phpstan-phpunit/rules.neon + - vendor/phpstan/phpstan-strict-rules/rules.neon diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..79a46de --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,4 @@ +includes: + - vendor/phpstan/phpstan-strict-rules/rules.neon +parameters: + tmpDir: data/cache/phpstan diff --git a/phpunit.xml b/phpunit.xml index 2db3447..8a6fe84 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,7 +13,13 @@ - test/unit/ + test/Unit/ + + + test/Component/ + + + test/Integration/ diff --git a/ruleset.xml b/ruleset.xml new file mode 100644 index 0000000..2647d87 --- /dev/null +++ b/ruleset.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Builder.php b/src/Builder.php index fee1b06..7484229 100644 --- a/src/Builder.php +++ b/src/Builder.php @@ -1,8 +1,9 @@ -blueprintFactory = $factory; + $this->strategy = $strategy; + } + + /** @param mixed[] $data */ + public function build(string $class, array $data): object + { + $blueprint = $this->blueprintFactory->create($class); + + $preparedData = $this->prepareData($data); + + return $blueprint($preparedData); + } + + /** + * @param mixed[] $data + * @return mixed[] + */ + private function prepareData(array $data): array + { + $preparedData = []; + + foreach ($data as $key => $value) { + if (is_array($value)) { + $value = $this->prepareData($value); + } + + if (!is_int($key)) { + $key = $this->strategy->getName($key); + } + + $preparedData[$key] = $value; + } + + return $preparedData; + } +} diff --git a/src/Builder/Blueprint/Factory.php b/src/Builder/Blueprint/Factory.php new file mode 100644 index 0000000..269b6ce --- /dev/null +++ b/src/Builder/Blueprint/Factory.php @@ -0,0 +1,8 @@ +generator = $generator; + } + + public function create(string $class): callable + { + $pattern = $this->generator->create($class); + + $blueprint = eval($pattern); + if (! is_callable($blueprint)) { + throw new BuildingError( + sprintf( + 'Generated blueprint is not valid %s', + (string) $blueprint + ) + ); + } + + return $blueprint; + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Node.php b/src/Builder/Blueprint/Factory/CodeGenerator/Node.php new file mode 100644 index 0000000..bb8f201 --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Node.php @@ -0,0 +1,54 @@ +name = $name; + $this->defaultValue = $defaultValue; + $this->type = $type; + $this->nullable = $nullable; + } + + public function type(): string + { + return $this->type; + } + + public function name(): string + { + return $this->name; + } + + public function hasDefaultValue(): bool + { + if ($this->nullable()) { + return true; + } + + return null !== $this->defaultValue; + } + + public function nullable(): bool + { + return $this->nullable; + } + + /** @return mixed */ + public function defaultValue() + { + return $this->defaultValue; + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Node/Composite.php b/src/Builder/Blueprint/Factory/CodeGenerator/Node/Composite.php new file mode 100644 index 0000000..8bd32a8 --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Node/Composite.php @@ -0,0 +1,22 @@ +nodes[] = $node; + } + + /** @return Node[] */ + public function innerNodes(): iterable + { + return $this->nodes; + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Node/ObjectList.php b/src/Builder/Blueprint/Factory/CodeGenerator/Node/ObjectList.php new file mode 100644 index 0000000..6c2c180 --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Node/ObjectList.php @@ -0,0 +1,27 @@ +type(), + $name, + false, + null + ); + $this->objectNode = $objectNode; + } + + public function objectNode(): Node + { + return $this->objectNode; + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Node/Scalar.php b/src/Builder/Blueprint/Factory/CodeGenerator/Node/Scalar.php new file mode 100644 index 0000000..de04a6b --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Node/Scalar.php @@ -0,0 +1,9 @@ +serializeScalarNode($node); + } + + if ($node instanceof Node\Composite) { + return $this->serializeComplexNode($node); + } + + if ($node instanceof Node\ObjectList) { + return $this->serializeObjectListNode($node); + } + + throw new BuildingError( + sprintf('Undefined node type: %s', get_class($node)) + ); + } + + private function serializeScalarNode(Node\Scalar $node): string + { + return sprintf(static::SCALAR_PATTERN, $node->name()); + } + + private function serializeComplexNode(Node\Composite $node): string + { + $nodes = []; + foreach ($node->innerNodes() as $innerNode) { + if ($innerNode instanceof Node\Scalar) { + $serializedInnerNode = $this->serialize($innerNode); + if ('' !== $node->name()) { + $serializedInnerNode = '[\'' . $node->name() . '\']' . $serializedInnerNode; + } + $nodes[] = '$data' . $serializedInnerNode; + continue; + } + $nodes[] = $this->serialize($innerNode); + } + + return sprintf( + self::COMPLEX_PATTERN, + $node->type(), + implode(', ', $nodes) + ); + } + + private function serializeObjectListNode(Node\ObjectList $node): string + { + return sprintf( + static::OBJECT_LIST_PATTERN, + $this->serialize($node->objectNode()), + $node->name() + ); + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator.php b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator.php new file mode 100644 index 0000000..70a8ca6 --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator.php @@ -0,0 +1,8 @@ +phpDocParser = $parser; + $this->serializer = $serializer; + } + + public function create(string $class): string + { + $reflection = new ReflectionClass($class); + + $node = $this->getNode($reflection); + + return sprintf( + self::FUNCTION_PATTERN, + $this->getDefaultSection($node), + $this->serializer->serialize($node) + ); + } + + private function getNode(ReflectionClass $class, string $name = ''): Node + { + $constructor = $class->getConstructor(); + + if (null === $constructor) { + return new Node\Composite($class->getName(), $name, false, null); + } + + return $this->getNodes($constructor, $name); + } + + private function getNodes(ReflectionMethod $method, string $name = ''): Node + { + $node = new Node\Composite( + $method->getDeclaringClass()->getName(), + $name, + false, + null + ); + + foreach ($method->getParameters() as $parameter) { + $class = $parameter->getClass(); + + if (null === $class) { + $comment = (bool) $method->getDocComment() + ? (string) $method->getDocComment() + : ''; + $node->add($this->createNode($parameter, $comment)); + + continue; + } + + $node->add($this->getNode($class, $parameter->getName())); + } + + return $node; + } + + private function createNode(\ReflectionParameter $parameter, string $phpDoc): Node + { + $type = $parameter->getType()->getName(); + + if ('array' === $type + && $this->phpDocParser->isListOfObject($phpDoc, $parameter->getName())) { + $class = $this->phpDocParser->getListType($phpDoc, $parameter); + + return new Node\ObjectList( + $parameter->getName(), + $this->getNode(new ReflectionClass($class)) + ); + } + + return new Node\Scalar( + $type, + $parameter->getName(), + $parameter->allowsNull(), + $parameter->isDefaultValueAvailable() + ? $parameter->getDefaultValue() + : null + ); + } + + private function getDefaultSection(Node $node): string + { + $defaultSection = ''; + $defaultValues = $this->getDefaultValues($node); + if ([] !== $defaultValues) { + $defaultSection = sprintf( + self::DEFAULT_VALUES_PATTERN, + var_export($defaultValues, true) + ); + } + + return $defaultSection; + } + + /** @return mixed */ + private function getDefaultValues(Node $node) + { + $values = []; + + if ($node instanceof Node\Composite) { + foreach ($node->innerNodes() as $innerNode) { + $innerNodeDefaultValues = $this->getDefaultValues($innerNode); + if ([] === $innerNodeDefaultValues) { + continue; + } + + $values[$innerNode->name()] = $innerNodeDefaultValues; + } + } + + if ($node->hasDefaultValue()) { + return $node->defaultValue(); + } + + if ($node->nullable()) { + return null; + } + + return $values; + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator/Dummy.php b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator/Dummy.php new file mode 100644 index 0000000..a106bcc --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator/Dummy.php @@ -0,0 +1,23 @@ +store = $store; + } + + public function create(string $class): string + { + return $this->store[$class]; + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator/StoreDecorator.php b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator/StoreDecorator.php new file mode 100644 index 0000000..f83d4d6 --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Generator/StoreDecorator.php @@ -0,0 +1,25 @@ +store = $store; + $this->generator = $generator; + } + + public function create(string $class): string + { + return $this->store->get($class) ?? $this->generator->create($class); + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Store.php b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Store.php new file mode 100644 index 0000000..5289bfd --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Store.php @@ -0,0 +1,9 @@ +path = $path; + } + + public function get(string $class): ?string + { + $fileFullPath = $this->path . $class; + if (file_exists($fileFullPath)) { + /** @var string $content */ + $content = file_get_contents($fileFullPath); + + return $content; + } + + return null; + } + + public function save(string $class, string $blueprint): void + { + file_put_contents($this->path . $class, $blueprint); + } +} diff --git a/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Store/Memory.php b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Store/Memory.php new file mode 100644 index 0000000..0e017f8 --- /dev/null +++ b/src/Builder/Blueprint/Factory/CodeGenerator/Pattern/Store/Memory.php @@ -0,0 +1,37 @@ +store = $blueprints; + } + + public function get(string $class): ?string + { + if (! isset($this->store[$class])) { + return null; + } + + return $this->store[$class]; + } + + public function save(string $class, string $blueprint): void + { + $this->store[$class] = $blueprint; + } + + /** @return string[] */ + public function store(): array + { + return $this->store; + } +} diff --git a/src/Builder/ParameterNameStrategy.php b/src/Builder/ParameterNameStrategy.php index b48ae92..697cc34 100644 --- a/src/Builder/ParameterNameStrategy.php +++ b/src/Builder/ParameterNameStrategy.php @@ -1,4 +1,4 @@ -snakeCaseToCamelCase($parameterName); + return $this->toCamelCase($parameterName); } - private function snakeCaseToCamelCase(string $string): string + private function toCamelCase(string $string): string { $str = str_replace('_', '', ucwords($string, '_')); diff --git a/src/Builder/Reflection.php b/src/Builder/Reflection.php index 8792eca..4658554 100644 --- a/src/Builder/Reflection.php +++ b/src/Builder/Reflection.php @@ -1,8 +1,10 @@ -parameterNameStrategy = $parameterNameStrategy; @@ -27,24 +30,32 @@ public function __construct(ParameterNameStrategy $parameterNameStrategy) /** * @param mixed[] $data - * @throws BuilderException + * @throws BuildingError */ public function build(string $class, array $data): object { try { $classReflection = new ReflectionClass($class); - /** @var ReflectionMethod $constructorMethod */ $constructor = $classReflection->getConstructor(); + $parameters = []; - $parameters = iterator_to_array($this->collect($constructor, $data)); + if (null !== $constructor) { + /** @var \Traversable $iterator */ + $iterator = $this->collect($constructor, $data); + $parameters = iterator_to_array($iterator); + } return new $class(...$parameters); } catch (Throwable $exception) { - throw new BuilderException('Cant build object', 0, $exception); + throw new BuildingError('Cant build object', 0, $exception); } } + /** + * @param mixed[] $data + * @return mixed[] + */ private function collect(ReflectionMethod $constructor, array $data): iterable { foreach ($constructor->getParameters() as $parameter) { @@ -72,6 +83,7 @@ private function collect(ReflectionMethod $constructor, array $data): iterable } } + /** @param mixed[] $data */ private function parameterDataIsInData(string $parameterName, array $data): bool { foreach (array_keys($data) as $key) { @@ -92,21 +104,14 @@ private function buildParameter(ReflectionParameter $parameter, $data, Reflectio $class = $parameter->getClass(); if (null !== $class) { - $name = $class->getName(); - /** @var ReflectionMethod $constructorMethod */ - $constructorMethod = $class->getConstructor(); - $parameters = []; - - if (null !== $constructorMethod) { - $parameters = iterator_to_array($this->collect($constructorMethod, $data)); - } - - return new $name(...$parameters); + return $this->build($class->getName(), $data); } if ($parameter->isArray()) { $parser = new PhpDocParser(new TypeParser(), new ConstExprParser()); - $node = $parser->parse(new TokenIterator((new Lexer())->tokenize($constructor->getDocComment()))); + /** @var string $comment */ + $comment = $constructor->getDocComment(); + $node = $parser->parse(new TokenIterator((new Lexer())->tokenize($comment))); foreach ($node->getParamTagValues() as $node) { if ($node->parameterName === '$' . $parameter->getName()) { $typeName = $node->type->type->name; @@ -114,15 +119,33 @@ private function buildParameter(ReflectionParameter $parameter, $data, Reflectio continue; } $list = []; + $parser = (new ParserFactory())->create( + ParserFactory::PREFER_PHP7, + new Emulative([ + 'usedAttributes' => [ + 'comments', + 'startLine', + 'endLine', + 'startFilePos', + 'endFilePos', + ], + ]) + ); + + /** @var ReflectionClass $class */ + $class = $parameter->getDeclaringClass(); + /** @var string $fileName */ + $fileName = $class->getFileName(); + /** @var string $phpFileContent */ + $phpFileContent = file_get_contents($fileName); + /** @var Stmt[] $parsedFile */ + $parsedFile = $parser->parse($phpFileContent); - $parser = (new BetterReflection())->phpParser(); - - $parsedFile = $parser->parse(file_get_contents($constructor->getDeclaringClass()->getFileName())); $namespace = $this->getNamespaceStmt($parsedFile); $uses = $this->getUseStmts($namespace); $namespaces = $this->getUsesNamespaces($uses); - foreach($data as $objectConstructorData) { + foreach ($data as $objectConstructorData) { $list[] = $this->build( $this->getFullClassName($typeName, $namespaces, $constructor->getDeclaringClass()), $objectConstructorData @@ -147,7 +170,7 @@ private function isScalar(string $value): bool 'mixed', ]; - return in_array($value, $scalars); + return in_array($value, $scalars, true); } /** @@ -167,14 +190,9 @@ private function getNamespaceStmt(array $nodes): Stmt\Namespace_ /** @return Stmt\Use_[] */ private function getUseStmts(Stmt\Namespace_ $node): array { - $uses = []; - foreach ($node->stmts as $node) { - if ($node instanceof Stmt\Use_) { - $uses[]= $node; - } - } - - return $uses; + return array_filter($node->stmts, function (Stmt $node): bool { + return $node instanceof Stmt\Use_; + }); } /** @@ -183,17 +201,15 @@ private function getUseStmts(Stmt\Namespace_ $node): array */ private function getUsesNamespaces(array $uses): array { - $names = []; - foreach ($uses as $use) { - $names[] = $use->uses[0]->name->toString(); - } - - return $names; + return array_map(function (Stmt\Use_ $use): string { + return $use->uses[0]->name->toString(); + }, $uses); } + /** @param string[] $namespaces */ private function getFullClassName(string $name, array $namespaces, ReflectionClass $class): string { - if ($name[0] === '\\') { + if ('\\' === $name[0]) { return $name; } @@ -206,7 +222,7 @@ private function getFullClassName(string $name, array $namespaces, ReflectionCla /** * @param string[] $namespaces - * @throws BuilderException + * @throws BuildingError */ private function getNamespaceForClass(string $className, array $namespaces): string { @@ -216,13 +232,13 @@ private function getNamespaceForClass(string $className, array $namespaces): str } } - throw new BuilderException('Can not resolve namespace for class ' . $className); + throw new BuildingError('Can not resolve namespace for class ' . $className); } private function endsWith(string $haystack, string $needle): bool { $length = strlen($needle); - return $length === 0 || (substr($haystack, -$length) === $needle); + return 0 === $length || (substr($haystack, -$length) === $needle); } } diff --git a/src/BuilderException.php b/src/BuilderException.php deleted file mode 100644 index 5bee453..0000000 --- a/src/BuilderException.php +++ /dev/null @@ -1,10 +0,0 @@ -phpDocParser = $phpDocParser; + $this->phpParser = $phpParser; + } + + public function isListOfObject(string $comment, string $parameterName): bool + { + $node = $this->phpDocParser->parse(new TokenIterator((new Lexer())->tokenize($comment))); + + foreach ($node->getParamTagValues() as $node) { + if ('$' . $parameterName === $node->parameterName) { + $typeName = $node->type->type->name; + if ($this->isScalar($typeName)) { + continue; + } + + return true; + } + } + + return false; + } + + public function getListType(string $comment, ReflectionParameter $parameter): string + { + $node = $this->phpDocParser->parse(new TokenIterator((new Lexer())->tokenize($comment))); + + foreach ($node->getParamTagValues() as $node) { + if ($node->parameterName === '$' . $parameter->getName()) { + $type = $node->type->type; + + /** @var ReflectionClass $class */ + $class = $parameter->getDeclaringClass(); + /** @var string $fileName */ + $fileName = $class->getFileName(); + /** @var string $phpFileContent */ + $phpFileContent = file_get_contents($fileName); + /** @var Stmt[] $parsedFile */ + $parsedFile = $this->phpParser->parse($phpFileContent); + + $namespace = $this->getNamespaceStmt($parsedFile); + $uses = $this->getUseStmts($namespace); + $namespaces = $this->getUsesNamespaces($uses); + + return $this->getFullClassName($type->name, $namespaces, $class); + } + } + + throw new BuildingError(); + } + + private function isScalar(string $value): bool + { + $scalars = [ + 'string', + 'bool', + 'int', + 'float', + 'double', + 'mixed', + ]; + + return in_array($value, $scalars, true); + } + + /** + * @param Stmt[] $nodes + */ + private function getNamespaceStmt(array $nodes): Stmt\Namespace_ + { + foreach ($nodes as $node) { + if ($node instanceof Stmt\Namespace_) { + return $node; + } + } + + return new Stmt\Namespace_(); + } + + /** @return Stmt\Use_[] */ + private function getUseStmts(Stmt\Namespace_ $node): array + { + return array_filter($node->stmts, function (Stmt $node): bool { + return $node instanceof Stmt\Use_; + }); + } + + /** + * @param Stmt\Use_[] $uses + * @return string[] + */ + private function getUsesNamespaces(array $uses): array + { + return array_map(function (Stmt\Use_ $use): string { + return $use->uses[0]->name->toString(); + }, $uses); + } + + /** @param string[] $namespaces */ + private function getFullClassName(string $name, array $namespaces, ReflectionClass $class): string + { + if ('\\' === $name[0] || explode('\\', $name)[0] !== $name) { + return $name; + } + + if (0 === count($namespaces)) { + return '\\' . $class->getNamespaceName() . '\\' . $name; + } + + return '\\' . $this->getNamespaceForClass($name, $namespaces); + } + + /** + * @param string[] $namespaces + * @throws BuildingError + */ + private function getNamespaceForClass(string $className, array $namespaces): string + { + foreach ($namespaces as $namespace) { + if ($this->endsWith($namespace, $className)) { + return $namespace; + } + } + + throw new BuildingError('Can not resolve namespace for class ' . $className); + } + + private function endsWith(string $haystack, string $needle): bool + { + $length = strlen($needle); + + return 0 === $length || (substr($haystack, -$length) === $needle); + } +} diff --git a/test/Component/Builder/BlueprintTest.php b/test/Component/Builder/BlueprintTest.php new file mode 100644 index 0000000..0a342d6 --- /dev/null +++ b/test/Component/Builder/BlueprintTest.php @@ -0,0 +1,39 @@ +create(ParserFactory::PREFER_PHP7, new Emulative([ + 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'], + ])) + ), + new Blueprint\Factory\CodeGenerator\Node\Serializer\ArrayAccess() + ) + ) + ), + new SnakeCase() + ); + } +} diff --git a/test/Component/Builder/BuilderTest.php b/test/Component/Builder/BuilderTest.php new file mode 100644 index 0000000..eb019b7 --- /dev/null +++ b/test/Component/Builder/BuilderTest.php @@ -0,0 +1,278 @@ +build($class, []); + + $this->assertInstanceOf(WithoutConstructor::class, $object); + } + + /** @test */ + public function iCanBuildSimpleObjectWithScalarValuesInConstructor(): void + { + $data = [ + 'some_string' => 'some string', + 'some_int' => 999, + ]; + $class = ScalarConstructor::class; + + /** @var ScalarConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(ScalarConstructor::class, $object); + $this->assertSame('some string', $object->someString); + $this->assertSame(999, $object->someInt); + } + + /** @test */ + public function iCanBuildSimpleObjectWithScalarAndObjectValuesInConstructor(): void + { + $data = [ + 'some_string' => 'some string', + 'some_int' => 999, + 'some_object' => [], + ]; + $class = MixedConstructor::class; + + /** @var MixedConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(MixedConstructor::class, $object); + $this->assertSame('some string', $object->someString); + $this->assertSame(999, $object->someInt); + $this->assertInstanceOf(EmptyConstructor::class, $object->someObject); + } + + /** @test */ + public function iCanBuildSimpleObjectWithDefaultValuesInConstructor(): void + { + $data = [ + 'some_object' => [], + ]; + $class = MixedConstructorWithDefaultValue::class; + + /** @var MixedConstructorWithDefaultValue $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(MixedConstructorWithDefaultValue::class, $object); + $this->assertSame('some string', $object->someString); + $this->assertSame(999, $object->someInt); + $this->assertInstanceOf(EmptyConstructor::class, $object->someObject); + } + + /** @test */ + public function iCanBuildObjectWithObjectCollectionWithoutUseInConstructor(): void + { + $data = [ + 'list' => [ + [ + 'some_string' => 'some string1', + 'some_int' => 1, + ], + [ + 'some_string' => 'some string2', + 'some_int' => 2, + ], + ], + ]; + $class = Collection\WithoutUseStmtConstructor::class; + + /** @var Collection\WithoutUseStmtConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(Collection\WithoutUseStmtConstructor::class, $object); + $this->assertCount(2, $object->list); + foreach ($object->list as $element) { + $this->assertInstanceOf(Collection\ScalarConstructor::class, $element); + } + } + + /** @test */ + public function iCanBuildObjectWithObjectCollectionWithUseInConstructor(): void + { + $data = [ + 'list' => [ + [], + [], + ], + ]; + $class = Collection\WithUseStmtConstructor::class; + + /** @var Collection\WithUseStmtConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(Collection\WithUseStmtConstructor::class, $object); + $this->assertCount(2, $object->list); + foreach ($object->list as $element) { + $this->assertInstanceOf(SecondEmptyConstructor::class, $element); + } + } + + /** @test */ + public function iCanBuildObjectWithObjectCollectionWithoutUseButWithFQNTypedArrayInConstructor(): void + { + $data = [ + 'list' => [ + [], + [], + ], + ]; + $class = Collection\WithoutUseButWithFQNTypedArrayConstructor::class; + + /** @var Collection\WithoutUseButWithFQNTypedArrayConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(Collection\WithoutUseButWithFQNTypedArrayConstructor::class, $object); + $this->assertCount(2, $object->list); + foreach ($object->list as $element) { + $this->assertInstanceOf(EmptyConstructor::class, $element); + } + } + + /** @test */ + public function iCanBuildAdvancedObjectHierarchy(): void + { + $data = [ + 'some_string' => 'some string3', + 'simple_object_1' => [ + 'some_string' => 'some string1', + 'someInt' => 1, + ], + 'simple_object_2' => [ + 'some_string' => 'some string2', + 'some_int' => 2, + 'some_object' => [], + ], + 'some_int' => 3, + ]; + $class = ComplexHierarchy::class; + + /** @var ComplexHierarchy $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(ComplexHierarchy::class, $object); + $this->assertSame('some string3', $object->someString); + $this->assertSame(3, $object->someInt); + $this->assertInstanceOf(ScalarConstructor::class, $object->simpleObject1); + $this->assertInstanceOf(MixedConstructor::class, $object->simpleObject2); + $this->assertSame(1, $object->simpleObject1->someInt); + $this->assertSame('some string1', $object->simpleObject1->someString); + $this->assertSame(2, $object->simpleObject2->someInt); + $this->assertSame('some string2', $object->simpleObject2->someString); + } + + /** @test */ + public function iCanBuildObjectWithScalarCollectionTypedArrayInConstructor(): void + { + $data = [ + 'list1' => ['str', 'str'], + 'list2' => ['str', 'str'], + ]; + $class = Collection\WithScalarTypedArrayConstructor::class; + + /** @var Collection\WithScalarTypedArrayConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(Collection\WithScalarTypedArrayConstructor::class, $object); + $this->assertCount(2, $object->list1); + $this->assertCount(2, $object->list2); + foreach ($object->list1 as $element) { + $this->assertSame('str', $element); + } + foreach ($object->list2 as $element) { + $this->assertSame('str', $element); + } + } + + /** @test */ + public function iCanBuildObjectWithBothScalarAndObjectCollectionTypedArrayInConstructor(): void + { + $data = [ + 'list1' => ['str', 'str'], + 'list2' => [ + [ + 'some_string' => 'some string1', + 'some_int' => 1, + ], + [ + 'some_string' => 'some string2', + 'some_int' => 2, + ], + ], + ]; + $class = Collection\WithScalarTypedArrayAndObjectListConstructor::class; + + /** @var Collection\WithScalarTypedArrayAndObjectListConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(Collection\WithScalarTypedArrayAndObjectListConstructor::class, $object); + $this->assertCount(2, $object->list1); + $this->assertCount(2, $object->list2); + foreach ($object->list1 as $element) { + $this->assertSame('str', $element); + } + foreach ($object->list2 as $element) { + $this->assertInstanceOf(Collection\ScalarConstructor::class, $element); + } + } + + /** @test */ + public function iCanBuildObjectWithNullableParameterWithoutDefaultValue(): void + { + $data = [ + 'some_string_1' => 'some string1', + 'some_string_2' => 'some string2', + ]; + $class = NullableConstructor::class; + + /** @var NullableConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(NullableConstructor::class, $object); + $this->assertSame('some string1', $object->someString1); + $this->assertSame('some string2', $object->someString2); + $this->assertNull($object->someInt); + } + + /** @test */ + public function iCanBuildObjectWithNullableParameterWithHimValueValue(): void + { + $data = [ + 'some_string_1' => 'some string1', + 'some_int' => 123, + 'some_string_2' => 'some string2', + ]; + $class = NullableConstructor::class; + + /** @var NullableConstructor $object */ + $object = static::$builder->build($class, $data); + + $this->assertInstanceOf(NullableConstructor::class, $object); + $this->assertSame('some string1', $object->someString1); + $this->assertSame('some string2', $object->someString2); + $this->assertSame(123, $object->someInt); + } +} diff --git a/test/Component/Builder/ReflectionTest.php b/test/Component/Builder/ReflectionTest.php new file mode 100644 index 0000000..5408173 --- /dev/null +++ b/test/Component/Builder/ReflectionTest.php @@ -0,0 +1,30 @@ + 'some string', + 'some_int' => 999, + ]; + $class = MixedConstructor::class; + + $this->expectException(BuildingError::class); + + static::$builder->build($class, $data); + } +} diff --git a/test/Integration/Builder/Blueprint/Factory/CodeGenerator/Store/FilesystemTest.php b/test/Integration/Builder/Blueprint/Factory/CodeGenerator/Store/FilesystemTest.php new file mode 100644 index 0000000..eb816c9 --- /dev/null +++ b/test/Integration/Builder/Blueprint/Factory/CodeGenerator/Store/FilesystemTest.php @@ -0,0 +1,71 @@ +save('SomeClass', 'content php'); + + $savedBlueprint = file_get_contents('/tmp/SomeClass'); + $this->assertSame('content php', $savedBlueprint); + } + + /** @test */ + public function iCanGetSavedBlueprintInFilesystem(): void + { + $store = new Filesystem('/tmp/'); + file_put_contents( + '/tmp/SomeClass', + 'return function() { return \'some string\'; };' + ); + + $function = $store->get('SomeClass'); + + $this->assertSame( + 'return function() { return \'some string\'; };', + $function + ); + } + + /** @test */ + public function whenFileExistsInFilesystemThenOverrideIt(): void + { + $store = new Filesystem('/tmp/'); + file_put_contents( + '/tmp/SomeClass', + 'some string' + ); + + $store->save('SomeClass', 'override'); + + $savedBlueprint = file_get_contents('/tmp/SomeClass'); + $this->assertSame('override', $savedBlueprint); + } + + /** @test */ + public function whenFileDoesNotExistInFilesystemThenReturnNull(): void + { + $store = new Filesystem('/tmp/'); + + $function = $store->get('SomeClass'); + + $this->assertNull($function); + } +} diff --git a/test/ListOfObjectsWithUseStmtConstructor.php b/test/ListOfObjectsWithUseStmtConstructor.php deleted file mode 100644 index 7fac801..0000000 --- a/test/ListOfObjectsWithUseStmtConstructor.php +++ /dev/null @@ -1,21 +0,0 @@ -list = $list; - $this->object = new SomeObject(); - } -} diff --git a/test/ListOfObjectsWithoutUseButWithFQNTypedArrayConstructor.php b/test/ListOfObjectsWithoutUseButWithFQNTypedArrayConstructor.php deleted file mode 100644 index 1aeabf4..0000000 --- a/test/ListOfObjectsWithoutUseButWithFQNTypedArrayConstructor.php +++ /dev/null @@ -1,16 +0,0 @@ -list = $list; - } -} diff --git a/test/ListOfObjectsWithoutUseStmtConstructor.php b/test/ListOfObjectsWithoutUseStmtConstructor.php deleted file mode 100644 index 38db6b5..0000000 --- a/test/ListOfObjectsWithoutUseStmtConstructor.php +++ /dev/null @@ -1,16 +0,0 @@ -list = $list; - } -} diff --git a/test/Object/Collection/ScalarConstructor.php b/test/Object/Collection/ScalarConstructor.php new file mode 100644 index 0000000..6ed4588 --- /dev/null +++ b/test/Object/Collection/ScalarConstructor.php @@ -0,0 +1,15 @@ +someString = $someString; + $this->someInt = $someInt; + } +} diff --git a/test/ListOfObjectsWithScalarTypedArrayAndObjectListConstructor.php b/test/Object/Collection/WithScalarTypedArrayAndObjectListConstructor.php similarity index 55% rename from test/ListOfObjectsWithScalarTypedArrayAndObjectListConstructor.php rename to test/Object/Collection/WithScalarTypedArrayAndObjectListConstructor.php index efdd148..065fe1d 100644 --- a/test/ListOfObjectsWithScalarTypedArrayAndObjectListConstructor.php +++ b/test/Object/Collection/WithScalarTypedArrayAndObjectListConstructor.php @@ -1,15 +1,15 @@ -list = $list; + $this->object = new EmptyConstructor(); + } +} diff --git a/test/Object/Collection/WithoutUseButWithFQNTypedArrayConstructor.php b/test/Object/Collection/WithoutUseButWithFQNTypedArrayConstructor.php new file mode 100644 index 0000000..46c8bba --- /dev/null +++ b/test/Object/Collection/WithoutUseButWithFQNTypedArrayConstructor.php @@ -0,0 +1,16 @@ +list = $list; + } +} diff --git a/test/Object/Collection/WithoutUseStmtConstructor.php b/test/Object/Collection/WithoutUseStmtConstructor.php new file mode 100644 index 0000000..27d9403 --- /dev/null +++ b/test/Object/Collection/WithoutUseStmtConstructor.php @@ -0,0 +1,16 @@ +list = $list; + } +} diff --git a/test/SomeAggregateRoot.php b/test/Object/ComplexHierarchy.php similarity index 66% rename from test/SomeAggregateRoot.php rename to test/Object/ComplexHierarchy.php index ab48e11..1d96003 100644 --- a/test/SomeAggregateRoot.php +++ b/test/Object/ComplexHierarchy.php @@ -1,8 +1,8 @@ -someString = $someString; diff --git a/test/Object/SomeObject.php b/test/Object/EmptyConstructor.php similarity index 63% rename from test/Object/SomeObject.php rename to test/Object/EmptyConstructor.php index 59e8e8c..bc11305 100644 --- a/test/Object/SomeObject.php +++ b/test/Object/EmptyConstructor.php @@ -1,8 +1,8 @@ -someString = $someString; $this->someInt = $someInt; diff --git a/test/SimpleMixedConstructorWithDefaultValue.php b/test/Object/MixedConstructorWithDefaultValue.php similarity index 65% rename from test/SimpleMixedConstructorWithDefaultValue.php rename to test/Object/MixedConstructorWithDefaultValue.php index d44b028..7b746f7 100644 --- a/test/SimpleMixedConstructorWithDefaultValue.php +++ b/test/Object/MixedConstructorWithDefaultValue.php @@ -1,15 +1,15 @@ -serialize($node); + + $this->assertSame('[\'someName\']', $serializedNode); + } + + /** @test */ + public function serializeComplexNodeToArrayAccessString(): void + { + $node = new Composite('SomeClassName', 'someName', false, null); + $scalarStringNode = new Scalar('string', 'someStringName', false, null); + $scalarIntNode = new Scalar('int', 'someInt', false, null); + $node->add($scalarStringNode); + $node->add($scalarIntNode); + + $serializedNode = static::$serializer->serialize($node); + + $this->assertSame( + 'new SomeClassName($data[\'someName\'][\'someStringName\'], $data[\'someName\'][\'someInt\'])', + $serializedNode + ); + } + + /** @test */ + public function serializeObjectListNodeToArrayAccessString(): void + { + $objectNode = new Composite('SomeClassName', 'someName', false, null); + $scalarStringNode = new Scalar('string', 'someStringName', false, null); + $scalarIntNode = new Scalar('int', 'someInt', false, null); + $objectNode->add($scalarStringNode); + $objectNode->add($scalarIntNode); + $node = new ObjectList('someList', $objectNode); + + $serializedNode = static::$serializer->serialize($node); + + $this->assertSame( + '(function (array $list) { + $arr = []; + foreach ($list as $data) { + $arr[] = new SomeClassName($data[\'someName\'][\'someStringName\'], $data[\'someName\'][\'someInt\']); + } + + return $arr; + })($data[\'someList\'])', + $serializedNode + ); + } + + /** @test */ + public function whenNodeObjectIsNotKnowThenThrowsException(): void + { + $node = new class('a', 'a', false, null) extends Node { + }; + + $this->expectException(BuildingError::class); + + static::$serializer->serialize($node); + } +} diff --git a/test/Unit/Builder/Blueprint/Factory/CodeGenerator/PatternGenerator/AnonymousTest.php b/test/Unit/Builder/Blueprint/Factory/CodeGenerator/PatternGenerator/AnonymousTest.php new file mode 100644 index 0000000..8ae53cb --- /dev/null +++ b/test/Unit/Builder/Blueprint/Factory/CodeGenerator/PatternGenerator/AnonymousTest.php @@ -0,0 +1,139 @@ +create(ParserFactory::PREFER_PHP7, new Emulative([ + 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'], + ])) + ), + new ArrayAccess() + ); + } + + /** @test */ + public function iCanGenerateSimpleObjectClosure(): void + { + $class = EmptyConstructor::class; + + $blueprint = self::$factory->create($class); + + $this->assertSame( + 'return function(array $data) use ($class): object { + + return new RstGroup\ObjectBuilder\Test\Object\EmptyConstructor(); +};', + $blueprint + ); + } + + /** @test */ + public function iCanBuildSimpleObjectWithScalarValuesInConstructor(): void + { + $class = ScalarConstructor::class; + + $blueprint = self::$factory->create($class); + +// @codingStandardsIgnoreStart + $this->assertSame( + 'return function(array $data) use ($class): object { + + return new RstGroup\ObjectBuilder\Test\Object\ScalarConstructor($data[\'someString\'], $data[\'someInt\']); +};', + $blueprint + ); +// @codingStandardsIgnoreEnd + } + + /** @test */ + public function iCanBuildSimpleObjectWithDefaultValuesInConstructor(): void + { + $class = MixedConstructorWithDefaultValue::class; + + $blueprint = self::$factory->create($class); + +// @codingStandardsIgnoreStart + $this->assertSame( + 'return function(array $data) use ($class): object { + $default = array ( + \'someString\' => \'some string\', + \'someInt\' => 999, +); + $data = array_merge($default, $data); + + return new RstGroup\ObjectBuilder\Test\Object\MixedConstructorWithDefaultValue(new RstGroup\ObjectBuilder\Test\Object\EmptyConstructor(), $data[\'someString\'], $data[\'someInt\']); +};', + $blueprint + ); +// @codingStandardsIgnoreEnd + } + + /** @test */ + public function iCanBuildAdvancedObjectHierarchy(): void + { + $class = ComplexHierarchy::class; + + $blueprint = self::$factory->create($class); + +// @codingStandardsIgnoreStart + $this->assertSame( + 'return function(array $data) use ($class): object { + + return new RstGroup\ObjectBuilder\Test\Object\ComplexHierarchy($data[\'someString\'], new RstGroup\ObjectBuilder\Test\Object\ScalarConstructor($data[\'simpleObject1\'][\'someString\'], $data[\'simpleObject1\'][\'someInt\']), new RstGroup\ObjectBuilder\Test\Object\MixedConstructor($data[\'simpleObject2\'][\'someString\'], $data[\'simpleObject2\'][\'someInt\'], new RstGroup\ObjectBuilder\Test\Object\EmptyConstructor()), $data[\'someInt\']); +};', + $blueprint + ); +// @codingStandardsIgnoreEnd + } + + /** @test */ + public function iCanBuildObjectWithObjectCollectionWithoutUseInConstructor(): void + { + $class = WithoutUseStmtConstructor::class; + + $blueprint = self::$factory->create($class); + +// @codingStandardsIgnoreStart + $this->assertSame('return function(array $data) use ($class): object { + + return new RstGroup\ObjectBuilder\Test\Object\Collection\WithoutUseStmtConstructor((function (array $list) { + $arr = []; + foreach ($list as $data) { + $arr[] = new RstGroup\ObjectBuilder\Test\Object\Collection\ScalarConstructor($data[\'someString\'], $data[\'someInt\']); + } + + return $arr; + })($data[\'list\'])); +};', + $blueprint + ); +// @codingStandardsIgnoreEnd + } +} diff --git a/test/Unit/Builder/Blueprint/Factory/CodeGenerator/PatternGenerator/StoreDecoratorTest.php b/test/Unit/Builder/Blueprint/Factory/CodeGenerator/PatternGenerator/StoreDecoratorTest.php new file mode 100644 index 0000000..bdd28bd --- /dev/null +++ b/test/Unit/Builder/Blueprint/Factory/CodeGenerator/PatternGenerator/StoreDecoratorTest.php @@ -0,0 +1,37 @@ + 'pattern']), + new Dummy() + ); + + $pattern = $patternGeneratorDecorator->create('class'); + + $this->assertSame('pattern', $pattern); + } + + /** @test */ + public function returnNewCreatedPatternWhenPatterDoesNotExistInMemory(): void + { + $patternGeneratorDecorator = new StoreDecorator( + new Memory(), + new Dummy(['class' => 'pattern']) + ); + + $pattern = $patternGeneratorDecorator->create('class'); + + $this->assertSame('pattern', $pattern); + } +} diff --git a/test/Unit/Builder/Blueprint/Factory/CodeGenerator/Store/InMemoryTest.php b/test/Unit/Builder/Blueprint/Factory/CodeGenerator/Store/InMemoryTest.php new file mode 100644 index 0000000..0f6cf46 --- /dev/null +++ b/test/Unit/Builder/Blueprint/Factory/CodeGenerator/Store/InMemoryTest.php @@ -0,0 +1,57 @@ +save('SomeClass', 'blueprint'); + + $this->assertSame('blueprint', $store->store()['SomeClass']); + } + + /** @test */ + public function iCanGetSavedBlueprintInMemory(): void + { + $store = new Memory([ + 'SomeClass' => 'return function() { return \'some string\'; };', + ]); + + $function = $store->get('SomeClass'); + + $this->assertSame( + 'return function() { return \'some string\'; };', + $function + ); + } + + /** @test */ + public function whenFileExistsInMemoryThenOverrideIt(): void + { + $store = new Memory([ + 'SomeClass' => 'some string', + ]); + + $store->save('SomeClass', 'override'); + + $memoryStore = $store->store(); + $this->assertSame('override', reset($memoryStore)); + } + + /** @test */ + public function whenFileDoesNotExistInMemoryThenReturnNull(): void + { + $store = new Memory(); + + $function = $store->get('SomeClass'); + + $this->assertNull($function); + } +} diff --git a/test/Unit/Builder/Blueprint/Factory/CodeGeneratorTest.php b/test/Unit/Builder/Blueprint/Factory/CodeGeneratorTest.php new file mode 100644 index 0000000..db5674b --- /dev/null +++ b/test/Unit/Builder/Blueprint/Factory/CodeGeneratorTest.php @@ -0,0 +1,38 @@ + 'return 123;', + ]) + ); + + $this->expectException(BuildingError::class); + + $codeGenerator->create('someClass'); + } + + /** @test */ + public function whenCreatedBlueprintIsCallableThenReturnIt(): void + { + $codeGenerator = new CodeGenerator( + new CodeGenerator\Pattern\Generator\Dummy([ + 'someClass' => 'return function() { return 123; };', + ]) + ); + + $blueprint = $codeGenerator->create('someClass'); + + $this->assertSame(123, $blueprint()); + } +} diff --git a/test/Unit/Builder/Blueprint/Factory/Simple.php b/test/Unit/Builder/Blueprint/Factory/Simple.php new file mode 100644 index 0000000..1d0654f --- /dev/null +++ b/test/Unit/Builder/Blueprint/Factory/Simple.php @@ -0,0 +1,20 @@ +blueprint = $blueprint; + } + + public function create(string $class): callable + { + return $this->blueprint; + } +} diff --git a/test/unit/Builder/ParameterNameStrategy/SimpleTest.php b/test/Unit/Builder/ParameterNameStrategy/SimpleTest.php similarity index 86% rename from test/unit/Builder/ParameterNameStrategy/SimpleTest.php rename to test/Unit/Builder/ParameterNameStrategy/SimpleTest.php index a288125..d81d437 100644 --- a/test/unit/Builder/ParameterNameStrategy/SimpleTest.php +++ b/test/Unit/Builder/ParameterNameStrategy/SimpleTest.php @@ -1,6 +1,6 @@ -isFulfilled($string); @@ -30,7 +30,7 @@ public function simpleStrategyReturnTrueForStringsWithoutUnderscoreMinusAndSpace * @test * @dataProvider invalidStrings */ - public function simpleStrategyReturnFalseForStringsWitUnderscoreOrMinusOrSpace(string $string) + public function simpleStrategyReturnFalseForStringsWitUnderscoreOrMinusOrSpace(string $string): void { $isFulfilled = static::$strategy->isFulfilled($string); @@ -38,7 +38,7 @@ public function simpleStrategyReturnFalseForStringsWitUnderscoreOrMinusOrSpace(s } /** @test */ - public function simpleStrategyReturnGivenParameterWithoutModification() + public function simpleStrategyReturnGivenParameterWithoutModification(): void { $string = static::$strategy->getName('simpleCamelCase'); diff --git a/test/unit/Builder/ParameterNameStrategy/SnakeCaseTest.php b/test/Unit/Builder/ParameterNameStrategy/SnakeCaseTest.php similarity index 84% rename from test/unit/Builder/ParameterNameStrategy/SnakeCaseTest.php rename to test/Unit/Builder/ParameterNameStrategy/SnakeCaseTest.php index ac8ec64..fc557fe 100644 --- a/test/unit/Builder/ParameterNameStrategy/SnakeCaseTest.php +++ b/test/Unit/Builder/ParameterNameStrategy/SnakeCaseTest.php @@ -1,9 +1,8 @@ -isFulfilled($string); @@ -31,7 +30,7 @@ public function snakeCaseStrategyReturnTrueForStringsOnlyInSneakCaseFormat(strin * @test * @dataProvider invalidStrings */ - public function snakeCaseStrategyReturnFalseForStringsWitUnderscoreOrSpace(string $string) + public function snakeCaseStrategyReturnFalseForStringsWitUnderscoreOrSpace(string $string): void { $isFulfilled = static::$strategy->isFulfilled($string); @@ -39,7 +38,7 @@ public function snakeCaseStrategyReturnFalseForStringsWitUnderscoreOrSpace(strin } /** @test */ - public function snakeCaseStrategyReturnGivenParameterAsCamelCase() + public function snakeCaseStrategyReturnGivenParameterAsCamelCase(): void { $string = static::$strategy->getName('simple_snake_case'); diff --git a/test/Unit/Builder/PhpDocParser/PhpStanTest.php b/test/Unit/Builder/PhpDocParser/PhpStanTest.php new file mode 100644 index 0000000..ec2695a --- /dev/null +++ b/test/Unit/Builder/PhpDocParser/PhpStanTest.php @@ -0,0 +1,216 @@ +create(ParserFactory::PREFER_PHP7, new Emulative([ + 'usedAttributes' => ['comments', 'startLine', 'endLine', 'startFilePos', 'endFilePos'], + ])) + ); + } + + /** + * @test + * @dataProvider commentsWithObjectList + */ + public function whenPhpDockContainListOfObjectThenReturnTrue(string $doc): void + { + $containsListOfObjects = self::$parser->isListOfObject($doc, 'list'); + + $this->assertTrue($containsListOfObjects); + } + + /** + * @test + * @dataProvider commentsWithoutObjectList + */ + public function whenPhpDockDoesNotContainListOfObjectThenReturnFalse(string $doc): void + { + $containsListOfObjects = self::$parser->isListOfObject($doc, 'list'); + + $this->assertFalse($containsListOfObjects); + } + + /** + * @test + * @dataProvider commentsWithScalarList + */ + public function whenPhpDockContainListOfScalarInsteadObjectThenReturnFalse(string $doc): void + { + $containsListOfObjects = self::$parser->isListOfObject($doc, 'list'); + + $this->assertFalse($containsListOfObjects); + } + + /** + * @test + * @dataProvider commentsWithDifferentObjectListDeclaration + */ + public function returnObjectClassOfObjectList( + string $doc, + ReflectionParameter $param, + string $expectedClass + ): void { + $class = self::$parser->getListType($doc, $param); + + $this->assertSame($expectedClass, $class); + } + + /** @test */ + public function throwExceptionWhenParameterIsNotDeclaredInPhpDoc(): void + { + $this->expectException(BuildingError::class); + $paramReflection = new class extends ReflectionParameter + { + public function __construct() + { + } + + public function getName(): string + { + return 'unexistedName'; + } + }; + + self::$parser->getListType( + '/** @param SimpleObject[] $list */', + $paramReflection + ); + } + + /** @return string[][] */ + public function commentsWithObjectList(): array + { + return [ + 'only param' => [ + '/** @param SimpleObject[] $list */', + ], + 'multi params' => [ + '/** + * @param string[] $strings + * @param int $int + * @param SimpleObject[] $list + * @param SimpleObjectTwo[] $collection + */', + ], + 'param and return' => [ + '/** + * @param SimpleObject[] $list + * @return SimpleObject[] + */', + ], + ]; + } + + /** @return string[][] */ + public function commentsWithoutObjectList(): array + { + return [ + 'only return' => [ + '/** @return SimpleObject[] */', + ], + 'multi params' => [ + '/** + * @param string[] $strings + * @param int $int + * @param SimpleObjectTwo[] $collection + */', + ], + 'param and return' => [ + '/** + * @param SimpleObject[] $collection + * @return SimpleObject[] + */', + ], + ]; + } + + /** @return string[][] */ + public function commentsWithScalarList(): array + { + return [ + 'string' => [ + '/** @param string[] $list */', + ], + 'int' => [ + '/** @param int[] $list */', + ], + 'bool' => [ + '/** @param bool[] $list */', + ], + 'float' => [ + '/** @param float[] $list */', + ], + 'double' => [ + '/** @param double[] $list */', + ], + 'mixed' => [ + '/** @param mixed[] $list */', + ], + ]; + } + + /** @return mixed[][] */ + public function commentsWithDifferentObjectListDeclaration(): array + { + $constructors = [ + 'FQCN' => (new ReflectionClass( + WithoutUseButWithFQNTypedArrayConstructor::class + ))->getConstructor(), + 'with use statement' => (new ReflectionClass( + WithUseStmtConstructor::class + ))->getConstructor(), + 'without use statement in same namespace' => (new ReflectionClass( + WithoutUseStmtConstructor::class + ))->getConstructor(), + ]; + + return [ + 'FQCN' => [ + $constructors['FQCN']->getDocComment(), + $constructors['FQCN']->getParameters()[0], + '\RstGroup\ObjectBuilder\Test\Object\EmptyConstructor', + ], + 'with use statement' => [ + $constructors['with use statement']->getDocComment(), + $constructors['with use statement']->getParameters()[0], + '\RstGroup\ObjectBuilder\Test\Object\SecondEmptyConstructor', + ], + 'without use statement in same namespace' => [ + $constructors['without use statement in same namespace']->getDocComment(), + $constructors['without use statement in same namespace']->getParameters()[0], + '\RstGroup\ObjectBuilder\Test\Object\Collection\ScalarConstructor', + ], +// TODO +// 'partial namespace with use statement' => [ +// ], +// 'partial namespace without use statement' => [ +// ], + ]; + } +} diff --git a/test/unit/Builder/ReflectionTest.php b/test/unit/Builder/ReflectionTest.php deleted file mode 100644 index 0b48bce..0000000 --- a/test/unit/Builder/ReflectionTest.php +++ /dev/null @@ -1,275 +0,0 @@ - 'some string', - 'someInt' => 999, - ]; - $class = SimpleScalarConstructor::class; - - /** @var SimpleScalarConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(SimpleScalarConstructor::class, $object); - $this->assertSame('some string', $object->someString); - $this->assertSame(999, $object->someInt); - } - - /** @test */ - public function iCanBuildSimpleObjectWithScalarAndObjectValuesInConstructor() - { - $data = [ - 'someString' => 'some string', - 'someInt' => 999, - 'someObject' => [], - ]; - $class = SimpleMixedConstructor::class; - - /** @var SimpleMixedConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(SimpleMixedConstructor::class, $object); - $this->assertSame('some string', $object->someString); - $this->assertSame(999, $object->someInt); - $this->assertInstanceOf(SomeObjectWithEmptyConstructor::class, $object->someObject); - } - - /** @test */ - public function iCanBuildSimpleObjectWithDefaultValuesInConstructor() - { - $data = [ - 'someObject' => [], - ]; - $class = SimpleMixedConstructorWithDefaultValue::class; - - /** @var SimpleMixedConstructorWithDefaultValue $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(SimpleMixedConstructorWithDefaultValue::class, $object); - $this->assertSame('some string', $object->someString); - $this->assertSame(999, $object->someInt); - $this->assertInstanceOf(SomeObjectWithEmptyConstructor::class, $object->someObject); - } - - /** @test */ - public function iCanBuildObjectWithObjectCollectionWithoutUseInConstructor() - { - $data = [ - 'list' => [ - [ - 'someString' => 'some string1', - 'someInt' => 1, - ], - [ - 'someString' => 'some string2', - 'someInt' => 2, - ], - ], - ]; - $class = ListOfObjectsWithoutUseStmtConstructor::class; - - /** @var ListOfObjectsWithoutUseStmtConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(ListOfObjectsWithoutUseStmtConstructor::class, $object); - $this->assertCount(2, $object->list); - foreach($object->list as $element) { - $this->assertInstanceOf(SimpleScalarConstructor::class, $element); - } - } - - /** @test */ - public function iCanBuildObjectWithObjectCollectionWithUseInConstructor() - { - $data = [ - 'list' => [ - [], - [], - ], - ]; - $class = ListOfObjectsWithUseStmtConstructor::class; - - /** @var ListOfObjectsWithUseStmtConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(ListOfObjectsWithUseStmtConstructor::class, $object); - $this->assertCount(2, $object->list); - foreach($object->list as $element) { - $this->assertInstanceOf(SomeSecondObject::class, $element); - } - } - - /** @test */ - public function iCanBuildObjectWithObjectCollectionWithoutUseButWithFQNTypedArrayInConstructor() - { - $data = [ - 'list' => [ - [], - [], - ], - ]; - $class = ListOfObjectsWithoutUseButWithFQNTypedArrayConstructor::class; - - /** @var ListOfObjectsWithoutUseButWithFQNTypedArrayConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(ListOfObjectsWithoutUseButWithFQNTypedArrayConstructor::class, $object); - $this->assertCount(2, $object->list); - foreach($object->list as $element) { - $this->assertInstanceOf(SomeObject::class, $element); - } - } - - /** @test */ - public function iCanBuildObjectWithScalarCollectionTypedArrayInConstructor() - { - $data = [ - 'list1' => ['str', 'str'], - 'list2' => ['str', 'str'], - ]; - $class = ListOfObjectsWithScalarTypedArrayConstructor::class; - - /** @var ListOfObjectsWithScalarTypedArrayConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(ListOfObjectsWithScalarTypedArrayConstructor::class, $object); - $this->assertCount(2, $object->list1); - $this->assertCount(2, $object->list2); - foreach($object->list1 as $element) { - $this->assertSame('str', $element); - } - foreach($object->list2 as $element) { - $this->assertSame('str', $element); - } - } - - /** @test */ - public function iCanBuildObjectWithBothScalarAndObjectCollectionTypedArrayInConstructor() - { - $data = [ - 'list1' => ['str', 'str'], - 'list2' => [ - [ - 'someString' => 'some string1', - 'someInt' => 1, - ], - [ - 'someString' => 'some string2', - 'someInt' => 2, - ], - ], - ]; - $class = ListOfObjectsWithScalarTypedArrayAndObjectListConstructor::class; - - /** @var ListOfObjectsWithScalarTypedArrayAndObjectListConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(ListOfObjectsWithScalarTypedArrayAndObjectListConstructor::class, $object); - $this->assertCount(2, $object->list1); - $this->assertCount(2, $object->list2); - foreach($object->list1 as $element) { - $this->assertSame('str', $element); - } - foreach($object->list2 as $element) { - $this->assertInstanceOf(SimpleScalarConstructor::class, $element); - } - } - - /** @test */ - public function iCanBuildAdvancedObjectHierarchy() - { - $data = [ - 'someString' => 'some string', - 'simpleObject1' => [ - 'someString' => 'some string', - 'someInt' => 1, - ], - 'simpleObject2' => [ - 'someString' => 'some string', - 'someInt' => 2, - 'someObject' => [], - ], - 'someInt' => 3, - ]; - $class = SomeAggregateRoot::class; - - /** @var SomeAggregateRoot $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(SomeAggregateRoot::class, $object); - $this->assertSame('some string', $object->someString); - $this->assertSame(3, $object->someInt); - $this->assertInstanceOf(SimpleScalarConstructor::class, $object->simpleObject1); - $this->assertInstanceOf(SimpleMixedConstructor::class, $object->simpleObject2); - $this->assertSame(1, $object->simpleObject1->someInt); - $this->assertSame(2, $object->simpleObject2->someInt); - } - - /** @test */ - public function iCanBuildObjectWithNullableParameterWithoutDefaultValue() - { - $data = [ - 'someString1' => 'some string1', - 'someString2' => 'some string2', - ]; - $class = SimpleNullableConstructor::class; - - /** @var SimpleNullableConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(SimpleNullableConstructor::class, $object); - $this->assertSame('some string1', $object->someString1); - $this->assertSame('some string2', $object->someString2); - $this->assertNull($object->someInt); - } - - /** @test */ - public function iCanBuildObjectWithNullableParameterWithHimValueValue() - { - $data = [ - 'someString1' => 'some string1', - 'someInt' => 123, - 'someString2' => 'some string2', - ]; - $class = SimpleNullableConstructor::class; - - /** @var SimpleNullableConstructor $object */ - $object = static::$builder->build($class, $data); - - $this->assertInstanceOf(SimpleNullableConstructor::class, $object); - $this->assertSame('some string1', $object->someString1); - $this->assertSame('some string2', $object->someString2); - $this->assertSame(123, $object->someInt); - } -}