Skip to content

Cover more patterns in Drupal framework #300

@marcelovani

Description

@marcelovani

Summary

Extends the Drupal framework resolver to cover the full range of hook and plugin patterns used in Drupal 10.2+ / 11.x projects. The initial PR (#268) added route extraction and procedural hook detection; this issue tracks the missing pieces.

Relates to #268


What is being fixed

1. OOP hooks via #[Hook] attribute (Drupal 10.2+)

Drupal 10.2 introduced class-based hook implementations using PHP 8 attributes. Three placement styles are supported:

Method-level (most common):

#[Hook('entity_presave')]
public function entityPresave(EntityInterface $entity): void { ... }

Class-level with method: parameter (Drupal's recommended style for dedicated src/Hook/ classes):

#[Hook('form_alter', method: 'formAlter')]
class FormHooks {
  public function formAlter(array &$form, ...): void { ... }
}

Class-level with __invoke():

#[Hook('cron')]
class CronHandler {
  public function __invoke(): void { ... }
}

Stacked attributes (one method implements multiple hooks — previously only the last was detected):

#[Hook('comment_insert')]
#[Hook('comment_update')]
public function commentSave(CommentInterface $comment): void { ... }

2. Plugin declarations — docblock annotations and PHP 8 attributes

Both pre-10.2 annotation style and modern attribute style are detected and linked to a canonical drupal:plugin:{PluginType} reference:

// Annotation style (pre-10.2)
/** @Block(id = "my_block", ...) */
class MyBlock extends BlockBase { ... }

// PHP 8 attribute style (10.2+)
#[Block(id: 'my_block', ...)]
class MyBlock extends BlockBase { ... }

// Views plugins, entity types, REST resources, etc.
#[ViewsField('my_field')]
class MyField extends FieldPluginBase { ... }

#[ContentEntityType(id: 'my_entity', ...)]
class MyEntity extends ContentEntityBase { ... }

DRUPAL_PLUGIN_TYPES expanded from 28 → 55 types, covering all plugin types converted to PHP attributes in Drupal core (#3396165): ContentEntityType, ConfigEntityType, EntityType, RestResource, Layout, SectionStorage, MediaSource, DataType, Archiver, Editor, HelpSection, ImageToolkitOperation, LanguageNegotiation, Mail, MigrateField, MigrateProcessPlugin, PageDisplayVariant, RenderElement, SearchPlugin, WorkflowType, and all remaining Views plugin types.

3. Symfony event subscribers via *.services.yml

Services tagged event_subscriber are extracted as component nodes with a references edge to the PHP class:

services:
  mymodule.event_subscriber:
    class: Drupal\mymodule\EventSubscriber\MySubscriber
    tags:
      - { name: event_subscriber }

Verification — real Drupal 11 project (custom + contrib modules)

Metric Before (initial PR) After (this PR)
Route nodes 1,397 1,397
Procedural hook refs 1,826 1,826
OOP hook refs (#[Hook]) 0 1,439
Plugin refs (decorates) 0 1,630
Event subscriber nodes 0 75

Test coverage

54 unit tests covering all patterns:

  • Procedural hooks (Strategy A docblock + Strategy B name pattern)
  • OOP hooks: method-level, class-level method:, class-level __invoke(), stacked attributes
  • Plugin annotations: @Block, @QueueWorker, @Filter, @ViewsField, @Layout, etc.
  • Plugin PHP 8 attributes: #[Block], #[Action], #[ViewsField], #[ContentEntityType], #[RestResource], etc.
  • Event subscribers: single, multiple, non-subscriber services ignored
  • Route extraction: single/multiple routes, HTTP methods, commented lines
  • Resolution: controller FQCN → method, form FQCN → class, plugin → base class, FQCN → subscriber class

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions