Skip to content

[DI] Randomize private services #19683

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

Closed
wants to merge 5 commits into from
Closed

[DI] Randomize private services #19683

wants to merge 5 commits into from

Conversation

ro0NL
Copy link
Contributor

@ro0NL ro0NL commented Aug 20, 2016

Q A
Branch? "master"
Bug fix? yes
New feature? yes
BC breaks? no
Deprecations? no
Tests pass? not yet
Fixed tickets #19680
License MIT
Doc PR reference to the documentation PR, if any

Alternative approach compared to #19682

Thanks to @wouterj for explaining this approach.

Theory of operation;

  • compiler pass
    • build a map of private id => random id
    • remap private definitions to its new id
    • update references and aliases
    • inject id map into container builder (for BC)
  • container
    • id's found in the (injected) map are forwarded to the new id + deprecation notice
  • todo
    • add tests
    • fix tests due randomization
    • dump the container with orignal id's in generated docs, etc. for convenience.

The deprecation now only happens after compilation(!). This can be seen a design choice, i guess.

@ro0NL ro0NL changed the title Di/dump privates [DI] Fix sharing privates Aug 20, 2016
@@ -839,6 +839,7 @@ public function __construct()

$code .= "\n \$this->services = array();\n";
$code .= $this->addMethodMap();
$code .= $this->addPrivateServices();
Copy link
Contributor Author

@ro0NL ro0NL Aug 20, 2016

Choose a reason for hiding this comment

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

Needed to trigger deprecations in get for a compiled container when dumped.

@wouterj
Copy link
Member

wouterj commented Aug 20, 2016

This is basically a big copy/paste from the original get() function, right? I don't like that too much (it gets out of sync very quick and requires more work when some code is changed in get()/resolveService()).

Also, please note that in Symfony 4, private services will be implemented by using a randomly generated service ID. So implementing a complete new API for private services isn't needed.

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

This is about separating (concerns), not copy-paste. Basically this makes get final, have a look at the diff.

About 4.x changes.. that is eventually still future talk. I compare against the current codebase.

Even then, if you have a "random id" you need to be able to get it internally, without deprecations. That would only cleanup get.. unless you make a real good guess ;-) which should still, eh trigger.

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

Im not even sure we need random id's, if it's bulletproof already.

@sstok
Copy link
Contributor

sstok commented Aug 20, 2016

Im not even sure we need random id's, if it's bulletproof already.

Using random id's makes it possible to use same logic, it be like using a normal id except you don't know the id.

Is it possible to benchmark these changes with Blackfire.io? I'm worried performance could be degraded.

@wouterj
Copy link
Member

wouterj commented Aug 20, 2016

Even then, if you have a "random id" you need to be able to get it internally, without deprecations. That would only cleanup get.. unless you make a real good guess ;-) which should still, eh trigger.

A compiler would generate random IDs for private services and fix the service definitions that depend on them. No need to change anything in the Container.

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

@wouterj Fair enough 👍

I just dont understand how that reflects dumping..;

  • generate random id
  • update references
  • a private service needs to be injected by some id
    • are we gonna bypass the container to get the object?

@sstok i think it's boosted (we bypass get many times now).. but im not sure. Not familiar yet with blackfire.io.. could you collab?

@wouterj
Copy link
Member

wouterj commented Aug 20, 2016

I just dont understand how that reflects dumping..;

  1. Compiler finds private service
  2. Generates random ID
  3. Updates all references to the old ID with the generated one
  4. Container is dumped, using the random ID for the service, just like normal.

As said by @sstok, the only difference between a private and a public service will be that you don't know the ID of the private one (as it's automatically generated).

@sstok
Copy link
Contributor

sstok commented Aug 20, 2016

You could start by randomizing them already, and use those. But keep an alias array with private services, pointing to the actual id 👍

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

Based on the current design;

  • php dumper generates calls to (private) services using get(). How will that not trigger/throw,
    unless we alter get to allow for privates? Again; are we gonna bypass the container getter?
  • having a list of private id's (randomized or not) is useful
  • randomizing id's is a next step

@wouterj
Copy link
Member

wouterj commented Aug 20, 2016

php dumper generates calls to (private) services using get(). How will that not trigger/throw? Again are gonna bypass the container methods?

If requested service is the randomized ID, it's an internal call and thus should not trigger deprecations. If it uses the original service ID, it's a call from outside world and the deprecation should be triggered (and randomized service returned).

E.g:

public function get($id, ...)
{
    if (isset($this->privates[$id])) {
        @trigger_error(..., E_USER_DEPRECATED);

        $id = $this->privates[$id];
    }

    // ... behave like normal, $id is either a public service or the randomized ID
}

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

Ok, that clarifies. Leaving a minor leak whenever the user has a random id some way, some how. We could live with that, i guess 👍 i personally favor the separation by design though.

Still; get is a api method that does normalization and such. Using it internal seems overhead.

@wouterj
Copy link
Member

wouterj commented Aug 20, 2016

