Create Rector rule for converting procedural hook implementations to OOP

Created on 16 October 2024, 3 months ago

Drupal version

Not in yet

Drupal Rector version

Problem / Motivation

When πŸ“Œ OOP hooks using event dispatcher Needs review lands it would be nice to have a rector rule to convert the procedural hooks into OOP attributes.

An attempt was made here https://git.drupalcode.org/-/snippets/195 to write the rule, but something is wrong with indentation.
πŸ› Incorrect indent in fixed up Rector generated file Active

I'll work on moving that Rector rule over to github, any advice on how to test it or update the indentation would be appreciated.

@chx suggested replacing the print with the print from api the api module.

πŸ“Œ Task
Status

Active

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States nicxvan

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

Comments & Activities

  • Issue created by @nicxvan
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    For local testing @bbrala mentioned this in slack:

    The test structure has a configured rule file which configures which rules to run. Check out the other tests for the structure. This is the same default structure as rector has itself.
    For locally, you could copy the default rector.php file and remove the sets and only add your rule.

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    I'm having trouble running this locally against announcements_feed.

    I am running this:

    ./vendor/bin/phpcbf \
      --standard="Drupal,DrupalPractice" -n \
      --extensions="php,module,inc,install,test,profile,theme" \
      core/modules/announcements_feed
    

    Here is a draft pr: https://github.com/palantirnet/drupal-rector/pull/308/files

    Locally I commented out the other rules just to test this one.

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    Ok I got this working with the PR here:

    I need some advice on how to include this since it's not a deprecation yet, we can use this just for core for now.
    My Draft comments out all of the drupal deprecations just for the hook convert rule.

    I had to read the README here: https://github.com/palantirnet/drupal-rector-sandbox

    How I got this working locally in ddev:
    First require drupal-rector
    ddev composer require --dev palantirnet/drupal-rector
    I then copied my changes from the PR: https://github.com/palantirnet/drupal-rector/pull/308/file

    Create rector web command in ddev:

    #!/bin/bash
    
    ## Description: Fix code
    ## Usage: rector
    ## Example: rector
    
    ./vendor/bin/rector \
      --config ./vendor/palantirnet/drupal-rector/rector.php \
      process \
      core/modules/announcements_feed
    

    Run ddev rector

    Create codefix command in ddev:

    #!/bin/bash
    
    ## Description: Sniff code
    ## Usage: cdf
    ## Example: cdf
    
    ./vendor/bin/phpcbf \
      --standard="Drupal,DrupalPractice" -n \
      --extensions="php,module,inc,install,test,profile,theme" \
      core/modules/announcements_feed/src/Hook
    

    BEFORE
    announcements_feed.module

    <?php
    
    /**
     * @file
     * Fetch community announcements from www.drupal.org feed.
     */
    
    use Drupal\announcements_feed\RenderCallbacks;
    use Drupal\Core\Link;
    use Drupal\Core\Routing\RouteMatchInterface;
    
    /**
     * Implements hook_help().
     */
    function announcements_feed_help($route_name, RouteMatchInterface $route_match) {
      switch ($route_name) {
        case 'help.page.announcements_feed':
          $output = '';
          $output .= '<h2>' . t('About') . '</h2>';
          $output .= '<p>' . t('The Announcements module displays announcements from the Drupal community. For more information, see the <a href=":documentation">online documentation for the Announcements module</a>.', [':documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed']) . '</p>';
          $output .= '<h2>' . t('Uses') . '</h2>';
          $output .= '<dl><dt>' . t('Accessing announcements') . '</dt>';
          $output .= '<dd>' . t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration toolbar, or access @link, to see all announcements relevant to the Drupal version of your site.', [
            '@link' => Link::createFromRoute(t('Announcements'), 'announcements_feed.announcement')->toString(),
          ]) . '</dd>';
          $output .= '</dl>';
          return $output;
      }
    }
    
    /**
     * Implements hook_toolbar().
     */
    function announcements_feed_toolbar() {
      if (!\Drupal::currentUser()->hasPermission('access announcements')) {
        return [
          '#cache' => ['contexts' => ['user.permissions']],
        ];
      }
      $items['announcement'] = [
        '#type' => 'toolbar_item',
        'tab' => [
          '#lazy_builder' => [
            'announcements_feed.lazy_builders:renderAnnouncements',
            [],
          ],
          '#create_placeholder' => TRUE,
          '#cache' => [
            'tags' => [
              'announcements_feed:feed',
            ],
          ],
        ],
        '#wrapper_attributes' => [
          'class' => ['announce-toolbar-tab'],
        ],
        '#cache' => ['contexts' => ['user.permissions']],
        '#weight' => 3399,
      ];
    
      // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
      // #attributes property to each toolbar item's tab child automatically.
      // Lazy builders don't support an #attributes property so we need to
      // add another render callback to remove the #attributes property. We start by
      // adding the defaults, and then we append our own pre render callback.
      $items['announcement'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
      $items['announcement']['#pre_render'][] = [RenderCallbacks::class, 'removeTabAttributes'];
      return $items;
    }
    
    /**
     * Implements hook_toolbar_alter().
     */
    function announcements_feed_toolbar_alter(&$items) {
      // As the "Announcements" link is shown already in the top toolbar bar, we
      // don't need it again in the administration menu tray, so hide it.
      if (!empty($items['administration']['tray'])) {
        $callable = function (array $element) {
          unset($element['administration_menu']['#items']['announcements_feed.announcement']);
          return $element;
        };
    
        $items['administration']['tray']['toolbar_administration']['#pre_render'][] = $callable;
      }
    }
    
    /**
     * Implements hook_theme().
     */
    function announcements_feed_theme($existing, $type, $theme, $path) {
      return [
        'announcements_feed' => [
          'variables' => [
            'featured' => NULL,
            'standard' => NULL,
            'count' => 0,
            'feed_link' => '',
          ],
        ],
        'announcements_feed_admin' => [
          'variables' => [
            'featured' => NULL,
            'standard' => NULL,
            'count' => 0,
            'feed_link' => '',
          ],
        ],
      ];
    }
    
    /**
     * Implements hook_cron().
     */
    function announcements_feed_cron() {
      $config = \Drupal::config('announcements_feed.settings');
      $interval = $config->get('cron_interval');
      $last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
      $time = \Drupal::time()->getRequestTime();
      if (($time - $last_check) > $interval) {
        \Drupal::service('announcements_feed.fetcher')->fetch(TRUE);
        \Drupal::state()->set('announcements_feed.last_fetch', $time);
      }
    }
    

    AFTER
    announcements_feed.module

    <?php
    
    /**
     * @file
     * Fetch community announcements from www.drupal.org feed.
     */
    use Drupal\Core\Hook\LegacyHook;
    use Drupal\announcements_feed\Hook\AnnouncementsFeedHooks;
    use Drupal\announcements_feed\RenderCallbacks;
    use Drupal\Core\Link;
    use Drupal\Core\Routing\RouteMatchInterface;
    
    /**
     * Implements hook_help().
     */
    #[LegacyHook]
    function announcements_feed_help($route_name, RouteMatchInterface $route_match) {
      \Drupal::service(AnnouncementsFeedHooks::CLASS)->announcementsFeedHelp($route_name, $route_match);
    }
    
    /**
     * Implements hook_toolbar().
     */
    #[LegacyHook]
    function announcements_feed_toolbar()
    {
        \Drupal::service(AnnouncementsFeedHooks::CLASS)->announcementsFeedToolbar();
    }
    
    /**
     * Implements hook_toolbar_alter().
     */
    #[LegacyHook]
    function announcements_feed_toolbar_alter(&$items) {
      \Drupal::service(AnnouncementsFeedHooks::CLASS)->announcementsFeedToolbarAlter($items);
    }
    
    /**
     * Implements hook_theme().
     */
    #[LegacyHook]
    function announcements_feed_theme($existing, $type, $theme, $path) {
      \Drupal::service(AnnouncementsFeedHooks::CLASS)->announcementsFeedTheme($existing, $type, $theme, $path);
    }
    
    /**
     * Implements hook_cron().
     */
    #[LegacyHook]
    function announcements_feed_cron()
    {
        \Drupal::service(AnnouncementsFeedHooks::CLASS)->announcementsFeedCron();
    }
    

    AnnouncementsFeedHooks.php

    <?php
    
    namespace Drupal\announcements_feed\Hook;
    
    use Drupal\announcements_feed\RenderCallbacks;
    use Drupal\Core\Hook\Hook;
    use Drupal\Core\Link;
    use Drupal\Core\Routing\RouteMatchInterface;
    
    /**
     *
     */
    class AnnouncementsFeedHooks {
    
      /**
       * Implements hook_help().
       */
      #[Hook('help')]
        public function announcementsFeedHelp($route_name, RouteMatchInterface $route_match) {
        switch ($route_name) {
          case 'help.page.announcements_feed':
            $output = '';
            $output .= '<h2>' . \t('About') . '</h2>';
            $output .= '<p>' . \t('The Announcements module displays announcements from the Drupal community. For more information, see the <a href=":documentation">online documentation for the Announcements module</a>.', [':documentation' => 'https://www.drupal.org/docs/core-modules-and-themes/core-modules/announcements-feed']) . '</p>';
            $output .= '<h2>' . \t('Uses') . '</h2>';
            $output .= '<dl><dt>' . \t('Accessing announcements') . '</dt>';
            $output .= '<dd>' . \t('Users with the "View drupal.org announcements" permission may click on the "Announcements" item in the administration toolbar, or access @link, to see all announcements relevant to the Drupal version of your site.', ['@link' => Link::createFromRoute(\t('Announcements'), 'announcements_feed.announcement')->toString()]) . '</dd>';
            $output .= '</dl>';
            return $output;
        }
        }
    
        /**
         * Implements hook_toolbar().
         */
        #[Hook('toolbar')]
        public function announcementsFeedToolbar() {
          if (!\Drupal::currentUser()->hasPermission('access announcements')) {
            return ['#cache' => ['contexts' => ['user.permissions']]];
          }
          $items['announcement'] = ['#type' => 'toolbar_item', 'tab' => ['#lazy_builder' => ['announcements_feed.lazy_builders:renderAnnouncements', []], '#create_placeholder' => \TRUE, '#cache' => ['tags' => ['announcements_feed:feed']]], '#wrapper_attributes' => ['class' => ['announce-toolbar-tab']], '#cache' => ['contexts' => ['user.permissions']], '#weight' => 3399];
          // \Drupal\toolbar\Element\ToolbarItem::preRenderToolbarItem adds an
          // #attributes property to each toolbar item's tab child automatically.
          // Lazy builders don't support an #attributes property so we need to
          // add another render callback to remove the #attributes property. We start by
          // adding the defaults, and then we append our own pre render callback.
          $items['announcement'] += \Drupal::service('plugin.manager.element_info')->getInfo('toolbar_item');
          $items['announcement']['#pre_render'][] = [RenderCallbacks::class, 'removeTabAttributes'];
          return $items;
        }
    
        /**
         * Implements hook_toolbar_alter().
         */
        #[Hook('toolbar_alter')]
        public function announcementsFeedToolbarAlter(&$items) {
          // As the "Announcements" link is shown already in the top toolbar bar, we
          // don't need it again in the administration menu tray, so hide it.
          if (!empty($items['administration']['tray'])) {
            $callable = function (array $element) {
              unset($element['administration_menu']['#items']['announcements_feed.announcement']);
              return $element;
            };
            $items['administration']['tray']['toolbar_administration']['#pre_render'][] = $callable;
          }
        }
    
        /**
         * Implements hook_theme().
         */
        #[Hook('theme')]
        public function announcementsFeedTheme($existing, $type, $theme, $path) {
          return ['announcements_feed' => ['variables' => ['featured' => \NULL, 'standard' => \NULL, 'count' => 0, 'feed_link' => '']], 'announcements_feed_admin' => ['variables' => ['featured' => \NULL, 'standard' => \NULL, 'count' => 0, 'feed_link' => '']]];
        }
    
        /**
         * Implements hook_cron().
         */
        #[Hook('cron')]
        public function announcementsFeedCron() {
          $config = \Drupal::config('announcements_feed.settings');
          $interval = $config->get('cron_interval');
          $last_check = \Drupal::state()->get('announcements_feed.last_fetch', 0);
          $time = \Drupal::time()->getRequestTime();
          if ($time - $last_check > $interval) {
            \Drupal::service('announcements_feed.fetcher')->fetch(\TRUE);
            \Drupal::state()->set('announcements_feed.last_fetch', $time);
          }
        }
    
    }
    
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    You can also see a more full test against the user module here: πŸ“Œ Test issue please ignore: Rector on user Active

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    You can also run ddev composer phpcbf

  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    I could use some advice on how to inject this as a separate convert command for core.

    We will likely want to add this for contrib too once procedural hooks are deprecated, but we have some time for that.

    It's obviously not ready for merging, but it's probably close enough for an approach review.

    There are several warnings in the rule that I can look at separately.

  • πŸ‡³πŸ‡±Netherlands bbrala Netherlands
  • πŸ‡ΊπŸ‡ΈUnited States nicxvan

    Thanks for taking a look, the extra changes were so I could run it locally until I got your advice.

    If you want to see it run against the user module you can see it here: https://git.drupalcode.org/project/drupal/-/merge_requests/9890/diffs

Production build 0.71.5 2024