10000 [VarExporter] add Instantiator::instantiate() to create+populate objects without calling their constructor nor any other methods by nicolas-grekas · Pull Request #28417 · symfony/symfony · GitHub
[go: up one dir, main page]

Skip to content

[VarExporter] add Instantiator::instantiate() to create+populate objects without calling their constructor nor any other methods #28417

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 26, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions src/Symfony/Component/VarExporter/Instantiator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\VarExporter;

use Symfony\Component\VarExporter\Exception\ExceptionInterface;
use Symfony\Component\VarExporter\Exception\NotInstantiableTypeException;
use Symfony\Component\VarExporter\Internal\Hydrator;
use Symfony\Component\VarExporter\Internal\Registry;

/**
* A utility class to create objects without calling their constructor.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Most likely a good idea to keep this @internal until proven to be stable for usage within the component

Copy link
Member Author
@nicolas-grekas nicolas-grekas Sep 10, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The proposed method borrows from the existing infrastructure in the component: if there are any issues with Instantiator::instantiate(), it's also an issue for VarExporter::export(). It would make no sense to mark this method as internal, and not the other.

And if we mark the other internal, then the component would have zero public interfaces. It wouldn't make sense :)

*
* @author Nicolas Grekas <p@tchwork.com>
*/
final class Instantiator
{
/**
* Creates an object and sets its properties without calling its constructor nor any other methods.
*
* For example:
*
* // creates an empty instance of Foo
* Instantiator::instantiate(Foo::class);
*
* // creates a Foo instance and sets one of its properties
* Instantiator::instantiate(Foo::class, ['propertyName' => $propertyValue]);
*
* // creates a Foo instance and sets a private property defined on its parent Bar class
* Instantiator::instantiate(Foo::class, [], [
* Bar::class => ['privateBarProperty' => $propertyValue],
* ]);
*
* Instances of ArrayObject, ArrayIterator and SplObjectHash can be created
* by using the special "\0" property name to define their internal value:
*
* // creates an SplObjectHash where $info1 is attached to $obj1, etc.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strongly suggest keeping anything coming from php core or extensions out of the scope of the component, as well as its documentation: there's an infinity of wrongness in classed that are not defined in PHP userland, and we really should rather tell users to not rely on them.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above: this is already dealt with for VarExporter::export(), so any issue found there should be spotted and fixed. There are already tests for these btw (which doesn't mean it's yet bulletproof of course.)

* Instantiator::instantiate(SplObjectStorage::class, ["\0" => [$obj1, $info1, $obj2, $info2...]]);
*
* // creates an ArrayObject populated with $inputArray
* Instantiator::instantiate(ArrayObject::class, ["\0" => [$inputArray]]);
*
* @param string $class The class of the instance to create
* @param array $properties The properties to set on the instance
* @param array $privateProperties The private properties to set on the instance,
* keyed by their declaring class
*
* @return object The created instance
*
* @throws ExceptionInterface When the instance cannot be created
*/
public static function instantiate(string $class, array $properties = array(), array $privateProperties = array())
{
$reflector = Registry::$reflectors[$class] ?? Registry::getClassReflector($class);

if (Registry::$cloneable[$class]) {
$wrappedInstance = array(clone Registry::$prototypes[$class]);
} elseif (Registry::$instantiableWithoutConstructor[$class]) {
$wrappedInstance = array($reflector->newInstanceWithoutConstructor());
} elseif (null === Registry::$prototypes[$class]) {
throw new NotInstantiableTypeException($class);
} elseif ($reflector->implementsInterface('Serializable')) {
$wrappedInstance = array(unserialize('C:'.\strlen($class).':"'.$class.'":0:{}'));
} else {
$wrappedInstance = array(unserialize('O:'.\strlen($class).':"'.$class.'":0:{}'));
}

if ($properties) {
$privateProperties[$class] = isset($privateProperties[$class]) ? $properties + $privateProperties[$class] : $properties;
}

foreach ($privateProperties as $class => $properties) {
if (!$properties) {
continue;
}
foreach ($properties as $name => $value) {
// because they're also used for "unserialization", hydrators
// deal with array of instances, so we need to wrap values
$properties[$name] = array($value);
}
(Hydrator::$hydrators[$class] ?? Hydrator::getHydrator($class))($properties, $wrappedInstance);
}

return $wrappedInstance[0];
}
}
3 changes: 3 additions & 0 deletions src/Symfony/Component/VarExporter/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ The VarExporter component allows exporting any serializable PHP data structure t
plain PHP code. While doing so, it preserves all the semantics associated with
the serialization mechanism of PHP (`__wakeup`, `__sleep`, `Serializable`).

It also provides an instantiator that allows creating and populating objects
without calling their constructor nor any other methods.

The reason to use this component *vs* `serialize()` or
[igbinary](https://github.com/igbinary/igbinary) is performance: thanks to
OPcache, the resulting code is significantly faster and more memory efficient
Expand Down
75 changes: 75 additions & 0 deletions src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
<?php

/*
* This file is part of the Symfony package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace Symfony\Component\VarExporter\Tests;

use PHPUnit\Framework\TestCase;
use Symfony\Component\VarExporter\Instantiator;

class InstantiatorTest extends TestCase
{
/**
* @expectedException \Symfony\Component\VarExporter\Exception\ClassNotFoundException
* @expectedExceptionMessage Class "SomeNotExistingClass&qu AF37 ot; not found.
*/
public function testNotFoundClass()
{
Instantiator::instantiate('SomeNotExistingClass');
}

/**
* @dataProvider provideFailingInstantiation
* @expectedException \Symfony\Component\VarExporter\Exception\NotInstantiableTypeException
* @expectedExceptionMessageRegexp Type ".*" is not instantiable.
*/
public function testFailingInstantiation(string $class)
{
Instantiator::instantiate($class);
}

public function provideFailingInstantiation()
{
yield array('ReflectionClass');
yield array('SplHeap');
yield array('Throwable');
yield array('Closure');
yield array('SplFileInfo');
}

public function testInstantiate()
{
$this->assertEquals((object) array('p' => 123), Instantiator::instantiate('stdClass', array('p' => 123)));
$this->assertEquals((object) array('p' => 123), Instantiator::instantiate('STDcLASS', array('p' => 123)));
$this->assertEquals(new \ArrayObject(array(123)), Instantiator::instantiate(\ArrayObject::class, array("\0" => array(array(123)))));

$expected = array(
"\0".__NAMESPACE__."\Bar\0priv" => 123,
"\0".__NAMESPACE__."\Foo\0priv" => 234,
);

$this->assertSame($expected, (array) Instantiator::instantiate(Bar::class, array('priv' => 123), array(Foo::class => array('priv' => 234))));

$e = Instantiator::instantiate('Exception', array('foo' => 123, 'trace' => array(234)));

$this->assertSame(123, $e->foo);
$this->assertSame(array(234), $e->getTrace());
}
}

class Foo
{
private $priv;
}

class Bar extends Foo
{
private $priv;
}
0