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

neclimdul created an issue. See original summary.

neclimdul’s picture

Issue summary: View changes
neclimdul’s picture

Oh hey past me, this is future me finding this through googling this error. :( I guess I should follow up with this.

jeffc518’s picture

+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()?

neclimdul’s picture

right 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.

Version: 9.1.x-dev » 9.2.x-dev

Drupal 9.1.0-alpha1 will be released the week of October 19, 2020, which means new developments and disruptive changes should now be targeted for the 9.2.x-dev branch. For more information see the Drupal 9 minor version schedule and the Allowed changes during the Drupal 9 release cycle.

guypaddock’s picture

I'd love it if this issue showed a before vs after code example, even if hypothetical, to understand what's being proposed in practice.

neclimdul’s picture

Issue tags: +sty

Here I am again... following a google search.

The before and after are pretty simple. As the title says, its just use new self instead of new static
Before:

class {
  /**
   * ......
   */

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('menu_link.static.overrides')
    );
  }
class {
  /**
   * ......
   */

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new self(
      $configuration,
      $plugin_id,
      $plugin_definition,
      $container->get('menu_link.static.overrides')
    );
  }
kingdutch’s picture

I agree that this change should be made but this requires a change in the ContainerFactoryPluginInterface annotation as well.

This is mostly to make PHPStan happy:

Method Drupal\social_chat\Plugin\GraphQL\DataProducer\Mutation\SendUserMessage::create() should return static(Drupal\social_chat\Plugin\GraphQL\DataProducer\Mutation\SendUserMessage) but returns Drupal\social_chat\Plugin\GraphQL\DataProducer\Mutation\SendUserMessage.                                                                    

Technically this may make it an API break? Specifically, in cases where a constructor was overwritten but create was not. The majority of those cases will probably break in other fun and unexpected ways though.

Version: 9.2.x-dev » 9.3.x-dev

Drupal 9.2.0-alpha1 will be released the week of May 3, 2021, which means new developments and disruptive changes should now be targeted for the 9.3.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

kristiaanvandeneynde’s picture

I 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.

  /**
   * The foo service.
   *
   * @var \Drupal\foo\ServiceInterface
   */
  protected $fooService;

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    $instance = new static($configuration, $plugin_id, $plugin_definition);
    $instance->fooService = $container->get('foo.service');
    return $instance;
  }

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

neclimdul’s picture

Issue tags: -sty

Webform 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)

  1. It either explicitly breaks or significantly complicates test-ability
  2. It obfuscates the classes dependencies.
  3. Specific to this issue, it doesn't solve the problem it just tricks static analysis.

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 self but I'm _super_ against putting everything in the factory method.

guypaddock’s picture

I 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.

kristiaanvandeneynde’s picture

Re #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?

neclimdul’s picture

I'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.

Version: 9.3.x-dev » 9.4.x-dev

Drupal 9.3.0-rc1 was released on November 26, 2021, which means new developments and disruptive changes should now be targeted for the 9.4.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.4.x-dev » 9.5.x-dev

Drupal 9.4.0-alpha1 was released on May 6, 2022, which means new developments and disruptive changes should now be targeted for the 9.5.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.5.x-dev » 10.1.x-dev

Drupal 9.5.0-beta2 and Drupal 10.0.0-beta2 were released on September 29, 2022, which means new developments and disruptive changes should now be targeted for the 10.1.x-dev branch. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.