Problem/Motivation

We're trying to deprecate some hooks in #2234479: Deprecate hook_test_* hooks in simpletest and #2460521: Deprecate hook_simpletest_alter()

It would be nice to be able to trigger E_USER_DEPRECATED for these hooks when they are invoked.

Note that one is for hooks and the other is for _alter hooks.

There are also at least two deprecated alter hooks in core already, in rest.api.php: hook_rest_type_uri_alter() and hook_rest_relation_uri_alter().

Proposed resolution

Add invokeDeprecated(), invokeAllDeprecated(), and alterDeprecated() to ModuleHandlerInterface.

These will act as facades for invoke(), invokeAll(), and alter(), which will behave normally, but will also check for implementations and trigger the deprecation error if there are any.

Remaining tasks

User interface changes

Adds explicit ways to deprecate hooks and alter hooks.

API changes

Adds methods to ModulerHandlerInterface.

Data model changes

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

Mile23 created an issue. See original summary.

Mile23’s picture

Status: Active » Needs review
FileSize
3.71 KB

This patch illustrates the idea and is WIP. It does not handle alter hooks.

Mile23’s picture

Issue summary: View changes
Mile23’s picture

Issue summary: View changes
Issue tags: +@deprecated
dawehner’s picture

+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
index 6b2738c779..c2cbc29afe 100644
--- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php

--- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
+++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php

+++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
@@ -239,6 +239,49 @@ public function invoke($module, $hook, array $args = []);
+   */
+  public function invokeDeprecated($module, $hook, array $args = []);
+

We could avoid changing the interface by having a module_handler.deprecated which is like a regular module handler but marks every function call as deprecated. Any thoughts about that?

Mile23’s picture

Having a separate service means that you have to grab two different services if you invoke hooks that are both deprecated and non-deprecated. So for some services that might mean changing the service definition and constructor signature just to do deprecations.

joachim’s picture

This looks good, though I am wondering whether there is a legitimate way in the documentation standards to say 'see moduleInvokeAll() for the parameters' rather than repeat them here. I think this would improve clarity that the method signature is identical, and that it's a drop-in replacement.

Mile23’s picture

Status: Needs review » Postponed

Postponing on #2488860: Bring phpunit bridge into drupal and use it for unit tests and simpletest to handle Deprecation because there's currently no way to get the triggered error into a human-useful form.

dawehner’s picture

Agreed ... we should ensure we add test coverage for that, and well, for that we first need actual capabilities for that.

Mile23’s picture

dawehner’s picture

Here is an alternative implementation using hook_hook_info().

Mile23’s picture

+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -412,6 +413,19 @@ public function invokeAll($hook, array $args = []) {
+    $hook_info = $this->getHookInfo();
+    if (isset($hook_info[$hook]['deprecated'])) {
+      @trigger_error($hook_info[$hook]['deprecated'], E_USER_DEPRECATED);
+    }

It seems like we'd only want to trigger the error if there are implementations.

We'd definitely want a test case where hooks defined in hook_hook_info() and hooks defined in *.module both trigger errors that are found by the symfony phpunit bridge.

dawehner’s picture

It seems like we'd only want to trigger the error if there are implementations.

I think you are absolutely right here :)

Mile23’s picture

Leaving this postponed.

This patch makes alterDeprecated() work.

Note that there's a kernel test class. This test will be more meaningful after #2488860: Bring phpunit bridge into drupal and use it for unit tests and simpletest to handle Deprecation

But... Applying the patch from that issue, re-composer installing, and then running the test results in a fail. So it may be that kernel tests have some magic error handling that we haven't accounted for.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
    index 6b2738c779..eadeebfad7 100644
    --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
    
    --- a/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
    +++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
    

    For better BC maybe add a new interface for that? To be honest I don't expect thought that anyone extends this particular class to be honest.

  2. +++ b/core/tests/Drupal/KernelTests/Core/Extension/ModuleHandlerTest.php
    @@ -0,0 +1,56 @@
    +   * @expectedDeprecation The deprecated alter hook hook_deprecate_alter() is implemented: deprecation_test_deprecate_alter
    ...
    +   * @expectedDeprecation The following modules implement the deprecated hook hook_deprecate(): deprecation_test
    

    It is a bit weird that the first message is about the concrete function name and the second is just containing the module name. I think using the full function makes it easier to find the particular implementation.

Mile23’s picture

For better BC maybe add a new interface for that? To be honest I don't expect thought that anyone extends this particular class to be honest.

module_handler is a service, so it needs one interface that all consumers can use. Since we can invoke() etc on ModuleHandlerInterface, the deprecated invoke has to be on the same interface.

It is a bit weird that the first message is about the concrete function name and the second is just containing the module name.

Easiest way to code it. :-) getImplementationInfo() doesn't return function names, but I guess we can assemble them.

Mile23’s picture