Still; get is a user method that does normalization and such. Using it internal seems overhead.

The internal get() method skips almost all of the method. The first condition matches for new services and the method is simply called. (See https://github.com/symfony/dependency-injection/blob/master/Container.php#L258-L259 )

Leaving a minor leak whenever the user has a random id some way, some how.

We don't check for random ID (what's random?), we check if the ID exists in the array that contains generated IDs by the compiler.

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

Thanks @wouterj this was really clarifying, and probably makes a separate method obsolete.

What about if we do it right now? I really dont think we should bridge it with workarounds (no offense).

edit: about the leak; whenever you have $id = $this->privates[$id]; you can access a private service (what we're gonna do internally right? :-)). Anyway, doesnt matter they're not exposed, and nobody makes that good guesses.

@ro0NL ro0NL changed the title [DI] Fix sharing privates [WIP][DI] Fix sharing privates Aug 20, 2016
@sstok
Copy link
Contributor

sstok commented Aug 20, 2016

The @trigger_error(..., E_USER_DEPRECATED); $id = $this->privates[$id]; is for BC only, in the future this resolving is simple removed, and as the original service-id is replaced with something random it will fail (saying the service doesn't exist).

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

@sstok @wouterj ping ping :-)

This works out pretty nice so far...

@ogizanagi
Copy link
Contributor

ogizanagi commented Aug 20, 2016

@ro0NL: Even if your work looks great, @wouterj 's approach has the benefits of keeping things simple and pragmatic to fix the related issue.
I don't know if it's a good idea both trying to fix the issue and introducing the private services id generation into the very same PR as a "bug fix". It won't be a bug fix anymore, but somehow mixing bug fix, new feature and BC for this new feature. I'd say it's too much :/

But that's only my opinion, a core member is more legitimate to hint you on this. Just saying you should probably don't work too hard on this right now ;)

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

Im just playing around with randomizing now.. eventually i want to revert what i did to get. (The changes are not that dramatic).

However, the tests are updated to the right behavior. That was was an effort, yes.

I understand @wouterj 's approach and motivation, im only proposing a structural solution to consider as we can currently still target master.

@ro0NL ro0NL changed the title [WIP][DI] Fix sharing privates [WIP][DI] Randomize private services Aug 20, 2016
@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 20, 2016

@fabpot as this was your idea more or less.. WDYT?

Tests are failing nicely so far;

@@ @@

-        $instance->dep = $this->get('shared_private');
+        $instance->dep = $this->get('2a21409629880ccd9259eda073b930c1');

         return $instance;
     }

     /**
-     * Gets the 'shared_private' service.
+     * Gets the '2a21409629880ccd9259eda073b930c1' service.

@@ @@
      */
-    protected function getSharedPrivateService()
+    protected function get2a21409629880ccd9259eda073b930c1Service()
     {
-        return $this->services['shared_private'] = new \stdClass();
+        return $this->services['2a21409629880ccd9259eda073b930c1'] = new \stdClass();
     }

We have the map now in Container.. so we can really be flexible (ie. only use the random id for code, not docs).

$this->idMap = array();
foreach ($container->getDefinitions() as $id => $definition) {
if (!$definition->isPublic()) {
$this->idMap[$id] = $this->randomizer ? (string) call_user_func($this->randomizer, $id) : md5(uniqid($id));
Copy link
Contributor

Choose a reason for hiding this comment

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

uniqid should not be used here, it's anything but unique and actually causes collisions very easily.

See https://github.com/symfony/symfony/blob/master/src/Symfony/Component/DependencyInjection/Loader/XmlFileLoader.php#L291 for how it's done there.

@sstok
Copy link
Contributor

sstok commented Aug 21, 2016

Status: Needs work

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 21, 2016

@sstok would it make sense to mark a random definition (old private) as public after compiling? Currently we deprecated has etc. for all privates (random or not), but in get we must allow for it (if requested by the random id).

This doesnt make sense to me: has('random') // false vs. get('random') // object. Marking them public simplifies things :)

@ro0NL ro0NL changed the title [WIP][DI] Randomize private services [DI] Randomize private services Aug 21, 2016
@nicolas-grekas
Copy link
Member

We might require this kind of randomization on 4.0 when getting private service will be removed.
For now, I don't think it provides more external side effect than #19708.
If I'm not wrong, this could be closed in favor of #19708
WDYT?

@stof
Copy link
Member

stof commented Aug 22, 2016

Yeah, I don't think we need to do this in 3.x

@ro0NL
Copy link
Contributor Author

ro0NL commented Aug 22, 2016

I dont mind :) i just dont like how we work with privates right now. And i do think this is the cleanest solution eventually, as the container does not have to care about privates. Currently leading to all sorts of quirkiness, as you might have noticed.

But yes, the pass can be added on top of #19708 in 4.x. From the component pov i would prefer this though (understanding the scope is wider, however we do advertise with standalone components ;-)).

*
* @author Roland Franssen <franssen.roland@gmail.com>
*/
class RandomizePrivateServiceIdentifiers implements CompilerPassInterface
Copy link
Member

Choose a reason for hiding this comment

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

IMO, randomization shouldn't be handled by a compiler pass, but by the PhpDumper directly. Otherwise it won't be applied without the compiler, and many apps won't have it enabled when migrating to 4.0

Copy link
Contributor Author

@ro0NL ro0NL Aug 23, 2016

Choose a reason for hiding this comment

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

I considered this. But eventually chose to separate it into a compiler pass (yes; making it an optional feature). Without compiling you have a normal container.

Thing is i dont really like the concept of coupliing privates with a container. The container shouldnt care about privates (it's just a normal service from that perspective). Hence $this->privates could be totally unneeded...

No need to bypass get or checking private definitions in PhpDumper. We work with the container :)

Copy link
Member

Choose a reason for hiding this comment

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

What you call a normal container doesn't have private services. The container shouldn't care about private services, right, but the PhpDumper should (and already does). So I still think that the PhpDumper should do this.

Copy link
Contributor Author

@ro0NL ro0NL Aug 24, 2016

Choose a reason for hiding this comment

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

We disagree :) i think we have compiler passes for that (optional) or otherwise Container::compile (by design).

