Thanks to visit codestin.com
Credit goes to github.com

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

Conversation

nicolas-grekas
Copy link
Member

@nicolas-grekas nicolas-grekas commented Sep 9, 2018

Q A
Branch? master
Bug fix? no
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? yes
Fixed tickets -
License MIT
Doc PR -

A blend of features also provided by https://github.com/doctrine/instantiator and https://github.com/Ocramius/GeneratedHydrator in one simple method. Because it's just a few more lines on top of the existing code infrastructure in the component :)

For example, from the docblock:

// creates an empty instance of Foo
Instantiator::instantiate(Foo::class);

// creates a Foo instance and sets one of its public, protected or private 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, $optionalFlag]]);

Misses some tests for now, but reuses the existing code infrastructure used to "unserialize" objects.

@nicolas-grekas nicolas-grekas added this to the next milestone Sep 9, 2018
@nicolas-grekas nicolas-grekas force-pushed the ve-instantiator branch 4 times, most recently from b157f1d to 702fb4d Compare September 9, 2018 21:21
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 :)

* // creates an empty instance of Foo
* Instantiator::instantiate([Foo::class => []]);
*
* // creates a Foo instance and sets one of its public, protected or private properties
Copy link
Contributor

Choose a reason for hiding this comment

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

The public/private/protected case should likely be always distinguished, because of inheritance and possible property overrides.

