Problem/Motivation

The DisplayPluginBase class uses services but not they are not properly injected so it cannot be tested properly.

Steps to reproduce

N/A

Proposed resolution

Inject these services for better unit testability. Do that in the child classes too

Remaining tasks

Add BC layer
Review
Commit

User interface changes

N/A

API changes

Any class extending \Drupal\views\Plugin\views\display\DisplayPluginBase or its children will require the following parameters to their constructor

\Drupal::service('views.views_data'),
\Drupal::service('plugin.manager.views.access'),
\Drupal::service('plugin.manager.views.cache'),
\Drupal::service('plugin.manager.views.display_extender'),
\Drupal::service('plugin.manager.views.exposed_form'),
\Drupal::service('plugin.manager.views.pager'),
\Drupal::service('plugin.manager.views.row'),
\Drupal::service('plugin.manager.views.style'),
\Drupal::service('plugin.manager.views.query'),

Data model changes

N/A

Release notes snippet

N/A

Original report by dawehner

For better unit testability this converts the display base plugin to get it's dependencies injected.

Issue fork drupal-2015121

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

dawehner’s picture

Status: Active » Needs review
StatusFileSize
new14 KB

At least the frontpage works, so let's see.

Status: Needs review » Needs work

The last submitted patch, vdc-2015121-1.patch, failed testing.

damiankloip’s picture

This looks crazy, but it does make sense!

+++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.phpundefined
@@ -99,6 +106,165 @@
+  public function __construct(array $configuration, $plugin_id, array $plugin_definition, ViewsData $views_data, ViewsPluginManager $access_plugin_manager, ViewsPluginManager $argument_default_plugin_manager, ViewsPluginManager $argument_validator_plugin_manager, ViewsPluginManager $cache_plugin_manager, ViewsPluginManager $display_extender_plugin_manager, ViewsPluginManager $exposed_form_plugin_manager, ViewsPluginManager $query_plugin_manager, ViewsPluginManager $pager_plugin_manager, ViewsPluginManager $row_plugin_manager, ViewsPluginManager $style_plugin_manager, UrlGenerator $url_generator) {

I know we usually put the constructor on one line, but this seems like a special case.

+++ b/core/modules/views/lib/Drupal/views/Plugin/views/display/DisplayPluginBase.phpundefined
@@ -99,6 +106,165 @@
+      $container->get('url_generator'2015121)

Hmm, that might be the problem here :)

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new14.02 KB
new2.15 KB

Ha! What do you think about this line-structure?

Status: Needs review » Needs work

The last submitted patch, vdc-2015121-4.patch, failed testing.

dawehner’s picture

Yeah this failure is a proper one. I still think that caching render arrays does not work, as now you have all kind of objects on there.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new2.59 KB
new16.24 KB

YES!

It is ugly but it works.

Status: Needs review » Needs work

The last submitted patch, vdc-2015121-7.patch, failed testing.

dawehner’s picture

Status: Needs work » Needs review
StatusFileSize
new16.4 KB

Just a rerole.

The last submitted patch, vdc-2015121-9.patch, failed testing.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

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

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

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

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

Drupal 9.3.15 was released on June 1st, 2022 and is the final full bugfix release for the Drupal 9.3.x series. Drupal 9.3.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.4.x-dev branch from now on, and new development or disruptive changes should 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.4.x-dev » 9.5.x-dev

Drupal 9.4.9 was released on December 7, 2022 and is the final full bugfix release for the Drupal 9.4.x series. Drupal 9.4.x will not receive any further development aside from security fixes. Drupal 9 bug reports should be targeted for the 9.5.x-dev branch from now on, and new development or disruptive changes should 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.

dimitriskr made their first commit to this issue’s fork.

dimitriskr changed the visibility of the branch 11.x to hidden.

andypost’s picture

Version: 9.5.x-dev » 11.x-dev
Issue summary: View changes
Issue tags: +Needs issue summary update
dimitriskr’s picture

