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);
- }
-}