-
-
Notifications
You must be signed in to change notification settings - Fork 9.6k
[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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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. | ||
* | ||
* @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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above: this is already dealt with for |
||
* 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]) { | ||
nicolas-grekas marked this conversation as resolved.
Show resolved
Hide resolved
|
||
$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]; | ||
} | ||
} |
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; | ||
} |
There was a problem hiding this comment.
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 componentThere was a problem hiding this comment.
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 forVarExporter::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 :)