Issue summary: View changes
dimitriskr’s picture

Issue summary: View changes
Issue tags: -Needs issue summary update

I've also created a draft CR for this change

dimitriskr’s picture

Status: Needs work » Needs review

Finally, ready for review.

P.S. I've deliberatly put the issue node instead of the draft CR link to the trigger_error(), per a conversation with @berdir at Slack.

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative, +Needs issue summary update

Left a comment on the MR.

But think the MR and maybe title should be updated as not super clear why the Rest plugin is needed to be updated.

dimitriskr’s picture

Issue summary: View changes
Status: Needs work » Needs review
Issue tags: -Needs issue summary update

RestExport plugin is updated because it extends PathPluginBase, which itself extends DisplayPluginBase

smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Feedback appears to be addressed.

catch’s picture

Status: Reviewed & tested by the community » Needs work

I think we should use constructor property promotion here.

dimitriskr’s picture

Status: Needs work » Needs review
Issue tags: +GreeceSpringSprint2024
smustgrave’s picture

Status: Needs review » Reviewed & tested by the community

Good call on the constructor promotion.

For the follow up issue of removing these deprecations would recommend tagging for novice. Would be excellent for new users.

alexpott’s picture

Status: Reviewed & tested by the community » Needs work

As per @catch's comment on the MR we can remove the constructor docs everywhere and we can use property promotion on all the classes.

larowlan’s picture

Status: Needs work » Reviewed & tested by the community

The issue summary has 'Add BC layer' deleted - this is a fairly common extension point.
We're not obligated to provide a BC layer, but should we do the right thing and try to avoid hard breaking people's things without warning?

larowlan’s picture

Status: Reviewed & tested by the community » Needs work
dimitriskr’s picture

Issue summary: View changes
Status: Needs work » Needs review

Added the constructor property promotion.
About the constructor docs, I cannot remove them because PHPCS will complain and tests will fail. Do I need to add a phpcs bypass for this to work?

@larowlan BC layer exists, it's crossed out because it is already done

Edit: A fix of this #3459746: [meta] Method getMockForAbstractClass() of class PHPUnit\Framework\TestCase is deprecated in PHPUnit 10 has been implemented for \Drupal\Tests\views\Unit\Plugin\display\PathPluginBaseTest. Shall I separate it?

dimitriskr’s picture

Issue summary: View changes
smustgrave’s picture

Status: Needs review » Needs work

@dimitriskr sorry haven't had a chance to get to this sooner. I believe we have probably missed the 11.1x window mind updating those for 11.2.x please

I'd assign the issue to you if I could

dimitriskr’s picture

Status: Needs work » Needs review
Issue tags: +GreeceWinterSprint2024
smustgrave’s picture

Status: Needs review » Needs work

Left comments and questions on the MR.

dimitriskr’s picture

Status: Needs work » Needs review
needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

dimitriskr’s picture

Status: Needs work » Needs review
godotislate’s picture

I was going to suggest that these constructors are good candidates for autowiring (though maybe in a follow up), since #3452852: Add create() factory method with autowired parameters to PluginBase is in. But there are container parameters involved here, which AutowireTrait/AutowireInstanceTrait don't support yet, so we'd need a an issue first for them to support autowiring container parameters.

Separately I wonder if it'd be useful for AutowireTrait/AutowireInstanceTrait::create*() methods to support the #Required attribute to do setter injection, which could make extending DisplayPluginBase easier.

godotislate’s picture

Status: Needs review » Reviewed & tested by the community

I think there might be performance benefit from loading the services lazily into the plugin classes via service closures, but we'd need #3544994: Make service closures serializable in classes using DependencySerializationTrait in first. If we don't need to wait for that, then this lgtm.

godotislate’s picture

It might be good to rebase to check that PHP 8.5 tests pass.

dimitriskr’s picture

longwave’s picture

Status: Reviewed & tested by the community » Needs review

