Coder Social home page Coder Social logo

generatedhydrator's Introduction

Generated Hydrator

GeneratedHydrator is a library about high performance transition of data from arrays to objects and from objects to arrays.

Tests Releases Downloads
Mutation testing badge Type Coverage Latest Stable Version Total Downloads

What does this thing do?

A hydrator is an object capable of extracting data from other objects, or filling them with data.

A hydrator performs following operations:

  • Convert Object to array
  • Put data from an array into an Object

GeneratedHydrator uses proxying to instantiate very fast hydrators, since this will allow access to protected properties of the object to be handled by the hydrator.

Also, a hydrator of GeneratedHydrator implements Laminas\Hydrator\HydratorInterface.

Installation

To install GeneratedHydrator, install Composer and issue the following command:

composer require ocramius/generated-hydrator

Usage

Here's an example of how you can create and use a hydrator created by GeneratedHydrator:

<?php

use GeneratedHydrator\Configuration;

require_once __DIR__ . '/vendor/autoload.php';

class Example
{
    public    $foo = 1;
    protected $bar = 2;
    protected $baz = 3;
}

$config        = new Configuration('Example');
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator      = new $hydratorClass();
$object        = new Example();

var_dump($hydrator->extract($object)); // ['foo' => 1, 'bar' => 2, 'baz' => 3]
$hydrator->hydrate(
    ['foo' => 4, 'bar' => 5, 'baz' => 6],
    $object
);
var_dump($hydrator->extract($object)); // ['foo' => 4, 'bar' => 5, 'baz' => 6]

Performance comparison

A hydrator generated by GeneratedHydrator is very, very, very fast. Here's the performance of the various hydrators of Laminas\Hydrator compared to a hydrator built by GeneratedHydrator:

<?php
require_once __DIR__ . '/vendor/autoload.php';

$iterations = 10000;

class Example
{
    public $foo;
    public $bar;
    public $baz;
    public function setFoo($foo) { $this->foo = $foo; }
    public function setBar($bar) { $this->bar = $bar; }
    public function setBaz($baz) { $this->baz = $baz; }
    public function getFoo() { return $this->foo; }
    public function getBar() { return $this->bar; }
    public function getBaz() { return $this->baz; }
    public function exchangeArray($data) {
        $this->foo = $data['foo']; $this->bar = $data['bar']; $this->baz = $data['baz'];
    }
    public function getArrayCopy() {
        return array('foo' => $this->foo, 'bar' => $this->bar, 'baz' => $this->baz);
    }
}

$object        = new Example();
$data          = array('foo' => 1, 'bar' => 2, 'baz' => 3);
$config        = new GeneratedHydrator\Configuration('Example');
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrators     = array(
    new $hydratorClass(),
    new Laminas\Hydrator\ClassMethods(),
    new Laminas\Hydrator\Reflection(),
    new Laminas\Hydrator\ArraySerializable(),
);

foreach ($hydrators as $hydrator) {
    $start = microtime(true);

    for ($i = 0; $i < $iterations; $i += 1) {
        $hydrator->hydrate($data, $object);
        $hydrator->extract($object);
    }

    var_dump(microtime(true) - $start);
}

This will produce something like following:

0.028156042098999s
2.606673002243s
0.56710886955261s
0.60278487205505s

As you can see, the generated hydrator is 20 times faster than Laminas\Hydrator\Reflection and Laminas\Hydrator\ArraySerializable, and more than 90 times faster than Laminas\Hydrator\ClassMethods.

Tuning for Production

By default, GeneratedHydrator will generate hydrators on every new request. While this is relatively fast, it will cause I/O operations, and you can achieve even better performance by pre-generating your hydrators and telling your application to autoload them instead of generating new ones at each run.

Avoiding regeneration involves:

  1. pre-generating your hydrators
  2. ensuring that your autoloader is aware of them

The instructions that follow assume you are using Composer.

Pre-generating your hydrators

There is no built-in way to bulk-generate all required hydrators, so you will need to do so on your own.

Here is a simple snippet you can use to accomplish this:

require '/path/to/vendor/autoload.php'; // composer autoloader

// classes for which we want to pre-generate the hydrators
$classes = [
    \My\Namespace\ClassOne::class,
    \My\Namespace\ClassTwo::class,
    \My\Namespace\ClassThree::class,
];

foreach ($classes as $class) {
    $config = new \GeneratedHydrator\Configuration($class);

    $config->setGeneratedClassesTargetDir('/path/to/target-dir');
    $config->createFactory()->getHydratorClass();
}

Just add all the classes for which you need hydrators to the $classes array, and have your deployment process run this script. When complete, all of the hydrators you need will be available in /path/to/target-dir.

Making the autoloader aware of your hydrators

Using your pre-generated hydrators is as simple as adding the generation target directory to your composer.json:

{
    "autoload": {
        "classmap": [
            "/path/to/target-dir"
        ]
    }
}

