How best to handle "Unsafe usage of new static()" PHPStan errors

Published December 13, 2023

PhpStan logoAs a Drupal module developer, if you're as big of a fan of PHPStan as I am, then you'll no doubt have run across the Unsafe usage of new static() error that PHPStan throws when analyzing code at level 0. In this article I will describe what causes this error and provide four possible solutions, weighing the pros and cons of each.

Generally, this will occur in a create() method when you are injecting a dependency into a class using either ContainerFactoryPluginInterface or ContainerInjectionInterface using the common (simplified) Drupal pattern of:

class baseClass {

 public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler) {
   parent::__construct($configuration, $plugin_id, $plugin_definition);
   $this->moduleHandler = $module_handler;
 }
 
 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
   return new static(
     $configuration,
     $plugin_id,
     $plugin_definition,
     $container->get('module_handler'),
   );
 }
 
}

Basically, PHPStan is warning you that there could be an issue if the class is extended and the new class overrides the constructor with different arguments. For example, if the Token service class is added:

class newClass extends baseClass {

 public function __construct(array $configuration, $plugin_id, $plugin_definition, Token $token, ModuleHandlerInterface $module_handler) {
   parent::__construct($configuration, $plugin_id, $plugin_definition);
   $this->token = $token;
   $this->moduleHandler = $module_handler;
 }
 
}

At this point, when newClass is instantiated by the container, it will fail because its inherited create() method no longer matches its overridden constructor. This is bad.

Now, this coding pattern is all over Drupal core and contributed modules, and we are generally very careful about breaking things (thank you, automated tests.) Regardless, PHPStan isn't cool with it, so let's look at some alternatives. 

Dig hole in sand, stick head in it

Easiest, but probably not a good long-term solution.

PHPStan provides the ability for certain errors to be ignored, and this is definitely a good candidate for ignoring. Additionally, it is simple enough to do with a quick addition to your phpstan.neon configuration file:

parameters:
   level: 0
   paths:
       - web/modules
   ignoreErrors:
       - '#Unsafe usage of new static\(\)#'

Generally, I'm not a big fan of ignoring any code quality issues, whether they are from phpcs, PHPStan or any other code quality tool.

Pros

  • PHPStan error goes away

Cons

  • That nagging feeling that you really didn't fix anything.

Prevent others from extending the class

Easiest for custom code, with limitations.

If using new static is only an issue when the class is extended, then why not just prevent the class from being extended with the addition of the final keyword?

final class baseClass {
 …
}

This definitely solves the issue, and in many cases, is a valid solution. I almost always use final when writing a custom module - especially when I am in control of the code base.

Using final in core or a contributed module is a bit trickier, as in most cases the assumption does have to be made that someone somewhere is going to extend that class for reasons you haven't thought of. Being the co-maintainer of several contributed modules, I'm experimenting with using it - to see if anyone opens an issue letting me know that it is blocking them in some way or another. Time will tell.

Pros

  • PHPStan is happy.
  • Code protection against someone extending the class and not properly overriding the constructor and create() methods (if necessary).

Cons

  • The class cannot be extended, possibly limiting its usefulness.

Change new static to new self

Most correct, but with potential repercussions.

Recommended by a Drupal.org documentation page, but with little explanation, this change looks like this (from our example above:)

 public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
   return new self(
     $configuration,
     $plugin_id,
     $plugin_definition,
     $container->get('module_handler'),
   );
 }

In this case, if the class is extended and the constructor is overridden, then the code will always fail unless the create() method is also overridden. Curiously, the documentation page also suggests adding final to the class. I suspect that the documentation page should be updated, but I'm not confident enough in what is the best way to handle this to make a suggestion. Yet.

Pros

  • PHPStan is happy.
  • This is probably the "most correct" solution.

Cons

  • While the class can be extended, less experienced developers might not always recognize that they must override the create() method when overriding the constructor, potentially leading to a bug that can be tricky to figure out. For example, if the create() method isn't overridden, then the result would be that the base class is created and not the (expected) extended class. 

Make the constructor final

Similar to marking the entire class "final".

I haven't spotted this potential solution in a Drupal community discussion, but on the PHPStan blog, one of the suggestions is to just make the constructor final. Again updating our code sample from above:

 final public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleHandlerInterface $module_handler) {
   parent::__construct($configuration, $plugin_id, $plugin_definition);
   $this->moduleHandler = $module_handler;
 }

This seems like a slightly more flexible solution than making the entire class final or changing from new static to new self, but still would limit anyone extending the class. Maybe this is a reasonable middle-of-the-road solution? I haven't seen it implemented in Drupal yet.

This method would allow the class to be extended, but the constructor wouldn't be allowed to be overridden - which means no dependency changes can be made. Obviously, this limits what extended classes would be able to do.

Similar to this method, the blog post also suggested adding the constructor signature to an interface, which would have the same effect of locking it down so that it cannot be changed.

Pros

  • PHPStan is happy.
  • Code protection against someone extending the class and not properly overriding the constructor and create() methods by not allowing those methods to be overridden. 

Cons

  • If the constructor can't be overridden, then while the class can be extended, its dependencies cannot be changed, potentially limiting its usefulness. 

UPDATE: December 18, 2023 Based on Jurgen's comment below, it turns out marking the contructor as final is probably the best solution as subclasses can then add additional dependencies using a create() method using the pattern:

  /**
  * {@inheritdoc}
  */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher) {
    $instance = parent::create(
      $container,
      $configuration,
      $plugin_id,
      $plugin_definition,
      $event_dispatcher
    );
    $instance->moduleHandler = $container->get('module_handler'));
    return $instance;
  }

This allows the subclass to be able to utilize the parent create() method while also adding new dependencies to the subclass without calling or overwriting the parent class's constructor. 

Summary

So where does that leave us? Hopefully, with the ability to make a more informed decision on how to handle this particular PHPStan error. As I mentioned above, I'm not a big fan of ignoring PHPStan errors, so for now, I'll be adding final most of the time 😉.

Additional reading

Thanks to Professional Module Development course alumni James Shields and Hanane Kacemi as well as Ryan Price for reviewing this blog post.

Comments

Great article, thanks a lot Michael. We have been on this topic the last many months as well as we're having hundreds of plugins or custom forms in most of our modules, e.g. in ECA. And we came up with another way, that only uses the create method without a constructor in the implementing sub-class. As it happened, PhpStan recently updated their rules which broke one component of that construct but we found an improvement which makes that approach even better, and keeps PhpStan happy to.

In short, a create function can almost always look like the following, and no constructor required:


public static function create(ContainerInterface $container): static {
$instance = parent::create($container);
$instance->time = $container->get('datetime.time');
return $instance;
}

That allows to inject more dependencies without ever having to wonder about changes in the parent class.

A detailed discussion about this topic also took place in Slack: https://drupal.slack.com/archives/C033S2JUMLJ/p1702454384195689

Submitted by Jürgen Haas (not verified) on Fri, 12/15/2023 - 05:27

If you change "new static" to "new self" and someone extends the class but does not add their own create() and __construct(), then it will create the base class instead of the extended class.

Submitted by Guest (not verified) on Thu, 01/04/2024 - 13:02

Add new comment

Sign up to receive email notifications of whenever we publish a new blog post or quicktip!

Name
CAPTCHA