diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5ce5d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.idea +.DS_Store +bin +build +composer.lock +composer.phar +vendor diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..921d293 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,29 @@ +language: php +cache: + directories: + - vendor +php: + - 5.6 + - 5.5 + - nightly + +matrix: + fast_finish: true + allow_failures: + - php: nightly + +before_install: + - /home/travis/.phpenv/versions/$(phpenv version-name)/bin/composer self-update + - sh -c "sudo mkdir vendor" + - sh -c "sudo mount -t tmpfs -o size=512M tmpfs vendor" + +before_script: + - alias composer="php -d zend.enable_gc=0 /usr/bin/composer" + - composer install + +script: + - bin/phpunit --coverage-text + +matrix: + allow_failures: + - php: nightly diff --git a/README.md b/README.md new file mode 100644 index 0000000..09cb08b --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# JimmyOak Utilities + +[![Build Status](https://travis-ci.org/jimmyoak/utilities.svg?branch=master)](https://travis-ci.org/jimmyoak/utilities) +[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/jimmyoak/utilities/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/jimmyoak/utilities/?branch=master) diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..9011084 --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "jimmyoak/arrayed-object", + "authors": [ + { + "name": "Adrián Robles", + "email": "adrian.robles.maiz@gmail.com", + "homepage": "http://github.com/jimmyoak" + } + ], + "license": "MIT", + "keywords": ["arrayed", "object", "instantiator", "jimmy", "jimmyoak"], + "require": { + "php": ">=5.5.0", + "jimmyoak/utilities": "2.*" + }, + "require-dev": { + "phpunit/phpunit": ">=3.7.0" + }, + "autoload": { + "psr-4": { + "JimmyOak\\ArrayedObject\\": "src/ArrayedObject/" + } + }, + "autoload-dev": { + "psr-4": { + "JimmyOak\\Test\\ArrayedObject\\": "test/ArrayedObject/" + } + }, + "config": { + "bin-dir": "bin/" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..a49b16a --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,46 @@ + + + + + + + + + + + + + ./test + + + + + + ./ + + ./vendor/ + + + + + + + + + + \ No newline at end of file diff --git a/src/ArrayedObject/ArrayedObject.php b/src/ArrayedObject/ArrayedObject.php new file mode 100644 index 0000000..e8eb562 --- /dev/null +++ b/src/ArrayedObject/ArrayedObject.php @@ -0,0 +1,30 @@ +class = $class; + $this->collection = $data; + } + + /** + * @return string + */ + public function getClass() + { + return $this->class; + } + + public function getData() + { + return $this->collection; + } +} diff --git a/src/ArrayedObject/ArrayedObjectFactory.php b/src/ArrayedObject/ArrayedObjectFactory.php new file mode 100644 index 0000000..8822d64 --- /dev/null +++ b/src/ArrayedObject/ArrayedObjectFactory.php @@ -0,0 +1,56 @@ +arrayObjectVars($object)); + } + + private function arrayObjectVars($object) + { + $arrayedObjectFactory = $this; + + $valueProcessor = function ($value) use (&$valueProcessor, $arrayedObjectFactory) { + if (is_object($value)) { + $value = $arrayedObjectFactory->create($value); + } elseif (is_array($value)) { + foreach ($value as &$innerValue) { + $innerValue = $valueProcessor($innerValue); + } + } + + return $value; + }; + + $getObjectVarsClosure = function () use ($valueProcessor) { + return array_map($valueProcessor, get_object_vars($this)); + }; + + $vars = []; + $class = get_class($object); + do { + $bindedGetObjectVarsClosure = \Closure::bind($getObjectVarsClosure, $object, $class); + $vars = array_merge($vars, $bindedGetObjectVarsClosure()); + } while ($class = get_parent_class($class)); + + return $vars; + } +} diff --git a/src/ArrayedObject/ArrayedObjectInstantiator.php b/src/ArrayedObject/ArrayedObjectInstantiator.php new file mode 100644 index 0000000..c0a391b --- /dev/null +++ b/src/ArrayedObject/ArrayedObjectInstantiator.php @@ -0,0 +1,156 @@ + 'createDateTimeInterfaceInstance', + \DateInterval::class => 'createDateIntervalInstance', + ]; + + private function __construct() + { + } + + public static function instance() + { + static $instance; + + if (null === $instance) { + $instance = new static(); + } + + return $instance; + } + + /** + * @param ArrayedObject $arrayedObject + * + * @return mixed + */ + public function instantiate(ArrayedObject $arrayedObject) + { + if ($this->isSpecialCase($arrayedObject)) { + return $this->createSpecialCaseInstance($arrayedObject); + } + + $instance = $this->createClassInstance($arrayedObject); + + $this->fillClassInstance($instance, $arrayedObject->getData()); + + return $instance; + } + + private function createClassInstance(ArrayedObject $arrayedObject) + { + $instance = (new \ReflectionClass($arrayedObject->getClass()))->newInstanceWithoutConstructor(); + + return $instance; + } + + private function fillClassInstance($instance, $data) + { + $instantiator = $this; + $valueProcessor = function ($value) use (&$valueProcessor, $instantiator) { + if ($value instanceof ArrayedObject) { + $value = $instantiator->instantiate($value); + } + + if (is_array($value)) { + foreach ($value as &$innerValue) { + $innerValue = $valueProcessor($innerValue); + } + } + + return $value; + }; + + $setObjectVarsClosure = function ($data, $class, &$valueProcessor) { + foreach ($data as $property => $value) { + if (property_exists($class, $property)) { + $value = $valueProcessor($value); + $this->$property = $value; + } + } + }; + + $class = get_class($instance); + do { + $bindedSetObjectVarsClosure = \Closure::bind($setObjectVarsClosure, $instance, $class); + $bindedSetObjectVarsClosure($data, $class, $valueProcessor); + } while ($class = get_parent_class($class)); + } + + private function getSpecialCaseClass(ArrayedObject $arrayedObject) + { + $class = $arrayedObject->getClass(); + $reflectionClass = new \ReflectionClass($class); + foreach ($this->specialCasesClasses as $specialCaseClass) { + if ($reflectionClass->isSubclassOf($specialCaseClass) || $class === $specialCaseClass) { + return $specialCaseClass; + } + } + + return null; + } + + private function isSpecialCase(ArrayedObject $arrayedObject) + { + return (bool) $this->getSpecialCaseClass($arrayedObject); + } + + private function createSpecialCaseInstance(ArrayedObject $arrayedObject) + { + $specialCaseClass = $this->getSpecialCaseClass($arrayedObject); + $functionName = $this->specialCaseClassFunctionMap[$specialCaseClass]; + + return $this->$functionName($arrayedObject); + } + + private function createDateTimeInterfaceInstance(ArrayedObject $arrayedObject) + { + try { + $instance = (new \ReflectionClass($arrayedObject->getClass()))->newInstanceWithoutConstructor(); + } catch (\ReflectionException $e) { + $instance = (new \ReflectionClass($arrayedObject->getClass()))->newInstance(); + } + + $reflectionClass = new \ReflectionClass(\DateTime::class); + $dateTimeConstructor = $reflectionClass->getConstructor(); + + $data = $arrayedObject->getData(); + $date = isset($data['date']) ? $data['date'] : null; + $dateTimeZone = isset($data['timezone']) ? $data['timezone'] : null; + $dateTimeConstructor->invokeArgs($instance, [$date, new \DateTimeZone($dateTimeZone)]); + + return $instance; + } + + private function createDateIntervalInstance(ArrayedObject $arrayedObject) + { + $constructionData = ['P0D']; + try { + $instance = (new \ReflectionClass($arrayedObject->getClass()))->newInstanceWithoutConstructor(); + + $reflectionClass = new \ReflectionClass($instance); + + $dateIntervalConstructor = $reflectionClass->getConstructor(); + $dateIntervalConstructor->invokeArgs($instance, $constructionData); + } catch (\ReflectionException $e) { + $instance = (new \ReflectionClass($arrayedObject->getClass()))->newInstanceArgs($constructionData); + } + + $data = $arrayedObject->getData(); + foreach ($data as $property => $value) { + $instance->$property = $value; + } + + return $instance; + } +} diff --git a/test/ArrayedObject/ArrayedObjectFactoryTest.php b/test/ArrayedObject/ArrayedObjectFactoryTest.php new file mode 100644 index 0000000..edb6c47 --- /dev/null +++ b/test/ArrayedObject/ArrayedObjectFactoryTest.php @@ -0,0 +1,72 @@ +factory = ArrayedObjectFactory::instance(); + } + + + /** @test */ + public function shouldBeSingleton() + { + $isConstructorCallable = is_callable([$this->factory, '__construct']); + + $this->assertFalse($isConstructorCallable); + } + + /** @test */ + public function shouldCreateArrayedObjectFromObject() + { + $expectedData = $this->getExpectedData(); + $object = new DummyClass(); + + $arrayedObject = $this->factory->create($object); + + $this->assertInstanceOf(ArrayedObject::class, $arrayedObject); + $this->assertSame(DummyClass::class, $arrayedObject->getClass()); + $this->assertEquals($expectedData, $arrayedObject->getData()); + } + + private function getExpectedData() + { + $expectedData = [ + 'aPrivateProperty' => 'A STRING', + 'aProtectedProperty' => 1234, + 'aPublicProperty' => 'ANOTHER STRING', + 'anObject' => + new ArrayedObject(AnotherDummyClass::class, [ + 'aValue' => 'Jimmy', + 'anotherValue' => 'Kane', + 'oneMoreValue' => 'Oak', + ]), + 'anArrayOfObjects' => + array( + new ArrayedObject(AnotherDummyClass::class, [ + 'aValue' => 'Jimmy', + 'anotherValue' => 'Kane', + 'oneMoreValue' => 'Oak', + ]), + new ArrayedObject(AnotherDummyClass::class, [ + 'aValue' => 'Jimmy', + 'anotherValue' => 'Kane', + 'oneMoreValue' => 'Oak', + ]), + ), + 'aParentProperty' => 5, + ]; + + return $expectedData; + } +} diff --git a/test/ArrayedObject/ArrayedObjectInstantiatorTest.php b/test/ArrayedObject/ArrayedObjectInstantiatorTest.php new file mode 100644 index 0000000..d10944f --- /dev/null +++ b/test/ArrayedObject/ArrayedObjectInstantiatorTest.php @@ -0,0 +1,141 @@ +instantiator = ArrayedObjectInstantiator::instance(); + } + + /** @test */ + public function shouldBeSingleton() + { + $isConstructorCallable = is_callable([$this->instantiator, '__construct']); + + $this->assertFalse($isConstructorCallable); + } + + /** @test */ + public function shouldCreateAndFillDateTimeInstance() + { + $dateTimeData = [ + 'date' => '1992-10-07 21:05:00.000000', + 'timezone_type' => 3, + 'timezone' => 'Europe/Madrid', + ]; + $expected = new \DateTime($dateTimeData['date'], new \DateTimeZone($dateTimeData['timezone'])); + + $arrayedObject = new ArrayedObject(\DateTime::class, $dateTimeData); + + $instance = $this->instantiator->instantiate($arrayedObject); + + $this->assertEquals($expected, $instance); + } + + /** @test */ + public function shouldCreateAndFillDateIntervalInstance() + { + $dateIntervalData = [ + 'y' => 0, + 'm' => 0, + 'd' => 0, + 'h' => 1, + 'i' => 25, + 's' => 0, + 'weekday' => 0, + 'weekday_behavior' => 0, + 'first_last_day_of' => 0, + 'invert' => 0, + 'days' => false, + 'special_type' => 0, + 'special_amount' => 0, + 'have_weekday_relative' => 0, + 'have_special_relative' => 0, + ]; + + $expected = new \DateInterval('PT1H25M'); + + $arrayedObject = new ArrayedObject(\DateInterval::class, $dateIntervalData); + + $instance = $this->instantiator->instantiate($arrayedObject); + + $this->assertEquals($expected, $instance); + } + + /** @test */ + public function shouldFillObjectProperties() + { + $class = DummyClass::class; + $data = [ + 'aPrivateProperty' => 'PRIDUMMY', + 'aProtectedProperty' => 'PRODUMMY', + 'aPublicProperty' => 'PUBDUMMY', + 'aParentProperty' => 'PARDUMMY', + ]; + + $arrayedObject = new ArrayedObject($class, $data); + /** @var DummyClass $instance */ + $instance = $this->instantiator->instantiate($arrayedObject); + + $this->assertSame($data['aPrivateProperty'], $instance->getAPrivateProperty()); + $this->assertSame($data['aProtectedProperty'], $instance->getAProtectedProperty()); + $this->assertSame($data['aPublicProperty'], $instance->getAPublicProperty()); + $this->assertSame($data['aParentProperty'], $instance->getAParentProperty()); + } + + /** @test */ + public function shouldInstantiateRecursiveArrayedObjects() + { + $class = DummyClass::class; + $anArrayOfObjects = [ + new ArrayedObject(DummyClass::class, [ + 'aPrivateProperty' => 'DUMMYPRI', + 'aProtectedProperty' => 'DUMMYPRO', + 'aPublicProperty' => 'DUMMYPUB', + ]) + ]; + $data = [ + 'aPrivateProperty' => 'PRIDUMMY', + 'aProtectedProperty' => 'PRODUMMY', + 'aPublicProperty' => 'PUBDUMMY', + 'aParentProperty' => 'PARDUMMY', + 'anObject' => new ArrayedObject($class, [ + 'aPrivateProperty' => 'CHIPRIDUMMY' + ]), + 'anArrayOfObjects' => $anArrayOfObjects + ]; + + $arrayedObject = new ArrayedObject($class, $data); + /** @var DummyClass $instance */ + $instance = $this->instantiator->instantiate($arrayedObject); + + $this->assertInstanceOf($class, $instance); + $this->assertSame($data['aPrivateProperty'], $instance->getAPrivateProperty()); + $this->assertSame($data['aProtectedProperty'], $instance->getAProtectedProperty()); + $this->assertSame($data['aPublicProperty'], $instance->getAPublicProperty()); + $this->assertSame($data['aParentProperty'], $instance->getAParentProperty()); + + /** @var DummyClass $anObject */ + $anObject = $instance->getAnObject(); + $this->assertInstanceOf($class, $anObject); + $this->assertSame($data['anObject']['aPrivateProperty'], $anObject->getAPrivateProperty()); + + /** @var DummyClass $anObjectOfTheArrayOfObjects */ + $anObjectOfTheArrayOfObjects = $instance->getAnArrayOfObjects()[0]; + $this->assertInstanceOf($class, $anObjectOfTheArrayOfObjects); + $this->assertSame($anArrayOfObjects[0]['aPrivateProperty'], + $anObjectOfTheArrayOfObjects->getAPrivateProperty()); + $this->assertSame($anArrayOfObjects[0]['aProtectedProperty'], + $anObjectOfTheArrayOfObjects->getAProtectedProperty()); + $this->assertSame($anArrayOfObjects[0]['aPublicProperty'], $anObjectOfTheArrayOfObjects->getAPublicProperty()); + } +} diff --git a/test/ArrayedObject/ArrayedObjectTest.php b/test/ArrayedObject/ArrayedObjectTest.php new file mode 100644 index 0000000..49583c7 --- /dev/null +++ b/test/ArrayedObject/ArrayedObjectTest.php @@ -0,0 +1,23 @@ +assertSame($class, $arrayedObject->getClass()); + $this->assertSame($data, $arrayedObject->getData()); + } +} diff --git a/test/ArrayedObject/Value/AnotherDummyClass.php b/test/ArrayedObject/Value/AnotherDummyClass.php new file mode 100644 index 0000000..37654b4 --- /dev/null +++ b/test/ArrayedObject/Value/AnotherDummyClass.php @@ -0,0 +1,10 @@ +anObject = new AnotherDummyClass(); + $this->anArrayOfObjects = [ + $this->anObject, + $this->anObject + ]; + } + + /** + * @return string + */ + public function getAPrivateProperty() + { + return $this->aPrivateProperty; + } + + /** + * @param string $aPrivateProperty + * + * @return $this + */ + public function setAPrivateProperty($aPrivateProperty) + { + $this->aPrivateProperty = $aPrivateProperty; + return $this; + } + + /** + * @return int + */ + public function getAProtectedProperty() + { + return $this->aProtectedProperty; + } + + /** + * @param int $aProtectedProperty + * + * @return $this + */ + public function setAProtectedProperty($aProtectedProperty) + { + $this->aProtectedProperty = $aProtectedProperty; + return $this; + } + + /** + * @return string + */ + public function getAPublicProperty() + { + return $this->aPublicProperty; + } + + /** + * @param string $aPublicProperty + * + * @return $this + */ + public function setAPublicProperty($aPublicProperty) + { + $this->aPublicProperty = $aPublicProperty; + return $this; + } + + /** + * @return AnotherDummyClass + */ + public function getAnObject() + { + return $this->anObject; + } + + /** + * @param AnotherDummyClass $anObject + * + * @return $this + */ + public function setAnObject($anObject) + { + $this->anObject = $anObject; + return $this; + } + + /** + * @return array + */ + public function getAnArrayOfObjects() + { + return $this->anArrayOfObjects; + } + + /** + * @param array $anArrayOfObjects + * + * @return $this + */ + public function setAnArrayOfObjects($anArrayOfObjects) + { + $this->anArrayOfObjects = $anArrayOfObjects; + return $this; + } +} diff --git a/test/ArrayedObject/Value/ParentDummyClass.php b/test/ArrayedObject/Value/ParentDummyClass.php new file mode 100644 index 0000000..689234c --- /dev/null +++ b/test/ArrayedObject/Value/ParentDummyClass.php @@ -0,0 +1,27 @@ +aParentProperty; + } + + /** + * @param int $aParentProperty + * + * @return $this + */ + public function setAParentProperty($aParentProperty) + { + $this->aParentProperty = $aParentProperty; + return $this; + } +}