ocramius/generated-hydrator

An Object Hydrator that allows very fast array to object to array conversion

Fund package maintenance!
Ocramius
Tidelift

Installs: 1 175 945

Dependents: 33

Suggesters: 8

Security: 0

Stars: 727

Watchers: 27

Forks: 69

Open Issues: 29


README

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

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!