After generating your hydrators, have your deployment script run composer dump-autoload to regenerate your autoloader. From now on, GeneratedHydrator will skip code generation and I/O if a generated class already exists.

Fallback autoloader

Alternatively, GeneratedHydrator comes with a built-in autoloader that you can register on your own. This simplifies deployment, but is a bit slower:

$config = new \GeneratedHydrator\Configuration(\My\Namespace\ClassOne::class);

spl_autoload_register($config->getGeneratedClassAutoloader());

// now simply use your hydrator, and if possible, code generation will be skipped:
$hydratorName = $config->createFactory()->getHydratorClass();
$hydrator     = new $hydratorName();

Contributing

Please read the CONTRIBUTING.md contents if you wish to help out!

generatedhydrator's People

Contributors

carusogabriel avatar dependabot-preview[bot] avatar dependabot[bot] avatar github-actions[bot] avatar gsdevme avatar haampie avatar icarus94 avatar jensdenies avatar krymen avatar leedavis81 avatar magnusnordlander avatar majkl578 avatar michaelmoussa avatar ocramius avatar ostrolucky avatar pounard avatar prolic avatar remicollet avatar renovate[bot] avatar samnela avatar samsonasik avatar sasezaki avatar siwinski avatar staabm avatar thadafinser avatar volch avatar wyrihaximus avatar zielinskilukasz avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

generatedhydrator's Issues

Regenerating hydrators do not refresh them

First of all, I'm not sure whether this is a bug, an expected behaviour or a missing feature, so please go easy :)

I try to use pre-generating hydrators feature and hook it up with my deployment system. I've created generate.php script with a snippet doing basically what documentation says (and it works fine). I, then, intend to use it with pre-autoload-dump composer event script in order to achieve fresh hydrators for my objects every time I run composer install/update (regardless if it's development or production env).

The reason I want fresh hydrators every time is that the default values / attributes can easily change during development.

