Skip abstract classes (and interfaces) in HuxCompilerPass

Created on 19 June 2023, over 1 year ago
Updated 9 August 2024, about 2 months ago

Problem/Motivation

The HuxCompilerPass searches for classes with Hook or Alter attribute inside the \\Hooks\\ namespace.

Currently it does not skip abstract classes (and interfaces).
(It also does not check if methods are abstract, static, private/protected, but this is less of a problem because there is no reason to add the attribute here)
This is ok as long as these don't have the respective attributes.

But it can result in the following error:

The website encountered an unexpected error. Please try again later.
Error: Cannot instantiate abstract class Drupal\***\Hooks\**Base in Drupal\Component\DependencyInjection\Container->createService()

This becomes relevant in two types of cases:

  • A developer puts a hux attribute on a method that should not have it - e.g. an interface method, an abstract method, a static method. In this case we should throw an exception or something because it is clearly a mistake.
  • A developer puts a hux attribute on a non-abstract non-static method in a static class, so that all inheritors automatically implement this hook. This, I would say, is legitimate. In fact I was doing it and then noticed that it results in error.

A workaround is to put the abstract base class into a separate namespace/directory outside of /Hooks/.
But for code organization I prefer to have it inside that namespace.

Steps to reproduce

Create an abstract class MyHooksBase, inside /Hooks/ directory, and add one non-abstract method foo(), with a #[Hook('something')] attribute.
(optional) Create another class MyHooks extending MyHooksBase. Do not override any methods, just leave the class empty.

Expected: The MyHooks->foo() (defined in the base class) is registered as implementation for hook_something().
The MyHooksBase->foo() is ignored.

Actual: Error: Cannot instantiate abstract class Drupal\***\Hooks\MyHooksBase in Drupal\Component\DependencyInjection\Container->createService()

Proposed resolution

Add some checks in the compiler pass and during discovery:

  • Skip abstract classes and interfaces fully.
  • Warn or error out if a hux attribute is on a method that is private, protected, static, or abstract.
    (actually it could be interesting to have static methods as implementations, but this would be a separate discussion)

Remaining tasks

User interface changes

API changes

Data model changes

🐛 Bug report
Status

Needs review

Version

1.7

Component

Code

Created by

🇩🇪Germany donquixote

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @donquixote
  • 🇩🇪Germany donquixote

    MR is for 1.7.x.

  • Merge request !40Issue #3367717 "Skip abstract classes" → (Open) created by donquixote
  • Status changed to Needs review 2 months ago
  • 🇩🇪Germany donquixote

    The test cases cover some "hidden" features which already work today, but the support of which may not have been by design.

  • Pipeline finished with Success
    2 months ago
    #225597
  • Pipeline finished with Success
    2 months ago
    Total: 154s
    #225607
  • Pipeline finished with Success
    2 months ago
    Total: 151s
    #225675
  • Status changed to Needs work 2 months ago
  • 🇦🇺Australia dpi Perth, Australia

    (It also does not check if methods are abstract, static, private/protected, but this is less of a problem because there is no reason to add the attribute here)

    Yeah I dont see it as necessary. For what its worth, static is in fact something I want to allow 📌 Add static hook methods (and call them statically) Active .

    -

    Mostly accept the PR, except lets just stop at isInstantiable, and just ignore abstracts.

  • 🇩🇪Germany donquixote

    Mostly accept the PR, except lets just stop at isInstantiable, and just ignore abstracts. (tests and fixtures need updates as a result)

    We simply need to drop the second commit :)

    But I want to be clear: Currently the abstract class create() does work.
    So if we don't have the second commit, we lose functionality, and theoretically break BC.

    The example in the test is quite obscure. Perhaps somebody has a more valid use case.
    Of course such cases would be easy to fix.

    On the other hand, I can also imagine cases where this behavior would be clearly unintentional.

    I think it is good practice that a class with hook methods shall be either final or abstract, to clarify intent.
    Otherwise, if a hook method is inherited from a non-abstract base class, it is not clear if the developer intended this to be registered as a hook or not.
    But we can't really enforce this now.

  • 🇩🇪Germany donquixote

    We simply need to drop the second commit :)

    Or we could keep the test case from the second commit, but change the expected behavior.

  • Pipeline finished with Success
    2 months ago
    Total: 148s
    #227747
  • 🇩🇪Germany donquixote

    I added more test classes..

  • Pipeline finished with Success
    about 2 months ago
    Total: 145s
    #249298
  • Pipeline finished with Success
    about 2 months ago
    Total: 145s
    #249301
  • Pipeline finished with Success
    about 2 months ago
    Total: 145s
    #249509
  • Status changed to Needs review about 2 months ago
Production build 0.71.5 2024