diff --git a/src/Symfony/Component/VarExporter/Instantiator.php b/src/Symfony/Component/VarExporter/Instantiator.php new file mode 100644 index 0000000000000..0061d76e7842f --- /dev/null +++ b/src/Symfony/Component/VarExporter/Instantiator.php @@ -0,0 +1,94 @@ + + * + * 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. + * + * @author Nicolas Grekas + */ +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. + * 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]; + } +} diff --git a/src/Symfony/Component/VarExporter/README.md b/src/Symfony/Component/VarExporter/README.md index c8b1b1f173a80..c3a072127e4de 100644 --- a/src/Symfony/Component/VarExporter/README.md +++ b/src/Symfony/Component/VarExporter/README.md @@ -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 diff --git a/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php b/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php new file mode 100644 index 0000000000000..2cb8e7a359d31 --- /dev/null +++ b/src/Symfony/Component/VarExporter/Tests/InstantiatorTest.php @@ -0,0 +1,75 @@ + + * + * 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" 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; +}