Because of a class existence check in HydratorFactory::getHydratorClass(), HydratorFactory will not attempt to create a proxy class when it already exists. I think pre-generation is intended for production usage only, when classes are not meant to get changed (thus hydrators don't need refreshing). Anyway, removing this class existence check makes it fit my scenario. How do you feel about forceRefresh kind of flag @Ocramius ?

Alternatively, do you have any ideas how to work it around ?

With PHP 7.4, the hydrator breaks with required typed property

For example, in hydrate methods, this:

    if (isset($values['current_revision']) || $object->current_revision !== null && \array_key_exists('current_revision', $values)) {
        $object->current_revision = $values['current_revision'];
    }

will raise a "property cannot be accessed prior to initialization" if it's typed and required, because of the object->current_revision !== null statement.

This means that the generated code must vary depending on how the property was declared, for typed and required properties, default value must be determined during compilation and code must be generated otherwise.

For example, let's consider that all other use case keep the same code, but for typed required properties, if property has a default value:

    if (isset($values['current_revision']) || \array_key_exists('current_revision', $values)) {
        $object->current_revision = $values['current_revision'];
    }

Or:

    $object->current_revision = $values['current_revision'] ?? $object->current_revision;

And if it has none:

    if (isset($values['current_revision'])) {
        $object->current_revision = $values['current_revision'];
    }

Or:

    $object->current_revision = $values['current_revision'] ?? null;

Which also could be a runtime optimization (for the later) whereas it could be slower (for the former) but since now recent PHP versions have an optimised opcode for \array_key_exists() this probably wouldn't be visible.

Remove dependencies to ProxyManager

ProxyManager includes a lot of logic that is very useful to generate classes at runtime, but that logic should be removed from the main repository so that this package doesn't depend on it.

Generated hydrator and extractor should be a closure, not a class

As per discussions in #59, hydrator and extractor shouldn't be composed into a zend-hydrator, but should instead be two closures.

An example API would be:

interface GenerateHydrator 
{
    public function __invoke(string $className) : callable;
}

interface GenerateExtractor 
{
    public function __invoke(string $className) : callable;
}

They would be used as following:

$object = $generateHydrator->__invoke(get_class($object))($object, $data); 
$data = $generateExtractor->__invoke(get_class($object))($object); 

Composing them into a Hydrator instance that follows zend-hydrator spec is simple, but we'd also get a decent performance improvement by just relying on functional composition above.

This would allow us to:

  • gain performance (one less method call per hydrate/extract)
  • remove the need of the visitor pattern to modify an existing class (we just generate closures from scratch)
  • simplify API
  • provide type-safe closures (since we can add parameter and return type hints)

Suggested test on the homepage is not fair

You suggested a very primitive benchmark in the README.md file, but it's actually not very significant. Indeed, your API is much faster, but it does not take into account private and protected properties hydration, as well as it forgots pretty much everything about class hierarchy.

Metadata for hydrator generation?

@Ocramius and others, I wonder what your opinion is about metadata for hydrator generation.

For example, I'd like to see integration for hydrator strategies or nesting hydrators, but it seems to me that you'd need to have some kind of metadata about how to map the array to the object. Thinks like, for example:

  • DateTime hydration. What format is the date in?
  • Nesting hydrators. Have ['bar' => ['quu' => 'quuz']] arary, hydrate to a Foo object that has a Bar object as property. The FooHydrator would need to call (or inline) the BarHydrator.
  • Array hydration. How to hydrate or extract an array of objects.

I think some kind of metadata is required to support these use cases, but I don't think you'd want to replicate a large part of e.g. JMS/Serializer. The point of generated hydrators is to be very, very fast.

What would be a sane way to go about this?

Invalid Proxy Generation

I have been investigating an issue which is causing invalid proxies to be generated, and it appears to be associated with classes in namespaces starting with a "v" prefix, e.g. v1.

Example code:

<?php

require_once __DIR__ . '/vendor/autoload.php';

use GeneratedHydrator\Configuration;

$object = new \AcmeBank\Services\BankingService\Core\Contracts\v1\Broken('code');

$config        = new Configuration(get_class($object));
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator = new $hydratorClass();

$extracted = $hydrator->extract($object);

var_dump($extracted);

During the $hydrator->extract($object) call, the following error occurs:

PHP Notice:  Undefined index:  in /private/var/folders/fv/r2fq3kk54rb6g1ycxwpcyf4svg09tw/T/GeneratedHydratorGeneratedClass__PM__AcmeBankServicesBankingServiceCoreContractsv1BrokenYToxOntzOjc6ImZhY3RvcnkiO3M6NDE6IkdlbmVyYXRlZEh5ZHJhdG9yXEZhY3RvcnlcSHlkcmF0b3JGYWN0b3J5Ijt9.php on line 27

Here's a look at the generated proxy:

<?php

namespace GeneratedHydratorGeneratedClass\__PM__\AcmeBank\Services\BankingService\Core\Contracts\v1\Broken;

class YToxOntzOjc6ImZhY3RvcnkiO3M6NDE6IkdlbmVyYXRlZEh5ZHJhdG9yXEZhY3RvcnlcSHlkcmF0b3JGYWN0b3J5Ijt9 extends \AcmeBank\Services\BankingService\Core\Contracts\v1\Broke$
{
    private $institutionCode = null;
    private $name = null;
    function __construct()
    {
        $this->institutionCodeWriter56e1c00865d30311291394 = \Closure::bind(function ($object, $value) {
            $object->institutionCode = $value;
        }, null, 'AcmeBank\\Services\\BankingService\\Core\\Contracts\\v1\\Broken');
        $this->nameWriter56e1c00865d7b109427712 = \Closure::bind(function ($object, $value) {
            $object->name = $value;
        }, null, 'AcmeBank\\Services\\BankingService\\Core\\Contracts\\v1\\Broken');
    }
    function hydrate(array $data, $object)
    {
        $this->institutionCodeWriter56e1c00865d30311291394->__invoke($object, $data['institutionCode']);
        $this->nameWriter56e1c00865d7b109427712->__invoke($object, $data['name']);
        return $object;
    }
    function extract($object)
    {
        $data = (array) $object;
        return array('institutionCode' => $data['^@AcmeBank\\Services\\BankingService\\Core\\Contracts^K1\\Broken^@institutionCode'], 'name' => $data['^@AcmeBank\\S$
    }
}

If you take a look at the extract method in the proxy, you'll notice that instead of the v1 namespace, the string ^K1 is being included. It's not clear if this is an issue in PhpParser or GeneratedHydrator, but I've begun examining the source to determine where the problem is occurring.

I would be happy to provide a sample project to ease reproduction of the issue if needed.

version error

I'm trying to install drupal using composer.
Here's an error I get:

Problem 1
- ocramius/generated-hydrator 1.1.1 requires php ~5.4 -> your PHP version (7.0.4) does not satisfy that requirement.
- ocramius/generated-hydrator 1.1.1 requires php ~5.4 -> your PHP version (7.0.4) does not satisfy that requirement.
- ocramius/generated-hydrator 1.1.1 requires php ~5.4 -> your PHP version (7.0.4) does not satisfy that requirement.
- Installation request for ocramius/generated-hydrator (locked at 1.1.1, required as ^1.1) -> satisfiable by ocramius/generated-hydrator[1.1.1].

Doctrine Hydrator implementation?

Hello, @Ocramius.

I'm wondering what you think about implementing Doctrine GeneratedHydrator. I mean something like GeneratedHydrator extends \Doctrine\ORM\Internal\Hydration\AbstractHydrator.

What do you think, is this a good idea? Will it have any advantages over ObjectHydrator?

Or maybe someone has already implemented it? :)

My dream is to learn how to fetch & hydrate & serialize large collections of entities with doctrine for an acceptable time.

For example I have these entities:

class Post {
    /** @ORM\Id() */
    private $id;

    /** @ORM\Column(type="string") */
    private $name;

    /** @ORM\OneToMany(targetEntity="PostTag") */
    private $postTags;

    public function getTags(): Tag[]
    {
        return array_map(
            function (PostTag $postTag) {
                return $postTag->getTag();
            },
            $this->postTags->toArray()
        )
    }
}

class PostTag {
    /** @ORM\ManyToOne(targetEntity="Post") */
    private $post;

    /** @ORM\ManyToOne(targetEntity="Tag") */
    private $tag;
}

class Tag {
    /** @ORM\Id() */
    private $id;

    /** @ORM\Column(type="string") */
    private $name;

    /** @ORM\OneToMany(targetEntity="PostTag") */
    private $postTags;

    ...

    public function getNameProcessedWithSomeKindOfMagic(): string
    {
        return processWithSomeKindOfMagic($this->name);
    }
}

In my JSON API REST controller I want to do something like that:

class PostController {
    public function getAllAction(): Response
    {
        $queryBuilder = $this->postRepository->createQueryBuilder('post')
            ->orderBy('post.id');

        /** @var Post[] $posts */
        $posts = $queryBuilder->getQuery()->getResult();

        // Thx for 2-step hydration trick :)
        // http://ocramius.github.io/blog/doctrine-orm-optimization-hydration/
        $this->postRepository->createQueryBuilder('post')
            ->select('PARTIAL post.{id}')
            ->addSelect('postTag')
            ->addSelect('tag')
            ->leftJoin('post.postTags', 'postTag')
            ->leftJoin('postTag.tag', 'tag')
            ->getQuery()
            ->getResult();

        // In this place I use Symfony Serializer with serialization mappings for each entity
        // Normalized post will look like that example:
        // $post = [
        //     'id' => 1,
        //     'name' => 'Foo Bar',
        //     'tags' => [
        //         [
        //             'id' => 1,
        //             'name' => 'foo',
        //             'name_processed_with_some_kind_of_magic' => 'magic_foo'
        //         ],
        //         [
        //             'id' => 2,
        //             'name' => 'bar',
        //             'name_processed_with_some_kind_of_magic' => 'magic_bar'
        //         ]
        //     ],
        // ];
        $data = $this->serializer->serialize($posts);

        return new JsonResponse($data, 200, [], true);
    }
}

With few thousands posts in database this action will take SECONDS to generate JSON from database data and the only reason here will be complicated doctrine object hydration.
Switching to ArrayHydrator will reduce it to a couple of hundreds milliseconds, but I can't use it because entities is not a simple DTOs (look at getTags and getNameProcessedWithSomeKindOfMagic method examples). Database data denormalization and ArrayHydrator usage is also a pretty good solution in big collections API, but I wonder if its not the only solution.

My apologies for the fact that this longread is not directly related to your library, this is a really big problem for me and I will be infinitely glad for your ideas on this issue.

Thanks!

Skipping undefined keys in the $data array

I've tried to use generated hydrators in my new project (https://github.com/matthiasnoback/broadway-serialization) but ran into a bit of trouble: when hydrating an object the data array needs to have a value for each property of the object. But in the case of, for example, event objects serialized and stored in an event store, some properties may have been added in the class of the event, but not in the data array. So an extra call to array_key_exists() would be required in this case.

Do you have any suggestions on how to fix this problem? I assume, based on other issues that have been reported, that you are not going to add any runtime checks. But if I want to, how should I proceed?

Reflection speed

I am opening this issue just to make sure noone ever tries to do anything bad with the reflection part of generation.

I did some benchmarking as I liked the built-in filtering of reflection properties than filtering the array of them. Turned out it is much slower.

Benchmark (using athletic):

<?php

class Example extends \Athletic\AthleticEvent
{
    protected $class;

    public function setUp()
    {
        $this->class = new LongExampleWithValues;
    }

    /**
     * @iterations 1000
     */
    public function testBuiltInFiltering()
    {
        $reflection = new \ReflectionClass($this->class);

        $reflectionProperties = array_diff(
            $reflection->getProperties(\ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED),
            $reflection->getProperties(\ReflectionProperty::IS_STATIC)
        );
    }

    /**
     * @iterations 1000
     */
    public function testArrayFiltering()
    {
        $reflection = new \ReflectionClass($this->class);

        $reflectionProperties = array_filter(
            $reflection->getProperties(),
            function (\ReflectionProperty $property) {
                return ($property->isPublic() || $property->isProtected()) && ! $property->isStatic();
            }
        );
    }
}

Test class:

<?php

class LongExampleWithValues
{
    private $a = 'a';
    protected $b = null;
    public $c = 1;
    static $d = true;

    public $e = -1;
    public $f = 'F';
    public $g = 1.0;
    public $h = -1.0;
    public $i = PHP_INT_MAX;
    public $j;
}

Results:

Example
    Method Name            Iterations    Average Time      Ops/second
    --------------------  ------------  --------------    -------------
    testBuiltInFiltering: [1,000     ] [0.0000722653866] [13,837.88349]
    testArrayFiltering  : [1,000     ] [0.0000248982906] [40,163.39976]

@Ocramius you are free to close this, just wanted to share some experience.

PHP 8.3 support

Hi there,

Would like to ask if there is a planned support for PHP 8.3 anytime soon?
(as currently php is locked into ~8.1.0 || ~8.2.0)

Reason: Allowing codebase migration to php 8.3, thus having no composer issues with dependency tree requirements

Control entropy for $tmpFileName (file name to long)

I just hit the roof of file (not path) length on my fs (encrypted ext4 = 143 bytes):

AppHydrator__PM__ThingContainerYToxOntzOjc6ImZhY3RvcnkiO3M6NDE6IkdlbmVyYXRlZEh5ZHJhdG9yXEZhY3RvcnlcSHlkcmF0b3JGYWN0b3J5Ijt9.php.5d2dc0a9162c80.87561400

Its 151 bytes, without namespaces.

While in production it will be 255 bytes, maybe a control over entropy is a good idea?

Ah I see. Its the encoded params base64-encoded via NameInflector. Damn.

Double hydration problem

Hello,

Here is how I use this hydrator

  • I do an SQL query
  • I get the result in the form of an array
  • I hydrate the data in a DTO
  • I send this DTO to another service
  • This service also executes an SQL query and hydrates the data once in the DTO

But I have a problem with nullable properties, indeed the hydrator will hydrate the null value for these properties, if they are not present in the data array.

I am able to correct the problem by modifying the HydratorMethodsVisitor class at line 110 by replacing
return ['$ object->'. $ propertyName. '='. $ inputArrayName. '['. $ escapedName. '] ?? null; '];
by
return ['$ object->'. $ propertyName. '='. $ inputArrayName. '['. $ escapedName. '] ?? '. '$ object->'. $ propertyName. ' ?? null; '];

So my question is, shouldn't the hydrator only hydrate properties if it exists in the array, like Laminas Hydrator ?

Should I use my own FileWriterGeneratorStrategy to overcome this problem ?

Thanks

Unable to hydrate NULLs

Just making sure you are aware of that. Because issets are used in generated hydrators instead of array_key_exists, these hydrators are unable to override default property value with a null for non-public properties.

Cannot hydrate readonly properties

It seems very normal in the end, since that readonly properties can only be initialized from constructor, but since I mostly bypass constructors to hydrate all object values at once, I end up on this error:

Cannot initialize readonly property App\Domain\Bibliotheque\Model\ContenuBibliotheque::$id from scope GeneratedHydratorGeneratedClass\__PM__\App\Domain\Bibliotheque\Model\ContenuBibliotheque\YToxOntzOjc6ImZhY3RvcnkiO3M6NDE6IkdlbmVyYXRlZEh5ZHJhdG9yXEZhY3RvcnlcSHlkcmF0b3JGYWN0b3J5Ijt9

I wonder if there's a trick to bypass this ?

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

Repository problems

These problems occurred while renovating this repository. View logs.

  • WARN: Use matchDepNames instead of matchPackageNames

Open

These updates have all been created already. Click a checkbox below to force a retry/rebase of any.

Detected dependencies

composer
composer.json
  • php ~8.1.0 || ~8.2.0 || ~8.3.0
  • laminas/laminas-hydrator ^4.14.0
  • nikic/php-parser ^4.16.0
  • ocramius/code-generator-utils ^1.7.0
  • doctrine/coding-standard ^12.0.0
  • phpbench/phpbench ^1.2.14
  • phpunit/phpunit ^9.6.10
  • psalm/plugin-phpunit ^0.18.4
  • roave/infection-static-analysis-plugin ^1.32.0
  • vimeo/psalm ^5.13.1
github-actions
.github/workflows/continuous-integration.yml
  • laminas/laminas-ci-matrix-action 1.22.1
  • laminas/laminas-continuous-integration-action 1.32.0
  • actions/checkout v4
  • shivammathur/setup-php 2.25.4
  • actions/cache v3
.github/workflows/release-on-milestone-closed.yml
  • actions/checkout v4
  • laminas/automatic-releases v1
  • laminas/automatic-releases v1
  • laminas/automatic-releases v1
  • laminas/automatic-releases v1
  • laminas/automatic-releases v1

  • Check this box to trigger a request for Renovate to run again on this repository

Don't overwrite non-hydrated properties

Currently the hydrator overwrites every property in an object, even if they are not present in the data array. The hydrator shouldn't overwrite these properties.

HydrateFactory each time generate the temporary file. Should?

  1. Copy-paste first example from readme.

  2. Edit HydratorFactory class (we will add debug dumping for $hydratorClassName variable):

    public function getHydratorClass() : string
    {
        $inflector         = $this->configuration->getClassNameInflector();
        $realClassName     = $inflector->getUserClassName($this->configuration->getHydratedClassName());
        $hydratorClassName = $inflector->getGeneratedClassName($realClassName, ['factory' => get_class($this)]);
    
        var_export(class_exists($hydratorClassName));
    
        if (! class_exists($hydratorClassName) && $this->configuration->doesAutoGenerateProxies()) {
            $generator      = $this->configuration->getHydratorGenerator();
            $originalClass  = new ReflectionClass($realClassName);
            $generatedAst   = $generator->generate($originalClass);
            $traverser      = new NodeTraverser();
    
            $traverser->addVisitor(new ClassRenamerVisitor($originalClass, $hydratorClassName));
            $this->configuration->getGeneratorStrategy()->generate($traverser->traverse($generatedAst));
            $this->configuration->getGeneratedClassAutoloader()->__invoke($hydratorClassName);
        }
    
        return $hydratorClassName;
    }
  3. cd /path/to/tmp

Run:

  1. First: Tmp file created, all okey.
    Results class_exists === false;
    Edit (eg. add comment) tmp file and carefully look into the editor.
  2. Second: Tmp file again created (rewrite old)!.
    Results class_exists === false;
    Checking class exists is fail! Our comment will be deleted!.

Class not found, so how not added to autoloader.

This fix solve problem:

public function getHydratorClass() : string
    {
        $inflector         = $this->configuration->getClassNameInflector();
        $realClassName     = $inflector->getUserClassName($this->configuration->getHydratedClassName());
        $hydratorClassName = $inflector->getGeneratedClassName($realClassName, ['factory' => get_class($this)]);
        // FIX.
        $this->configuration->getGeneratedClassAutoloader()->__invoke($hydratorClassName);

        if (! class_exists($hydratorClassName) && $this->configuration->doesAutoGenerateProxies()) {
            $generator      = $this->configuration->getHydratorGenerator();
            $originalClass  = new ReflectionClass($realClassName);
            $generatedAst   = $generator->generate($originalClass);
            $traverser      = new NodeTraverser();

            $traverser->addVisitor(new ClassRenamerVisitor($originalClass, $hydratorClassName));
            $this->configuration->getGeneratorStrategy()->generate($traverser->traverse($generatedAst));
            $this->configuration->getGeneratedClassAutoloader()->__invoke($hydratorClassName);
        }

        return $hydratorClassName;
    }

Implement property map

In some cases, array keys provided to the hydrator may not correspond with what the object internal state looks like.

The hydrator should therefore be configurable, by allowing:

  • Disabling properties from being hydrated
  • Mapping properties to specific keys

Hydrators fails on objects that include hydrate / extract methods

Hi,
I've tried to extract an object that contains extract() or hydrate() methods in them. So basically, using the docs example it would be sth like:

class Example
{
    public    $foo = 1;
    protected $bar = 2;
    protected $baz = 3;

    public function extract() {
    // anything here
    }
}

$config        = new Configuration('Example');
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator      = new $hydratorClass();
$object        = new Example();

var_dump($hydrator->extract($object)); // PHP STRICT ERROR - extract() method interface is wrong

I presume this is because the usage of Visitor pattern in HydratorMethodsVisitor. I think it's a critial design part of this hydrator so I'd rather ask to patch up the docs mentioning this as a limitation. My final argument is that GeneratedHydrator works as Reflection hydrator replacement, but Reflection doesn't fail on this case.

EDIT: Of course, patching Example::extract($object) interface to match number of arguments works, but it's a nasty workaround.

Current version (4.1.0) does not support php 7.4

I got the following error message when I ran the example code in the PHP7.4 environment.

ParseError : syntax error, unexpected '|', expecting ';' or '{'

I think the reason is that it requires "ocramius/code-generator-utils": "^1.0.0" which requires "php": "^8.0",

Private members from the same class - grouping

When reading private class members, we usually do a function call per-property:

$prop1 = Closure::bind(function ($o) { return $o->prop1; }, null, 'ClassName');
$prop2 = Closure::bind(function ($o) { return $o->prop2; }, null, 'ClassName');

return array(
    'prop1' => $prop1($object),
    'prop2' => $prop2($object),
);

If prop1 and prop2 have the same declaring class, the method call can be simply one:

$props = Closure::bind(function ($o) {
    return array($o->prop1, $o->prop2);
}, null, 'ClassName');

list($prop1, $prop2) = $props($object);

return array(
    'prop1' => $prop1,
    'prop2' => $prop2,
);

Same could be done with just arrays:

$data = $props($object);

return array(
    'prop1' => $data[0],
    'prop2' => $data[1],
);

Comparison still to be done, but list may be faster

PHP 7.2 compatibility

Hi @Ocramius,

I've been stuck in a dependency hell for weeks between this package (which we use in our project) and PHPStan/Rector (common point: they all depend on nikic/php-parser, but different versions depending on the PHP version constraint). I've tried Docker'ing everything, using PHARs, and dark magic spells. But as our project is still on PHP 7.2 (and will be for some months - after all, EOL is 11.2020), nothing worked.

Then I looked up the history of this package and noticed that the version constraint was bumped from 7.2 to 7.3 in this commit from this PR. I've looked at the diff (and the one between 2.2.0 - compatible 7.2 - and 3.0 - requiring 7.3 -) and I can't for the sake of me find a single change that requires PHP 7.3. Which begs the question : why was the version constraint bumped in the first place ? (currently supposition : "because we can" ? ... ๐Ÿ™„ )

If so, I'd like to make a point for all those who can't upgrade just yet and/or would hugely benefits using tools like Rector to do so. So, could we revert this constraint back to 7.2 ?

Thanks for your consideration - and hopefully your help getting me out of this nightmare.
gnutix

Compatibility with nikic/php-parser v3

Hi @Ocramius,

Are you planning to upgrade nikic/php-parser dependency to use version 3? My project already uses nikic/php-parser, but requires version 3 which conflicts with version 2 used by ocramius/generated-hydrator.

Declare intent to abandon this library

This issue is a placeholder for the next minor release: I've yet to write a detailed reasoning for why I want to abandon this project.

The TLDR is:

  • it was an experiment / feasibility study for doctrine/orm internals
    • I no longer actively work on doctrine/orm
  • I firmly believe that when you scratch the limits of PHP to the point at which you need this library, you are better off using a compiled programming language instead

Therefore there won't be PHP 8.4 support for this library, which will instead be abandoned.

TODO: adjust README.md with architectural reasoning behind this library, and mark it security-only for a while.

Abandon later, once PHP 8.3 is EOL.

Dependency issue

With PHP 5.6, chicken and eggs issue:

Using

"require": {
    "zendframework/zendframework": "^3.0",
    "ocramius/generated-hydrator": "^2.0"
},

- ocramius/generated-hydrator 2.0.0 requires php ~7.0 -> your PHP version (5.6.23) does not satisfy that requirement.

Seems expected, so trying using previous version

"require": {
    "zendframework/zendframework": "^3.0",
    "ocramius/generated-hydrator": "^1.2"
},

Can't be installed as requires zend-stdlib 2

Use php-parser v4

I just ran into #69 but with nikic/php-parser v4, which was released in March.

I'm using phpstan - phpstan/phpstan: 0.10 requires nikic/php-parser` v4...

FWIW:

  1. I cloned this repo just to see if the tests might run with the v4 but composer update -v didn't upgrade nikic/php-parser because ocramius/code-generator-utils uses v3.
  2. I just cloned nikic/php-parser v4 directly into ./vendor but the tests failed with Class 'PhpParser\BuilderAbstract' not found.

It might be a bit of a job.

I'll look at downgrading phpstan to 0.9.2 to keep going.

Nested Objects

I'm getting NULL for nested objects, is it possible do use this recursively?

Property: @var bool[]

class ExampleDTO {
	public int $foo;
	protected string $bar;
	/** @var bool[] */
	protected $baz;
}

$config        = new Configuration('ExampleDTO');
$hydratorClass = $config->createFactory()->getHydratorClass();
$hydrator      = new $hydratorClass();

$hydrator->hydrate(
	['foo' => '150', 'bar' => 5, 'baz' => [1,0,0]],
	$object = new ExampleDTO()
);

var_dump($object);

baz array isn't getting converted to bools. Is there a proper way to hint this that will enforce the casting of baz to bool[]?

GeneratedHydrator Mixes up private properties

As of current implementation, GeneratedHydrator will not distinguish between properties from following
example:

class Foo
{
    private $bar;
}

class Bar extends Foo
{
    private $bar;
}

class Baz extends Foo
{
    private $bar;
}

Collections Hydration

Hello Guys, i have a question about collection hydration, since my collections include other domain objects.

Lets say that i have Car Domain Model with has a collection of Maintenance Logs which include Log Domain Model, and each has its own Hydrator when i call CarHydrator->hydrate will it use the LogHydrator automatically, or do i have to configure it in some way ?

Guys! Slow down!

I get wanting to implement the cool new features of PHP but how about a little backwards compatibility? I work with a company that JUST updated PHP to 7.2 (took months to migrate all the code with 3 programmers).

Going back into the commits and grabbing an older version I guess.

Another one with PHP 7.4

EDIT: I am so sorry, I was wrong, bug is elsewhere, I did too much code reading without actually testing before...

Hello, following #118 I stumbled upon yet another PHP 7.4 side effect.

I'm not sure this is really a bug, so I open this issue for discussion.

Considering the following PHP class:

namespace Some\Namespace;

class Foo
{
    public int $bar;
}

GeneratedHydrator will generate the following code:

class SomeGeneratedNameForHydrator implements \Zend\Hydrator\HydratorInterface
{
    private $hydrateCallbacks = array(), $extractCallbacks = array();
    function __construct()
    {
        $this->hydrateCallbacks[] = \Closure::bind(static function ($object, $values) {
            if (isset($values['bar']) || $object->bar !== null && \array_key_exists('bar', $values)) {
                $object->bar = $values['bar'];
            }
        }, null, 'Some\\Namespace\\Foo');
        $this->extractCallbacks[] = \Closure::bind(static function ($object, &$values) {
            $values['bar'] = $object->bar;
        }, null, 'Some\\Namespace\\Foo');
    }
    function hydrate(array $data, $object)
    {
        $this->hydrateCallbacks[0]->__invoke($object, $data);
        return $object;
    }
    function extract($object)
    {
        $ret = array();
        $this->extractCallbacks[0]->__invoke($object, $ret);
        return $ret;
    }
}

If I attempt an hydration, this will cause the following error:

"Typed property Some\Namespace\Foo::$bar must not be accessed before initialization"

Problem is that the following line:

            if (isset($values['bar']) || $object->bar !== null && \array_key_exists('bar', $values)) {
                $object->bar = $values['bar'];
            }

Will cause that.

Since that $bar is mandatory, and has no default, we could safely write:

            if (isset($values['bar'])) {
                $object->bar = $values['bar'];
            }

instead, and that would work.

Now, I cannot see any regression because of this code let me explain: let's consider the use case I'm working with, I always hydrate empty objects, created without a constructor, the following way:

$ref = new \ReflectionClass(Some\Namespace\Foo::class);
$object = $ref->newInstanceWithoutConstructor();

$class = $someHydratorFactory->getHydratorClass();

(new $class())->hydrate($object, [
    'bar' => 12,
]);

In this case, everything would be fine with the proposed generated code modification.

For other use cases such as:

namespace Some\Namespace;

class Foo
{
    public int $bar;

    public function __construct(int $bar)
    {
        $this->bar = $bar;
    }
}

Then:

$object = new Some\Namespace\Foo(7);

$class = $someHydratorFactory->getHydratorClass();

(new $class())->hydrate($object, [
    'bar' => 12,
]);

Should work as well, meaning that $bar value after hydration would be 12 as well.

If I remeber correctly, $object->bar !== null && \array_key_exists('bar', $values) was added so that null value can be explicitely set by the hydrator.

Devil here is in null handling, in this case (non nullable property), an explicit null value would be erroneous since that the target property can not be null and raise an error while attempt to set it explicitely. I guess we can safely remove the null handling then.

I can see one use case we miss: if the user do this:

$object = new Some\Namespace\Foo(7);

$class = $someHydratorFactory->getHydratorClass();

(new $class())->hydrate($object, [
    'bar' => null,
]);

Then isset($values['bar']) will miss that value is here, and is null. Should we do some kind of validation here, such as:

if (\array_key_exists($values['bar'])) {
    if (null === $values['bar']) {
        throw new HydratorKindOfException("Some\Namespace\Foo::\$bar cannot be null");
    }
    $object->bar = $values['bar'];
}

Or just generate:

if (\array_key_exists($values['bar'])) {
    $object->bar = $values['bar'];
}

And let PHP raise the exception itself? I think this option is the best, \array_key_exists() has now a specific OPCode for itself, and the performance impact would be, I guess, meaningless. Maybe it needs testing thought, but semantically, this is most accurate solution, and it solves all use cases altogether.

I will open a PR with tests and the modified code.

Use `Closure::call` over `Closure::bind`

The current generated hydrator has the signature:

class Example {
    private $a;
    protected $b;
    public $c;
}

class Generated extends Example implements HydratorInterface
{
    private $a = null;
    protected $b = null;
    public $c = null;
    function __construct()
    {
        $this->aWriter579676c837a6e801650765 = \Closure::bind(function ($object, $value) {
            $object->a = $value;
        }, null, 'Example');
    }
    function hydrate(array $data, $object)
    {
        $object->b = $data['b'];
        $object->c = $data['c'];
        $this->aWriter579676c837a6e801650765->__invoke($object, $data['a']);
        return $object;
    }

    // ... and extract()
}

But with PHP7 this can be simplified some :)

class Generated extends Example implements HydratorInterface
{
    private $a = null;
    protected $b = null;
    public $c = null;
    function __construct()
    {
    }
    function hydrate(array $data, $object)
    {
        $object->b = $data['b'];
        $object->c = $data['c'];
        (function($data) {
            $object->a = $data['a'];
        })->call($object, $data);
        return $object;
    }

    // ... and extract()
}

Closure::call does not return a new closure like Closure::bind does, but simply temporarily binds it and immediately calls it. Some overhead is saved this way, and it is probably faster. Haven't tested it though, but wanted to mention it here anyway.

And isn't it an idea to wrap hydrate's body in a closure by default? Probably most of the time at least some data members are private.

function hydrate(array $data, $object)
{
    (function($data) {
        $this->a = $data['a'];
        $this->b = $data['b'];
        $this->c = $data['c'];
    })->call($object, $data);
}

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.