This is a lot of dependencies to inject - the number of constructor arguments is kinda scary - and as #49 points out they are not always used.

Maybe all the views plugin managers should be put into a single service locator and they can be instantiated on demand from there?

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new90 bytes

The Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".

This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

longwave’s picture

Pushed an alternative approach using a service locator but because there are a lot of subclasses this is going to be super painful to implement.

I think setter injection with #[Required] is the way to go.

dimitriskr’s picture

@longwave, if you push an example with the #[Required] or link to another issue, I could work on this new approach

godotislate’s picture

Thinking about this a bit more, if the primary concern is to be able to use mocks in unit test, I think setters can be added here without being blocked on #3558306: Support automatic setter injection using the #[Required] attribute in AutowireTrait/AutowiredInstanceTrait (and/or #3558292: Support passing container parameters with the Autowire attribute in AutowireTrait and AutowiredInstanceTrait to be able to autowire container parameters in plugins, etc.). A getter can be used as well to replace to replace all the Views::somethingManager() calls.

It'd be something like:

public function setAccessManager(ViewsPluginManager $accessManager): static {
  $this->accessManager = $accessManager;
  return $this;
}

...

protected function getManager(string $type): ViewsPluginManager {
  $property = $type . 'Manager';
  if (!isset($this->{$property})) {
    $this->{$property} = Views::pluginManager($type);
  }
  return $this->{$property};
}

Then replace the uses of Views::pluginManager($type) with getManager(string $type).

In unit tests, you can then inject mocks into the display with the setters.

Once the #[Required] issue is in, we can add a follow up to put the attribute on all the setters in DisplayPluginBase, then there's the performance issue mentioned in #49 where all the service dependencies are being instantiated, possibly unnecessarily, when the plugin class is instantiated.

longwave’s picture

I think the service locator approach from MR!14258 is somewhat nicer, and solves the instantiation problem as well. Maybe we could split this up and add the service locator and change to Views.php in another issue, then we can start to deprecate ::pluginManager() and ::handlerManager() as there is a new service that replaces those methods?

godotislate’s picture

I think the service locator approach from MR!14258 is somewhat nicer

Oh, do you mean using a setter to inject the locator? If so, yes, that is a great idea and much cleaner.

longwave’s picture

Yes, if we add something like this to DisplayPluginBase:

protected ContainerInterface $pluginManagers;

#[Required]
public function setPluginManagers(#[Autowire(service: 'views.plugin_managers') ContainerInterface $pluginManagers): void {
  $this->pluginManagers = $pluginManagers;
}

then this avoids the deprecation dance in every Views display plugin that would otherwise need to add this itself.

godotislate’s picture

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.

mstrelan’s picture

#3558306: Support automatic setter injection using the #[Required] attribute in AutowireTrait/AutowiredInstanceTrait has landed.

Wondering if we can use property hooks, or asymmetric visibility on the property to skip the setter. Looks like it would be possible -
https://github.com/symfony/symfony/blob/8.1/src/Symfony/Component/Depend...

godotislate’s picture

I think we're still waiting on #3558292: Support passing container parameters with the Autowire attribute in AutowireTrait and AutowiredInstanceTrait here, but it's close.

Property hooks are somewhat blocked right now because of PHPStan (well, mglaman/phpstan-drupal) and PHPCS:
https://github.com/mglaman/phpstan-drupal/pull/935
https://github.com/PHPCSStandards/PHP_CodeSniffer/issues/734

The first likely will have resolution soon, but PHPCS will probably be a while. There've been discussions on slack on whether PHPCS should be a blocker for property hooks.

Looking at the documentation for asymmetric properties: https://www.php.net/manual/en/language.oop5.visibility.php#language.oop5...

Specifically, the set visibility may be specified separately, provided it is not more permissive than the default visibility.

From there, it doesn't seem like it would work? The property needs to be public set for #[Required].

mstrelan’s picture

Makes sense. I think it could be public readonly but not sure how people feel about that.