Status: Postponed » Needs review

Unpostponing since it looks like we've got some consensus in #2870058: [policy, no patch] Document how to deprecate hooks

dawehner’s picture

module_handler is a service, so it needs one interface that all consumers can use. Since we can invoke() etc on ModuleHandlerInterface, the deprecated invoke has to be on the same interface.

Fair, we can add new methods in minor releases ...

Status: Needs review » Needs work

The last submitted patch, 14: 2866779_14.patch, failed testing.

Mile23’s picture

Status: Needs work » Needs review
FileSize
12.09 KB
8.48 KB

Both error messages now show the method names instead of just the modules.

Moved the test to be a unit test, because kernel tests can't test deprecation errors at the moment.

Mile23’s picture

Issue tags: -Needs change notice +Needs change record updates, +needs documentation updates

Added a change notice: https://www.drupal.org/node/2881531

The change notice will need to be updated when the docs are updated, and when this issue is finalized.

Status: Needs review » Needs work

The last submitted patch, 20: 2866779_20.patch, failed testing.

Mile23’s picture

Status: Needs work » Needs review
FileSize
12.08 KB
1010 bytes

Fixed CS errors. No idea about the segmentation fault in the last test.

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
    @@ -414,6 +414,43 @@ public function invokeAll($hook, array $args = []) {
    +    $result = $this->invokeAll($hook, $args);
    +    $this->triggerDeprecationError($hook);
    
    @@ -503,6 +540,28 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
    +    // Invoke the alter hook. This has the side effect of populating
    +    // $this->alterFunctions.
    +    $this->alter($type, $data, $context1, $context2);
    ...
    +      @trigger_error($message, E_USER_DEPRECATED);
    

    Ubernitpick: Once the deprecation is before and once after the actual hook calls.

  2. +++ b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerDeprecatedTest.php
    @@ -0,0 +1,84 @@
    +class ModuleHandlerDeprecatedTest extends UnitTestCase {
    ...
    +    $this->moduleHandler->load('module_handler_test_deprecated');
    

    Given this loads code we should IMHO run the test in isolation/in its own separate process.

Mile23’s picture

Given this loads code we should IMHO run the test in isolation/in its own separate process.

Argh. :-)

So yah, we can't test this yet.

catch’s picture

@runTestsInSeparateProcesses should work for that I think.

dawehner’s picture

Status: Needs review » Needs work

Yeah totally, this is totally possible.

Mile23’s picture

Status: Needs work » Needs review
FileSize
12.12 KB
567 bytes

Hmm. I've been poking around the deprecation error problem with kernel and browser tests for #2870194: Ensure that process-isolated tests can use Symfony's PHPunit bridge to catch usages of deprecated code thinking that the reason the phpunit bridge doesn't work with them is because they're in isolated processes. That's why I was exasperated about this one.

Trying it now it seems to work locally, so yah.

Update: It passed locally because I had the 8.3.x dependencies set up in the repo.

Also, the other tests in core/tests/Drupal/Tests/Core/Extension have the same problem: They need to be isolated because they load modules.

Status: Needs review » Needs work

The last submitted patch, 28: 2866779_28.patch, failed testing.

Mile23’s picture

Mile23’s picture

Zeroing in on the fact that @runTestsInSeparateProcesses kills our ability to test deprecations: #2884530: Mark process-isolated test with @expectedDeprecation as risky

Mile23’s picture

Status: Needs work » Needs review
FileSize
11.14 KB
6.4 KB

Changed tests to mocked unit tests so we can proceed without blockers.

Mile23’s picture

Issue tags: -Needs change record updates, -needs documentation updates

Updated deprecation documentation: https://www.drupal.org/core/deprecation#how-hook

Updated change record: https://www.drupal.org/node/2881531

Re-running tests.

It would be really nice to get this in the 8.4.0 release so we can start deprecating stuff now.

dawehner’s picture

From my perspective this looks great!

+++ b/core/lib/Drupal/Core/Extension/ModuleHandlerInterface.php
@@ -239,6 +239,49 @@ public function invoke($module, $hook, array $args = []);
+   * @return mixed
+   *   The return value of the hook implementation.
+   */
+  public function invokeDeprecated($module, $hook, array $args = []);
...
+   *   done using array_merge_recursive().
+   */
+  public function invokeAllDeprecated($hook, array $args = []);

@@ -290,6 +333,60 @@ public function invokeAll($hook, array $args = []);
+   *   associative array as described above.
+   */
+  public function alterDeprecated($type, &$data, &$context1 = NULL, &$context2 = NULL);

I'm wondering whether we should put @see statements to the parallel methods && and then maybe get rid of the 1to1 copied documentation

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

Drupal 8.4.0-alpha1 will be released the week of July 31, 2017, which means new developments and disruptive changes should now 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.

Mile23’s picture

Moving back to 8.4.x because it would be super-duper awesome to be able to fully deprecate the simpletest module before 8.5.0.

alterDeprecated() makes sense to chop out because it's pretty long.

invokeDeprecated() and invokeAllDeprecated() are actually longer than the originals, explaining how the deprecation works.

The implementation just has {@inheritdoc} which is right and good.

Added @see to all the new methods, pointing back to the non-deprecated versions.

dawehner’s picture

Thank you! I think we have a solid plan now.

@Mile23
Do you think we need an actual module providing it to have some sort of complete test coverage?

Mile23’s picture

@dawehner: No, we can't do that because we can't use @expectedDeprecation in process-isolated tests. See the interdiff in #32, and comments here: #2870194-22: Ensure that process-isolated tests can use Symfony's PHPunit bridge to catch usages of deprecated code

dawehner’s picture

Well, we could write a test which ensures that the hooks are actually triggered ...

Mile23’s picture

+++ b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerDeprecatedTest.php
@@ -0,0 +1,97 @@
+    $handler->expects($this->once())
+      ->method('alter');
...
+    $handler->expects($this->once())
+      ->method('invoke')
+      ->willReturn('invoked');
...
+    $handler->expects($this->once())
+      ->method('invokeAll')
+      ->willReturn('invokedAll');

Like this? Because if we make an actual module and then invoke with deprecation, there will be a deprecation error which will fail the test. We can't prevent the deprecation error because @expectedDeprecation doesn't work for process-isolated tests, as pointed out above. It's the reason for the failing test in #28.

So until we've fixed #2870194: Ensure that process-isolated tests can use Symfony's PHPunit bridge to catch usages of deprecated code we can't test this deprecation feature using kernel or functional tests. And that issue is blocked because phpunit-bridge is already a kludge. https://github.com/symfony/symfony/issues/23003

So the solution is to unit test it, which is the only way to test a deprecation with @expectedDeprecation.

Mile23’s picture

Mile23’s picture

dawehner’s picture

  1. +++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
    @@ -411,6 +411,43 @@ public function invokeAll($hook, array $args = []) {
    +      @trigger_error($message . implode(', ', $hooks), E_USER_DEPRECATED);
    
    @@ -502,6 +539,28 @@ public function alter($type, &$data, &$context1 = NULL, &$context2 = NULL) {
    +      $message = 'The deprecated alter hook hook_' . $type . '_alter() is implemented in these functions: ' . implode(', ', $this->alterFunctions[$cid]);
    +      @trigger_error($message, E_USER_DEPRECATED);
    

    It would be kind of nice to say: Read more on hook_$hook() what you should do instead. As alternative maybe invokeDeprecated etc. could take an additional message parameter?

  2. +++ b/core/tests/Drupal/Tests/Core/Extension/ModuleHandlerDeprecatedTest.php
    @@ -0,0 +1,97 @@
    + * We have to build elaborate mocks for these tests because
    + * symfony/phpunit-bridge doesn't deal well with kernel tests or
    + * process-isolated tests.
    + * @see https://www.drupal.org/node/2884530
    ...
    +class ModuleHandlerDeprecatedTest extends UnitTestCase {
    

    We could remove this comment now. Maybe though testing this using a unit test isn't a good idea in the first place. Any opinion about that?

Mile23’s picture

Good idea adding specific deprecation messages.

Removed the unit test.

Added a test case for unimplemented hooks to make sure they don't trigger errors.

Edit: The unimplemented hooks test will need to be in a different class, since that one says @group legacy.

Mile23’s picture

Component: other » base system
FileSize
12.3 KB
2.57 KB

Re-arranged the tests for no false positives.

larowlan’s picture

Status: Needs review » Reviewed & tested by the community
+++ b/core/lib/Drupal/Core/Extension/ModuleHandler.php
@@ -411,6 +411,45 @@ public function invokeAll($hook, array $args = []) {
+      $hooks = [];
+      foreach ($modules as $module) {
+        $hooks[] = $module . '_' . $hook . '()';
+      }

nit: i'd use an array_map here, but that's just a personal preference

This looks ready to me, would be great to ship 8.5.0 with this.

Status: Reviewed & tested by the community » Needs work

The last submitted patch, 45: 2866779_45.patch, failed testing. View results

larowlan’s picture

Status: Needs work » Reviewed & tested by the community

Unable to allocate pool apcu issue again

Mile23’s picture

Status: Reviewed & tested by the community » Needs review
FileSize
12.33 KB
1011 bytes

Fixes nits from #46.

larowlan’s picture

Status: Needs review » Reviewed & tested by the community

thanks

Wim Leers’s picture

Nice!

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed 5485720 and pushed to 8.5.x. Thanks!

  • catch committed 5485720 on 8.5.x
    Issue #2866779 by Mile23, dawehner: Add a way to trigger_error() for...

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.