diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllPrivateClassHydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllPrivateClassHydrationBench.php new file mode 100644 index 00000000..1b72b212 --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllPrivateClassHydrationBench.php @@ -0,0 +1,31 @@ +createHydrator(AllPrivateClass::class); + $this->createData(); + $this->object = new AllPrivateClass(); + } + + /** + * @Revs(100) + * @Iterations(200) + */ + public function benchConsume() : void + { + ($this->hydrator)($this->data, $this->object); + } +} diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllProtectedClassHydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllProtectedClassHydrationBench.php new file mode 100644 index 00000000..768d3043 --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllProtectedClassHydrationBench.php @@ -0,0 +1,31 @@ +createHydrator(AllProtectedClass::class); + $this->createData(); + $this->object = new AllProtectedClass(); + } + + /** + * @Revs(100) + * @Iterations(200) + */ + public function benchConsume() : void + { + ($this->hydrator)($this->data, $this->object); + } +} diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllPublicClassHydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllPublicClassHydrationBench.php new file mode 100644 index 00000000..bcc65756 --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/AllPublicClassHydrationBench.php @@ -0,0 +1,31 @@ +createHydrator(AllPublicClass::class); + $this->createData(); + $this->object = new AllPublicClass(); + } + + /** + * @Revs(100) + * @Iterations(200) + */ + public function benchConsume() : void + { + ($this->hydrator)($this->data, $this->object); + } +} diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/HydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/HydrationBench.php new file mode 100644 index 00000000..46b67b99 --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/HydrationBench.php @@ -0,0 +1,45 @@ +hydrator = (new GenerateFileCacheHydrator('build/'))($class); + } + + /** + * Populate test data array + */ + protected function createData() : void + { + $this->data = [ + 'foo' => 'some foo string', + 'bar' => 42, + 'baz' => new DateTime(), + 'someFooProperty' => [12, 13, 14], + 'someBarProperty' => 12354.4578, + 'someBazProperty' => new stdClass(), + ]; + } +} diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/InheritanceClassHydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/InheritanceClassHydrationBench.php new file mode 100644 index 00000000..d5aaec8c --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/InheritanceClassHydrationBench.php @@ -0,0 +1,47 @@ +data += [ + 'foo1' => 'some foo string', + 'bar1' => 42, + 'baz1' => new DateTime(), + 'someFooProperty1' => [12, 13, 14], + 'someBarProperty1' => 12354.4578, + 'someBazProperty1' => new stdClass(), + ]; + } + + public function setUp() : void + { + $this->createHydrator(InheritanceClass::class); + $this->createData(); + $this->object = new InheritanceClass(); + } + + /** + * @Revs(100) + * @Iterations(200) + */ + public function benchConsume() : void + { + ($this->hydrator)($this->data, $this->object); + } +} diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/InheritanceDeepClassHydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/InheritanceDeepClassHydrationBench.php new file mode 100644 index 00000000..cc9ff907 --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/InheritanceDeepClassHydrationBench.php @@ -0,0 +1,53 @@ +data += [ + 'foo1' => 'some foo string', + 'bar1' => 42, + 'baz1' => new DateTime(), + 'someFooProperty1' => [12, 13, 14], + 'someBarProperty1' => 12354.4578, + 'someBazProperty1' => new stdClass(), + 'foo2' => 'some foo string', + 'bar2' => 42, + 'baz2' => new DateTime(), + 'someFooProperty2' => [12, 13, 14], + 'someBarProperty2' => 12354.4578, + 'someBazProperty2' => new stdClass(), + ]; + } + + public function setUp() : void + { + $this->createHydrator(InheritanceDeepClass::class); + $this->createData(); + $this->object = new InheritanceDeepClass(); + } + + /** + * @Revs(100) + * @Iterations(200) + */ + public function benchConsume() : void + { + ($this->hydrator)($this->data, $this->object); + } +} diff --git a/benchmarks/GeneratedHydratorBenchmark/FileClosure/MixedClassHydrationBench.php b/benchmarks/GeneratedHydratorBenchmark/FileClosure/MixedClassHydrationBench.php new file mode 100644 index 00000000..6e66e044 --- /dev/null +++ b/benchmarks/GeneratedHydratorBenchmark/FileClosure/MixedClassHydrationBench.php @@ -0,0 +1,31 @@ +createHydrator(MixedClass::class); + $this->createData(); + $this->object = new MixedClass(); + } + + /** + * @Revs(100) + * @Iterations(200) + */ + public function benchConsume() : void + { + ($this->hydrator)($this->data, $this->object); + } +} diff --git a/src/GeneratedHydrator/ClosureGenerator/FileCache/GenerateFileCacheHydrator.php b/src/GeneratedHydrator/ClosureGenerator/FileCache/GenerateFileCacheHydrator.php new file mode 100644 index 00000000..43ebfe25 --- /dev/null +++ b/src/GeneratedHydrator/ClosureGenerator/FileCache/GenerateFileCacheHydrator.php @@ -0,0 +1,196 @@ +targetDir = $targetDir ?? sys_get_temp_dir() . DIRECTORY_SEPARATOR; + } + + /** + * @throws ReflectionException + */ + public function __invoke(string $className) : callable + { + if (isset(self::$hydratorsCache[$className])) { + return self::$hydratorsCache[$className]; + } + + // @todo Use modern PSR folder structure? + $filename = $this->targetDir . str_replace('\\', '_', $className) . '.php'; + if (! file_exists($filename)) { + $this->generateFile($className, $filename); + } + $generateHydrator = $this; + + $hydrate = require $filename; + + return self::$hydratorsCache[$className] = $hydrate; + } + + /** + * @return string[] + * + * @throws ReflectionException + */ + private function getPropertyNames(string $className) : array + { + $class = $this->getClass($className); + $propertyNames = []; + foreach ($class->getProperties() as $property) { + if ($property->isStatic()) { + continue; + } + $propertyNames[] = $property->getName(); + } + + return $propertyNames; + } + + /** + * @throws ReflectionException + */ + private function getClass(string $className) : ReflectionClass + { + if (isset(self::$classesCache[$className])) { + return self::$classesCache[$className]; + } + + $class = new ReflectionClass($className); + +// // fill cache for parents +// while (($parentClass = $class->getParentClass()) !== false) { +// self::$classesCache[$parentClass->getName()] = $parentClass; +// } + + return self::$classesCache[$className] = $class; + } + + /** + * @throws ReflectionException + */ + private function getParentClassName(string $className) : ?string + { + $parentClass = $this->getClass($className)->getParentClass(); + if ($parentClass === false) { + return null; + } + + return $parentClass->getName(); + } + + /** + * @throws ReflectionException + */ + private function generateFile(string $className, string $filename) : void + { + $ast = $this->generateAst($className); + + $prettyPrinter = new Standard(); + $code = $prettyPrinter->prettyPrintFile($ast); + file_put_contents($filename, $code); + } + + /** + * @return Node[] + * + * @throws ReflectionException + */ + private function generateAst(string $className) : array + { + $factory = new BuilderFactory(); + $parentClassName = $this->getParentClassName($className); + $hasParent = $parentClassName !== null; + + $ast = []; + if ($hasParent) { + $ast[] = new Node\Stmt\Expression(new Node\Expr\Assign( + $factory->var('hydrateParent'), + $factory->funcCall( + $factory->var('generateHydrator'), + [$parentClassName], + ) + )); + } + + $closure = new Node\Expr\Closure([ + 'static' => true, + 'params' => [ + $factory->param('data')->setType('array')->getNode(), + $factory->param('object')->setType($className)->getNode(), + ], + 'uses' => $hasParent ? [$factory->var('hydrateParent')] : [], + 'returnType' => $className, + ]); + if ($hasParent) { + $closure->stmts[] = new Node\Stmt\Expression( + $factory->funcCall( + $factory->var('hydrateParent'), + [ + $factory->var('data'), + $factory->var('object'), + ] + ) + ); + } + foreach ($this->getPropertyNames($className) as $propertyName) { + $closure->stmts[] = + new Node\Stmt\If_( + $factory->funcCall( + '\array_key_exists', + $factory->args( + [ + $factory->val($propertyName), + $factory->var('data'), + ] + ) + ), + [ + 'stmts' => [new Node\Stmt\Expression( + new Node\Expr\Assign( + $factory->propertyFetch($factory->var('object'), $propertyName), + new Node\Expr\ArrayDimFetch($factory->var('data'), $factory->val($propertyName)) + ) + ), + ], + ], + ); + } + $closure->stmts[] = new Node\Stmt\Return_($factory->var('object')); + $assign = new Node\Stmt\Return_( + $factory->staticCall('\Closure', 'bind', [ + $closure, + null, + $className, + ]) + ); + $ast[] = $assign; + + return $ast; + } +} diff --git a/tests/GeneratedHydratorTest/ClosureGenerator/FileCache/GenerateHydratorTest.php b/tests/GeneratedHydratorTest/ClosureGenerator/FileCache/GenerateHydratorTest.php new file mode 100644 index 00000000..ecb615c1 --- /dev/null +++ b/tests/GeneratedHydratorTest/ClosureGenerator/FileCache/GenerateHydratorTest.php @@ -0,0 +1,137 @@ +generateHydrator = new GenerateFileCacheHydrator('build/'); + } + + /** + * @throws ReflectionException + */ + public function testDefaultBaseClass() : void + { + $hydrate = ($this->generateHydrator)(BaseClass::class); + $object = new BaseClass(); + + $result = $hydrate( + [ + 'publicProperty' => 'publicPropertyNew', + 'protectedProperty' => 'protectedPropertyNew', + 'privateProperty' => 'privatePropertyNew', + ], + $object + ); + + self::assertSame( + [ + 'publicProperty' => 'publicPropertyNew', + 'protectedProperty' => 'protectedPropertyNew', + 'privateProperty' => 'privatePropertyNew', + ], + $this->getProperties($result) + ); + } + + /** + * @throws ReflectionException + */ + public function testClassWithParents() : void + { + $object = new ClassWithPrivatePropertiesAndParents(); + $hydrate = ($this->generateHydrator)(get_class($object)); + + $hydrate( + [ + 'property0' => 'property0_new', + 'property1' => 'property1_new', + 'property2' => 'property2_new', + 'property3' => 'property3_new', + 'property4' => 'property4_new', + 'property5' => 'property5_new', + 'property6' => 'property6_new', + 'property7' => 'property7_new', + 'property8' => 'property8_new', + 'property9' => 'property9_new', + 'property20' => 'property20_new', + 'property21' => 'property21_new', + 'property22' => 'property22_new', + 'property30' => 'property30_new', + 'property31' => 'property31_new', + 'property32' => 'property32_new', + ], + $object + ); + self::assertSame( + [ + 'property0' => 'property0_new', + 'property1' => 'property1_new', + 'property2' => 'property2_new', + 'property3' => 'property3_new', + 'property4' => 'property4_new', + 'property5' => 'property5_new', + 'property6' => 'property6_new', + 'property7' => 'property7_new', + 'property8' => 'property8_new', + 'property9' => 'property9_new', + 'property20' => 'property20_new', + 'property21' => 'property21_new', + 'property22' => 'property22_new', + 'property30' => 'property30_new', + 'property31' => 'property31_new', + 'property32' => 'property32_new', + ], + $this->getProperties($object) + ); + } + + /** + * @return mixed[] + * + * @throws ReflectionException + */ + private function getProperties(object $object) : array + { + $reflectionClass = new ReflectionClass($object); + + return $this->getPropertiesWithReflection($object, $reflectionClass); + } + + /** + * @return mixed[] + */ + private function getPropertiesWithReflection(object $object, ReflectionClass $reflectionClass) : array + { + $properties = $reflectionClass->getParentClass() + ? $this->getPropertiesWithReflection($object, $reflectionClass->getParentClass()) + : []; + foreach ($reflectionClass->getProperties() as $reflectionProperty) { + if ($reflectionProperty->isStatic()) { + continue; + } + if ($reflectionProperty->isPrivate() || $reflectionProperty->isProtected()) { + $reflectionProperty->setAccessible(true); + } + $properties[$reflectionProperty->getName()] = $reflectionProperty->getValue($object); + } + + return $properties; + } +}