godotislate’s picture

I'm not sure readonly would work, because the value would be set outside the constructor. We also can't use readonly on the properties set by a setter.

mstrelan’s picture

I was mistaken again. Oh well, I guess the only way this would work is if the #[Required] magic worked from inside the class, e.g. via a trait, but it doesn't. Let's stick with setter methods then.

godotislate’s picture

I'm not personally in favor of having service properties be public, but if people do like the DX of using the Required attribute directly on properties, I don't feel that strongly against it. That said, the setter is already in, and we'd have to open an issue for Required properties. I do vaguely wonder about how much the Autowire* traits for these non-service objects will eventually need to expand to re-create what Symfony's container compiler passes do, but that's neither here nor there for now.

longwave’s picture

Updated to use setter injection and #[Required] as in #61, but there is a problem: the Page plugin has a create() method that overrides the autowire create() so the setter injection never gets called for Page. We could remove create() here but it doesn't solve it for contrib display plugins that might already have that method.

Perhaps the only solution is a getPluginManagers() fallback that calls \Drupal::service() that we can eventually deprecate?

dimitriskr’s picture

@longwave, I think you should change the target branch on !14258 to `main`

dimitriskr’s picture

Also, in !5928 I injected the ViewsData (found in core/modules/views/src/Plugin/views/display/DisplayPluginBase.php#L812). Are we going to inject it here too? if not, should the title change to represent the plugin mangers only?

longwave’s picture

@dimitriskr let's figure out a way forward here with the plugin managers before we decide about injecting anything else.

Added support for #[Required] to ContainerFactory, not yet sure if this is a good idea though.

godotislate’s picture

Re: #71

Maybe we can make sure the #[Required] methods run from calls to
ClassResolver::getInstanceFromDefinition(), relevant implementations of FactoryInterface::createInstance() like ContainerFactory and ConstraintFactory and anywhere else that I'm missing.

For example in ContainerFactory:

public function createInstance($plugin_id, array $configuration = []) {
    $plugin_definition = $this->discovery->getDefinition($plugin_id);
    $plugin_class = static::getPluginClass($plugin_id, $plugin_definition, $this->interface);

    // If the plugin provides a factory method, pass the container to it.
    if (is_subclass_of($plugin_class, 'Drupal\Core\Plugin\ContainerFactoryPluginInterface')) {
      $instance = $plugin_class::create(\Drupal::getContainer(), $configuration, $plugin_id, $plugin_definition);
      foreach ($reflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $method) {
        if (!empty($method->getAttributes(Required::class))) {
          // Autowire the arguments and invoke the method.
        }
      }
      return $instance;
    }

    // Otherwise, create the plugin directly.
    return new $plugin_class($configuration, $plugin_id, $plugin_definition);
  }

I'm groaning because it makes the work we did in the trait seem redundant though.

longwave’s picture

@godotislate well I'm glad we had the same idea here, but we can reuse the trait if we break it up more! Still not sure this is the right way to go though, this might be a slippery slope - are there too many ways that plugins can be instantiated?

longwave’s picture

Oh, and I guess the setter is getting called twice now where create() isn't overridden...

godotislate’s picture

I think the performance thing is equivalent to if all the plugin classes were using the trait. Adding this funcitonality to the factory now means reflection is running to do the same thing twice, which should be something to follow up with.

Yes, I fear the slippery slope as well, but I have no ideas at the moment.

The getPluginManagers() fallback might otherwise be a good solution, but I think we're still blocked by #3558292: Support passing container parameters with the Autowire attribute in AutowireTrait and AutowiredInstanceTrait?

godotislate’s picture

I think the performance thing is equivalent to if all the plugin classes were using the trait.

So even if we removed the #[Required] stuff from the trait to the factories, reflection would still be running twice, but the setters would not be. So there would be some performance impact, though I _think_ it's not too bad.

longwave’s picture

Added a check that should prevent the setters being called twice, but it requires reflection to do so!