A dumper (eg. PhpDumper) dumps the given container as is. Like others do, YAML, XML etc. I dont know why PhpDumer would randomize your services, whereas XmlDumper doesnt.

Currently it only deals with privates to hack the dumped container (ie. output), saying; here's a list of privates for you to validate business logic (which it shouldnt care about :)).

Copy link
Member

@nicolas-grekas nicolas-grekas Aug 24, 2016

Choose a reason for hiding this comment

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

Because it makes no sense to randomize with XmlDumper and in fact would be wrong: it would rename services for no reason and make dumps harder and not accurate.
What matters is having dumpers deal with private services, which XmlDumper does by adding the public="false" attribute, and PhpDumper doing some randomization. That! would make sense.

Copy link
Contributor Author

@ro0NL ro0NL Aug 24, 2016

Choose a reason for hiding this comment

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

Reconsidered :)

Because it makes no sense to randomize with XmlDumper and in fact would be wrong: it would rename services for no reason and make dumps harder and not accurate.

My first thought was, yeah this totally counts for a PhpDumper as well. Which in some ways is still true, but i guess you're right. Randomizing could be the way a php dumper deals with privates by design (whereas xml adds public="false" 👍 ).

Do we need to consider DX here?

  • Make it depend on some debug flag?
  • Have 2 implementations (think DebugPhpDumper)
  • Forget about randomizing, and compile different method names (think getPrivate(.+)Service)
    • Nah.. this makes the Container work with "privates" again 😕

Copy link
Member

Choose a reason for hiding this comment

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

Not sure how DX is involved here. So maybe but I don't what it'd mean :)

Forget about randomizing, and compile different method names

maybe :) the goal is to have private services be hidden from the outside, no interference with the public interface.
How it's done is an implementation detail We should chose the simplest one... :)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We should chose the simplest one

True.

DX, in the sense that you may not want to have random id's in the output, or at least customize/mock it in some way (think setRandomizer).

Copy link
Member

Choose a reason for hiding this comment

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

Then maybe the simplest and most DX friendly is to make private services leak a bit by reserving their id, throwing an exception whenever one uses or fetches an id that collides with a private service.

fabpot added a commit that referenced this pull request Sep 1, 2016
…ces internally (nicolas-grekas)

This PR was merged into the 3.2-dev branch.

Discussion
----------

[DI] Dont use Container::get() when fetching private services internally

| Q             | A
| ------------- | ---
| Branch?       | master
| Bug fix?      | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets | #19683, #19682, #19680
| License       | MIT

As spotted by @wouterj, we forgot to remove the deprecation notice when doing internal calls to get private services.

Yet, we don't need to get through this `get()` method, because we can already resolve many things at compile time for private services. This will provide another small performance optim, and fix the issue.

Commits
-------

a9c79fb [DI] Dont use Container::get() when fetching private services internally
@nicolas-grekas
Copy link
Member

So, following the discussion above, my stand on this is:

Then maybe the simplest and most DX friendly is to make private services leak a bit by reserving their id, throwing an exception whenever one uses or fetches an id that collides with a private service.

This means not randomizing private services, even in 4.0.
Given that 4.0 is far away, I think we should close this PR, at least for now.
It's way too early to work on it and the issue will naturally arise when working on 3.4/4.0 when we'll work on cleaning deprecations.

@ro0NL
Copy link
Contributor Author

ro0NL commented Sep 7, 2016

👍 eventually it comes down to track private id's yes/no. Like currently..

Personally i'd favor randomizing id's in the PhpDumper.. (privates are just normal services).

But it doesnt matter much i guess.

@ro0NL ro0NL closed this Sep 7, 2016
@ro0NL ro0NL deleted the di/dump-privates branch September 24, 2016 13:46
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.

7 participants