* Instantiator::instantiate([Foo::class => ['propertyName' => $propertyValue]]);
*
* // creates a Foo instance and sets a private property defined on its parent Bar class
* Instantiator::instantiate([
Copy link
Contributor

Choose a reason for hiding this comment

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

This syntax is very confusing: the object type should be the first string parameter. What's the reasoning behind putting the first type inside the array?

* 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.)

@javiereguiluz
Copy link
Member

javiereguiluz commented Sep 10, 2018

Thanks for the detailed description! Looking at the end-user API, I have two comments/questions:

  1. Do we really need to support an array as the argument of instantiate()? Is that common to instantiate more than one object at a time?
// Before
Instantiator::instantiate([
    Foo::class => [],
    Bar::class => ['propertyName' => $propertyValue],
]);

// After
Instantiator::instantiate(Foo::class, []);
Instantiator::instantiate(Bar::class, ['propertyName' => $propertyValue]);
  1. The use of \0 for some special objects looks like an internal detail which complicates the learning curve when exposing it publicly. Why not "hardcode" this detail internally and let the end-user not know about this?
// Before
Instantiator::instantiate([SplObjectStorage::class => ["\0" => [$obj1, $info1, $obj2, $info2]]]);
Instantiator::instantiate([ArrayObject::class => ["\0" => $inputArray]]);

// After
Instantiator::instantiate(SplObjectStorage::class, [$obj1 => $info1, $obj2 => $info2]);
Instantiator::instantiate(ArrayObject::class, $inputArray);

@nicolas-grekas
Copy link
Member Author

nicolas-grekas commented Sep 10, 2018

@Ocramius @javiereguiluz thanks for the review+comments, I addressed some inline, let's address the remaining ones:

I added a 2nd commit to change the signature to instantiate(string $class, array $properties, array $privateProperties);. See updated examples in the PR. If you think it's better for DX, fine to me.

The use of \0 for some special objects looks like an internal detail

It's not actually: e.g. you can also add dynamic properties to ArrayObject instances, that are not stored in the internal array structure. The instantiate() method has to provide a way to set both these properties and the internal array. \0 is specifically chosen because no dynamic properties can start with the nul character, so it is safe to use.

This doesn't mean we cannot do what you propose @javiereguiluz, but we still need a way to set dynamic properties when needed.

Actually, there is a trivial way that works already: using an stdClass key in the $privateProperties: this populates public (thus dynamic) properties already.

So, let's say we want to create something like that using instantiate:

$a = new ArrayObject($input);
$a->foo = 123;

The current code allows doing so using:

Instantiator::instantiate(ArrayObject::class, ['foo' => 123, "\0" => [$input]]);

And we could make it do the same using:

Instantiator::instantiate(ArrayObject::class, [$input], ['stdClass' => ['foo' => 123]]);

Please advise.
(note that I have a preference for the 1st variant because it makes it less an edge case, so would be easier to work with in generic code - at least that's how I feel about it.)

@javiereguiluz
Copy link
Member

Thanks for the detailed explanation. I hadn't thought about this use case:

$a = new ArrayObject($input);
$a->foo = 123;

I thought this utility was only for instantiating ... but that code instantiates and then initializes, so it's doing two different things :)

What if we use this signature?

public static function instantiate(
    string $classFqcn,
    array $constructorArguments = [],
    array $propertyValues = []
)

Then, this example would look as:

Instantiator::instantiate(ArrayObject::class, [$input], ['foo' => 123])

But most of the times you'd simply use something like this:

Instantiator::instantiate(ArrayObject::class, [$input])

@nicolas-grekas
Copy link
Member Author

@javiereguiluz wouldn't make sense: the specific property of instantiators is to not call the constructor nor any other methods...

@javiereguiluz
Copy link
Member

I just followed your last example: why pass $input if it's never used?

@javiereguiluz
Copy link
Member

After talking with Nico on Slack about this ... it's clear to me that I don't fully understand this 😅 So let's forget about my previous examples and proposals. Sorry!

@nicolas-grekas
Copy link
Member Author

Good news: shaking the code always helps to spot new edge cases.
PR is ready, with test cases and fixes found meanwhile.

@nicolas-grekas nicolas-grekas force-pushed the ve-instantiator branch 4 times, most recently from 5b16b53 to c18f056 Compare September 10, 2018 17:19
nicolas-grekas added a commit that referenced this pull request Sep 11, 2018
This PR was merged into the 4.2-dev branch.

Discussion
----------

[VarExporter] fix more edge cases

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| New feature?  | no
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

As found while preparing #28417

Commits
-------

443bd11 [VarExporter] fix more edge cases
@nicolas-grekas
Copy link
Member Author

Fixes split in PR #28437. This PR is now rebased on top of it.


class InstantiatorTest extends TestCase
{
use VarDumperTestTrait;
Copy link
Contributor

Choose a reason for hiding this comment

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

not sure im missing it.. but this can be removed right?

Copy link
Member Author

Choose a reason for hiding this comment

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

good catch, removed thanks

…cts without calling their constructor nor any other methods
@fabpot
Copy link
Member

fabpot commented Sep 26, 2018

@nicolas-grekas We needs good docs on that one (and the whole component). Can you create a PR for that in the docs repo? I think it won't take too much of your time as you're the best to write something smart about this :)

@fabpot
Copy link
Member

fabpot commented Sep 26, 2018

Thank you @nicolas-grekas.

@fabpot fabpot merged commit d9bade0 into symfony:master Sep 26, 2018
fabpot added a commit that referenced this pull request Sep 26, 2018
…e+populate objects without calling their constructor nor any other methods (nicolas-grekas)

This PR was merged into the 4.2-dev branch.

Discussion
----------

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

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | -
| License       | MIT
| Doc PR        | -

A blend of features also provided by https://github.com/doctrine/instantiator and https://github.com/Ocramius/GeneratedHydrator in one simple method. Because it's just a few more lines on top of the existing code infrastructure in the component :)

For example, from the docblock:

```php
// creates an empty instance of Foo
Instantiator::instantiate(Foo::class);

// creates a Foo instance and sets one of its public, protected or private 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:

```php
// 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, $optionalFlag]]);
```

Misses some tests for now, but reuses the existing code infrastructure used to "unserialize" objects.

Commits
-------

d9bade0 [VarExporter] add Instantiator::instantiate() to create+populate objects without calling their constructor nor any other methods
@nicolas-grekas nicolas-grekas deleted the ve-instantiator branch September 26, 2018 05:47
@nicolas-grekas nicolas-grekas modified the milestones: next, 4.2 Nov 1, 2018
This was referenced Nov 3, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants