Problem/Motivation
tl;dnr we should probably change our prolific usage of "new static" to "new self"
Similar to a lot of other people I think, I started turning my attention to what I could do to help contrib modules I relied on be ready for D9. As such, I started setting up php-stan/drupal-check to run against my contrib directory and I was startled by a slew of "Unsafe usage of new static()" message out of a default stan test.
You'll find a ton of similar reports searching the issue queue from stan reports other people have run like this one #3114031: Drupal 9 Deprecated Code Report.
My initial reaction was that doesn't make sense, we've been using it forever and it works great. Then I read the explanation and its actually pretty close to our existing policies. Basically it boils down to the constructor is not an interface. That's something we're pretty clear about _but_ if you call "new static" you are de-facto making the constructor an interface for any extending class.
Now, in practice I don't think this is likely to run into problems. We're pretty safe with our constructors generally maintaining b/c over changes to them and we generally keep the constructor and factory close. Also generally if you're extending the constructor you probably need to replace the factory to provide additional services assuming you're not being lazy. But lets be honest, we're all lazy from time to time and if a extending class overrides the constructor and not the create method and then the base class changes there could be some weird interactions or even possibly breakage. We can control checking null's and pushing deprecation warnings and defaults in the constructor but its not as easy trying to resolve static binding and doing the same in a factory.
Using new self instead pushes the maintenance of the create method and the connection to the constructor entirely onto the extending class and makes this relationship explicit which seems like a good thing.
Proposed resolution
Replace usage of new static in factory methods with new self.
Remaining tasks
Decide if this is the correct approach.
User interface changes
n/a
API changes
Maybe? If someone was relying on this non-interface they where already in a constructor as an API which might be a grey area.
Data model changes
n/a
Release notes snippet
TBD
Comments
Comment #2
neclimdulComment #3
neclimdulOh hey past me, this is future me finding this through googling this error. :( I guess I should follow up with this.
Comment #4
jeffc518 commented+1 this. In custom modules I'm mainly following the static convention for late static binding. I'm pretty sure I've seen a bunch of get_called_class() calls in various spots, wouldn't that be problematic with new self()?
Comment #5
neclimdulright on. using "get_called_class" sounds like a red flag already but I wouldn't think this would affect the modules. We're going to be dealing with a lot of BC issues doing this though. I'm not really sure how to answer it more without a more concrete example though.
Comment #7
guypaddock commentedI'd love it if this issue showed a before vs after code example, even if hypothetical, to understand what's being proposed in practice.
Comment #8
neclimdulHere I am again... following a google search.
The before and after are pretty simple. As the title says, its just use
new selfinstead ofnew staticBefore:
Comment #9
kingdutchI agree that this change should be made but this requires a change in the
ContainerFactoryPluginInterfaceannotation as well.This is mostly to make PHPStan happy:
Technically this may make it an API break? Specifically, in cases where a constructor was overwritten but
createwas not. The majority of those cases will probably break in other fun and unexpected ways though.Comment #11
kristiaanvandeneyndeI disagree with changing new static into new self.
They are both vastly different in implications and there is a new unofficial way of extending classes that use one of the ContainerFOOInterfaces that actually works better because it avoids having to write a constructor and copying all of the parent constructor's signature.
This will throw the PHPStan error but is completely safe as long as no-one touches the constructor somewhere down the line without also overwriting (in this case) the create method. Ideally, no-one touches any constructors any more because the above pattern achieves the same and is 100% proof to the introduction of new dependencies by the base class.
I'd rather see the above pattern implemented even more in core and the PHPStan error in question added to the list of ignored errors.
See for more info: https://github.com/mglaman/drupal-check/issues/136
Comment #12
neclimdulWebform does that and I _hate_ it. It just makes the factory method into the constructor taking a container argument. It wanted to do this you might as well just pass the container to the native constructor. It may not be called a duck but it walks and talks like one because it is.
We rejected this injecting the container design and created ContainerInjectionInterface specifically to discourage users from doing it.
Of the top of my head(it was a long drawn out discussion across multiple issues and platforms)
Honestly I think this design can be a lot worse then sending the container to the constructor because it doesn't follow then normal constructor mechanics and also creates _2_ constructors. I have got some webform handlers that extend some existing handlers and I've ended up having to completely copy the parent factory method, hack and rewrite it, then write a bunch of tests and pray the parent method never changes. All because there is some weird initialization stuff happening but its happening out side native constructors so you can't integrate with the normal parent::__construct() mechanics.
So, yeah... I keep going back and forth on
new selfbut I'm _super_ against putting everything in the factory method.Comment #13
guypaddock commentedI totally agree with @neclimdul; I am not fond of the `create()` method taking over what the constructor should be doing, even if it means writing more code to add a sub-class constructor.
Comment #14
kristiaanvandeneyndeRe #12 it's not meant to be used everywhere but rather in classes that don't have proper DI to begin with, e.g.: forms. The upside to this approach is that the class you're extending (example: FormBase) can add new dependencies in its constructor without breaking the classes extending it.
There are a lot of classes in Drupal that do not use Symfony's DIC and instead are left with getting the container injected in a create or createInstance method. Trying to duckpunch those into using constructors properly boils down to merely injecting the container with extra steps.
Services in Drupal don't use the new static pattern to begin with and if you want to "extend" those you are usually either decorating them, which means the DIC will feed them their dependencies, or you are replacing them, which leaves you the option to declare any dependencies you like.
TL;DR: Wherever we use "new static" we are already injecting the container, so why bother with constructors in extending classes?
Comment #15
neclimdulI'm quite aware of how forms work, and my comments where directed at the ContainerInjectionInterface you're describing. The thing is, forms do have injection, the injection just isn't handled by the container. The ContainerInjectionInterface was carefully named and the documentation is very clear I think that its intended to inject into the constructor, not _be_ the constructor.
The reason injection happens this way boils down to basically two things.
1) No one liked writing a ton of service container logic and yaml to inject controllers. And it was clear a ton of unnecessary entries where going to end up on an already large and expensive to rebuild container. #2049159: Create a ControllerBase class to stop the boilerplate code madness
2) The alternative seemed to be to have a factory. This required the injection to be predefined or auto-wired and it turned out to be _very_ expensive and complicated to autowire injection on the fly (@see ReflectionFactory for an attempt with plugins).
The compromise was to have factories(or just inlined boiler plate) that delegated the injection part to this very limited method with _one_ purpose of being that glue to handle injection. We knew people might misuse the interface but every effort was made to make it clear not to.
So basically no. don't do that or the ghost of msonnabaum will hunt you down.