├── src ├── Exception │ ├── ExceptionInterface.php │ ├── OptionNotSetException.php │ ├── InvalidClassException.php │ └── InvalidOptionsException.php ├── Options.php ├── ParameterAnalysis.php └── Cascader.php ├── CONTRIBUTING.md ├── CHANGELOG.md ├── examples ├── passing-concrete-class-name-for-object-arguments.php ├── nested-objects-creation.php └── simple-object-creation.php ├── LICENSE ├── composer.json └── README.md /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | getDeclaringClass(), 22 | $parameterAnalysis->getName() 23 | )); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /examples/passing-concrete-class-name-for-object-arguments.php: -------------------------------------------------------------------------------- 1 | test = $test; 16 | } 17 | } 18 | 19 | interface TestInterface 20 | { 21 | public function foo(); 22 | } 23 | 24 | class Test implements TestInterface 25 | { 26 | public $bar; 27 | 28 | public $baz; 29 | 30 | public function __construct(string $bar, int $baz = 3) 31 | { 32 | $this->bar = $bar; 33 | $this->baz = $baz; 34 | } 35 | 36 | public function foo() 37 | { 38 | } 39 | } 40 | 41 | $cascader = new Cascader(); 42 | 43 | $object = $cascader->create(RootObject::class, [ 44 | 'test' => [ 45 | '__class__' => Test::class, 46 | 'bar' => 'test', 47 | 'baz' => 10, 48 | ], 49 | ]); 50 | var_dump($object); 51 | -------------------------------------------------------------------------------- /examples/nested-objects-creation.php: -------------------------------------------------------------------------------- 1 | subObject = $subObject; 18 | $this->data = $data; 19 | } 20 | } 21 | 22 | class SubObject 23 | { 24 | public $name; 25 | 26 | public $count; 27 | 28 | public $isActive; 29 | 30 | public function __construct(string $name, int $count = 3, $isActive = false) 31 | { 32 | $this->name = $name; 33 | $this->count = $count; 34 | $this->isActive = $isActive; 35 | } 36 | } 37 | 38 | $cascader = new Cascader(); 39 | 40 | $object = $cascader->create(RootObject::class, [ 41 | 'sub_object' => [ 42 | 'name' => 'test', 43 | 'count' => 10, 44 | 'is_active' => true, 45 | ], 46 | 'data' => [ 47 | 'foo' => 'bar', 48 | ], 49 | ]); 50 | var_dump($object); 51 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 Nikola Posa 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /examples/simple-object-creation.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | $this->count = $count; 21 | $this->isActive = $isActive; 22 | } 23 | } 24 | 25 | $cascader = new Cascader(); 26 | 27 | $obj1 = $cascader->create(Foo::class, [ 28 | 'name' => 'foo1', 29 | 'count' => 10, 30 | 'isActive' => true, 31 | ]); 32 | var_dump($obj1); 33 | 34 | //option keys automatically normalized to camel-case 35 | $obj2 = $cascader->create(Foo::class, [ 36 | 'name' => 'foo2', 37 | 'count' => 10, 38 | 'is_active' => true, 39 | ]); 40 | var_dump($obj2); 41 | 42 | //random order of parameters 43 | $obj3 = $cascader->create(Foo::class, [ 44 | 'count' => 1, 45 | 'is_active' => true, 46 | 'name' => 'foo3', 47 | ]); 48 | var_dump($obj3); 49 | 50 | //optional parameters can be omitted 51 | $obj4 = $cascader->create(Foo::class, [ 52 | 'name' => 'foo4', 53 | 'is_active' => true, 54 | ]); 55 | var_dump($obj4); 56 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nikolaposa/cascader", 3 | "description": "Utility for creating objects in PHP from constructor parameters definitions.", 4 | "type": "library", 5 | "license": "MIT", 6 | "keywords": [ 7 | "instantiate", 8 | "cascade", 9 | "cascade-creation", 10 | "generic", 11 | "factory" 12 | ], 13 | "authors": [ 14 | { 15 | "name": "Nikola Posa", 16 | "email": "posa.nikola@gmail.com", 17 | "homepage": "http://www.nikolaposa.in.rs/" 18 | } 19 | ], 20 | "require": { 21 | "php": "^7.2 || ^8.0" 22 | }, 23 | "require-dev": { 24 | "phpunit/phpunit": "^8.5 || ^9.4", 25 | "friendsofphp/php-cs-fixer": "^2.7" 26 | }, 27 | "autoload": { 28 | "psr-4": { 29 | "Cascader\\": "src/" 30 | } 31 | }, 32 | "autoload-dev": { 33 | "psr-4": { 34 | "Cascader\\Tests\\": "tests/" 35 | } 36 | }, 37 | "scripts": { 38 | "test": "phpunit --colors=always", 39 | "cs-fix": "php-cs-fixer fix --config=.php_cs", 40 | "cs-check": "php-cs-fixer fix --config=.php_cs -v --diff --dry-run" 41 | }, 42 | "extra": { 43 | "branch-alias": { 44 | "dev-master": "1.3.x-dev" 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Options.php: -------------------------------------------------------------------------------- 1 | options = $options; 20 | } 21 | 22 | public static function fromArray(array $options) 23 | { 24 | self::validate($options); 25 | $options = self::normalize($options); 26 | 27 | return new static($options); 28 | } 29 | 30 | final protected static function validate(array $options) 31 | { 32 | foreach ($options as $key => $value) { 33 | if (! \is_string($key)) { 34 | throw InvalidOptionsException::invalidKeys(); 35 | } 36 | } 37 | } 38 | 39 | final protected static function normalize(array $options) : array 40 | { 41 | $normalizedOptions = []; 42 | 43 | foreach ($options as $key => $value) { 44 | $normalizedOptions[$key] = $value; 45 | 46 | $normalizedKey = str_replace(' ', '', ucwords(str_replace(['_', '-'], ' ', $key))); 47 | $normalizedKey[0] = strtolower($normalizedKey[0]); 48 | $normalizedOptions[$normalizedKey] = $value; 49 | } 50 | 51 | return $normalizedOptions; 52 | } 53 | 54 | public function has(string $key) : bool 55 | { 56 | return array_key_exists($key, $this->options); 57 | } 58 | 59 | public function get(string $key) 60 | { 61 | if (! $this->has($key)) { 62 | throw OptionNotSetException::forKey($key); 63 | } 64 | 65 | return $this->options[$key]; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cascader 2 | 3 | [![Build Status][ico-build]][link-build] 4 | [![Code Quality][ico-code-quality]][link-code-quality] 5 | [![Code Coverage][ico-code-coverage]][link-code-coverage] 6 | [![Latest Version][ico-version]][link-packagist] 7 | [![PDS Skeleton][ico-pds]][link-pds] 8 | 9 | Cascader enables the creation of objects from array definitions that represent constructor parameters. Given the class name and creation options array, it will try to create a target object, also creating nested objects that may exist. Convenient as a factory for generic kind of objects. 10 | 11 | ## Installation 12 | 13 | The preferred method of installation is via [Composer](http://getcomposer.org/). Run the following command to install the latest version of a package and add it to your project's `composer.json`: 14 | 15 | ```bash 16 | composer require nikolaposa/cascader 17 | ``` 18 | 19 | ## Usage 20 | 21 | ```php 22 | $cascader = new Cascader(); 23 | 24 | $object = $cascader->create(RootObject::class, [ 25 | 'name' => 'foo', 26 | 'sub_object' => [ 27 | 'category' => 'bar', 28 | 'count' => 10, 29 | ], 30 | 'is_active' => true, 31 | ]); 32 | ``` 33 | 34 | See [more examples][link-examples]. 35 | 36 | ## Credits 37 | 38 | - [Nikola Poša][link-author] 39 | - [All Contributors][link-contributors] 40 | 41 | ## License 42 | 43 | Released under MIT License - see the [License File](LICENSE) for details. 44 | 45 | 46 | [ico-version]: https://poser.pugx.org/nikolaposa/cascader/v/stable 47 | [ico-build]: https://github.com/nikolaposa/cascader/workflows/Build/badge.svg?branch=master 48 | [ico-code-coverage]: https://scrutinizer-ci.com/g/nikolaposa/cascader/badges/coverage.png?b=master 49 | [ico-code-quality]: https://scrutinizer-ci.com/g/nikolaposa/cascader/badges/quality-score.png?b=master 50 | [ico-pds]: https://img.shields.io/badge/pds-skeleton-blue.svg 51 | 52 | [link-examples]: examples 53 | [link-packagist]: https://packagist.org/packages/nikolaposa/cascader 54 | [link-build]: https://github.com/nikolaposa/cascader/actions 55 | [link-code-coverage]: https://scrutinizer-ci.com/g/nikolaposa/cascader/code-structure 56 | [link-code-quality]: https://scrutinizer-ci.com/g/nikolaposa/cascader 57 | [link-pds]: https://github.com/php-pds/skeleton 58 | [link-author]: https://github.com/nikolaposa 59 | [link-contributors]: ../../contributors 60 | -------------------------------------------------------------------------------- /src/ParameterAnalysis.php: -------------------------------------------------------------------------------- 1 | parameter = $parameter; 41 | $this->setParameterType(); 42 | $this->setArgument($options); 43 | } 44 | 45 | public function getName() : string 46 | { 47 | return $this->parameter->getName(); 48 | } 49 | 50 | public function getType() : string 51 | { 52 | return $this->parameterTypeName; 53 | } 54 | 55 | public function hasArgument() : bool 56 | { 57 | return $this->hasArgument; 58 | } 59 | 60 | public function getArgument() 61 | { 62 | return $this->argument; 63 | } 64 | 65 | public function isOptional() : bool 66 | { 67 | return $this->parameter->isOptional(); 68 | } 69 | 70 | public function getDefaultValue() 71 | { 72 | return $this->parameter->getDefaultValue(); 73 | } 74 | 75 | public function isNestedObject() : bool 76 | { 77 | return null !== $this->parameterType && !$this->parameterType->isBuiltin() && \is_array($this->argument); 78 | } 79 | 80 | public function isArray() : bool 81 | { 82 | return 'array' === $this->parameterTypeName && \is_array($this->argument); 83 | } 84 | 85 | public function getDeclaringClass() : string 86 | { 87 | return $this->parameter->getDeclaringClass()->name; 88 | } 89 | 90 | private function setParameterType() 91 | { 92 | if (null !== ($parameterType = $this->parameter->getType())) { 93 | $this->parameterType = $parameterType; 94 | $this->parameterTypeName = method_exists($parameterType, 'getName') 95 | ? $parameterType->getName() //PHP 7.1 96 | : (string) $parameterType; 97 | } 98 | } 99 | 100 | private function setArgument(Options $options) 101 | { 102 | try { 103 | $this->argument = $options->get($this->parameter->name); 104 | $this->hasArgument = true; 105 | } catch (OptionNotSetException $ex) { 106 | $this->hasArgument = false; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Cascader.php: -------------------------------------------------------------------------------- 1 | getReflectionClass($className); 28 | $options = Options::fromArray($options); 29 | 30 | $arguments = $this->resolveArguments($reflectionClass, $options); 31 | 32 | return new $className(...$arguments); 33 | } 34 | 35 | protected function getReflectionClass(string $className) : ReflectionClass 36 | { 37 | try { 38 | $reflectionClass = new ReflectionClass($className); 39 | } catch (\ReflectionException $ex) { 40 | throw InvalidClassException::nonExistingClass($className); 41 | } 42 | 43 | if (! $reflectionClass->isInstantiable()) { 44 | throw InvalidClassException::nonInstantiableClass($className); 45 | } 46 | 47 | return $reflectionClass; 48 | } 49 | 50 | protected function resolveArguments(ReflectionClass $reflectionClass, Options $options) : array 51 | { 52 | $constructor = $reflectionClass->getConstructor(); 53 | 54 | if (null === $constructor) { 55 | return []; 56 | } 57 | 58 | $arguments = []; 59 | 60 | $constructorParameters = $constructor->getParameters(); 61 | 62 | foreach ($constructorParameters as $parameter) { 63 | $arguments[] = $this->resolveArgument($parameter, $options); 64 | } 65 | 66 | return $arguments; 67 | } 68 | 69 | protected function resolveArgument(ReflectionParameter $reflectionParameter, Options $options) 70 | { 71 | $parameterAnalysis = new ParameterAnalysis($reflectionParameter, $options); 72 | 73 | if (! $parameterAnalysis->hasArgument()) { 74 | if (! $parameterAnalysis->isOptional()) { 75 | throw InvalidOptionsException::missingMandatoryParameter($parameterAnalysis); 76 | } 77 | 78 | return $parameterAnalysis->getDefaultValue(); 79 | } 80 | 81 | $argument = $parameterAnalysis->getArgument(); 82 | 83 | if ($parameterAnalysis->isArray()) { 84 | return $this->resolveArrayArgument($argument); 85 | } 86 | 87 | if ($parameterAnalysis->isNestedObject()) { 88 | return $this->createNestedObject($parameterAnalysis->getType(), $argument); 89 | } 90 | 91 | return $argument; 92 | } 93 | 94 | protected function resolveArrayArgument(array $argument) : array 95 | { 96 | foreach ($argument as $k => $value) { 97 | if (\is_array($value) && isset($value[self::ARGUMENT_CLASS])) { 98 | $argument[$k] = $this->createNestedObject('', $value); 99 | } 100 | } 101 | 102 | return $argument; 103 | } 104 | 105 | protected function createNestedObject(string $className, array $arguments) 106 | { 107 | if (isset($arguments[self::ARGUMENT_CLASS])) { 108 | $className = $arguments[self::ARGUMENT_CLASS]; 109 | unset($arguments[self::ARGUMENT_CLASS]); 110 | } 111 | 112 | return $this->create($className, $arguments); 113 | } 114 | } 115 | --------------------------------------------------------------------------------