Rewriting Contributed Modules for Drupal 8

Kevin Kaland - wizonesolutions

Presentation source: https://github.com/wizonesolutions/rewriting-modules-d8

What's new in Drupal 8?

Everything.

Yes, I'll tell you more.

Learning curve

<insert learning curve image with people falling off that @mortendk uses all the time here>

Some D8 advice

  • D8 isn't actually that scary.
  • It's the concepts you already know expressed in a different way.
  • Most module-porting work is re-organizing the same code.
  • Drupal 8 made @mortendk the Happy Themer (session).

D8 is more consistent

  • Patterns are used pretty consistently in core
  • Your IDE will happily help you understand
  • You have to define more things, so it's easier to find where things are defined
  • Less magic, more Modern PHP patterns

Now let me save you some time

#learnfrommyfail

  • The cake documentation page called "Converting modules from 7.x to 8.x" is a lie. Don't bother with it.
  • Start with Drupal Module Upgrader (DMU) instead! It will tell you a lot of what you need to fix. https://drupal.org/project/drupalmoduleupgrader
  • There will be other things besides what it says. For that, look at the change records: https://drupal.org/list-changes (filtered by changes introduced in 8.x). These tend to be pretty well-written.
  • Start with simple things like permissions. Move on to your settings forms if there are any, then move on to more and more complex stuff.
    (Coder Upgrade is discontinued, by the way, but regular Coder is still around.)

Step 1: Make module work

fillpdf.info.yml


name: 'FillPDF'
type: module
description: 'Allows users to populate PDF forms from website data.'
core: 8.x
package: Other
version: VERSION
dependencies:
  - file
  - link
  - token
  - views

The new APIs

hook_menu() The routing system

fillpdf.routing.yml - Form example


# Form example
fillpdf.settings:
  path: '/admin/config/media/fillpdf'
  defaults:
    _form: '\Drupal\fillpdf\Form\FillPdfSettingsForm'
    _title: 'FillPDF settings'
  requirements:
    _permission: 'administer pdfs'
  options:
    _admin_route: TRUE

fillpdf.routing.yml - Entity form example


entity.fillpdf_form.edit_form:
  path: '/admin/structure/fillpdf/{fillpdf_form}'
  defaults:
    _entity_form: fillpdf_form.edit
    _title: 'Edit FillPDF form'
  requirements:
    _permission: 'administer pdfs' # todo: do we have an administer own pdfs perm?
  options:
    _admin_route: TRUE

fillpdf.routing.yml - Controller example


fillpdf.populate_pdf:
  path: '/fillpdf'
  defaults:
    _controller: '\Drupal\fillpdf\Controller\HandlePdfController::populatePdf'
  requirements:
    _custom_access: '\Drupal\fillpdf\FillPdfAccessController::checkLink'

FAPI

Pretty much the same

buildForm() method for non-entity forms

form() for entity forms

Return a good ol' render array

Validation, submission similar

$form_state is an object now!!!

variable_set(), variable_get() The Configuration API


// In a ConfigFormBase form
$config = $this->config('fillpdf.settings');
$fillpdf_service = $config->get('fillpdf_service_backend');

// Save form values.
$this->config('fillpdf.settings')
  ->set('fillpdf_service_backend', $form_state->getValue('fillpdf_service_backend'))
  ->set('fillpdf_api_key', $form_state->getValue('fillpdf_api_key'))
  ->set('fillpdf_remote_protocol', $form_state->getValue('fillpdf_remote_protocol'))
  ->set('fillpdf_pdftk_path', $form_state->getValue('fillpdf_pdftk_path'))
  ->save();

db_* Entities!

See fago's awesome slide deck.

It's good.

tl;dr: ContentEntityType and baseFieldDefinitions()

(or ConfigEntityType)

drupal_add_js(), drupal_add_css() .libraries.yml and #attached

fillpdf.libraries.yml


fillpdf.admin.settings:
  version: VERSION
  js:
    fillpdf.js: {}
  css:
    component:
      fillpdf.css: {}
  dependencies:
    - core/jquery
    - core/drupal

FillPdfSettingsForm::buildForm()


$form['#attached'] = array(
  'library' => array('fillpdf/fillpdf.admin.settings')
);

Dependency Injection

  • Makes your app testable
  • Dependencies can be mocked during tests
  • Easily lets your class get the context it needs without having to look it up itself

Secret tip!

  • Many change records show stuff like
    \Drupal::config('foo.bar')->get('baz')
    , but you should use the injected versions except in procedural code (which should be rare)
    • For example, in form controllers, you can use
      $this->config->get('foo.bar')->get('baz')
    • But read the Configuration API docs first :)
    • (OK, it wasn't really that secret.)
  • So what's going on in this example?

    
    class HandlePdfController extends ControllerBase {
    
      protected $linkManipulator;
    
      public function __construct(FillPdfLinkManipulatorInterface
         $link_manipulator) {
        $this->linkManipulator = $link_manipulator;
      }
    
      // ControllerBase implements \Drupal\Core\DependencyInjection\ContainerInjectionInterface,
      // which provides the create method and makes this work.
      public static function create(ContainerInterface $container) {
        // factory method; returns instance of *this* class
        return new static($container->get('fillpdf.link_manipulator'));
      }
    
    }

    This is also the pattern to inject classes you need when the class you're extending does not already.

    mymodule_global_function() Services

    • Provide application-wide functionality without destroying testability
    • Defined in services.yml
    • Services are just normal classes; nothing special to do inside service
    • Dependencies injected from the services.yml file
    • Not everything has to be a service. Static utility functions can also do the job

    fillpdf.services.yml

    
      services:
      plugin.manager.fillpdf_backend:
      class: Drupal\fillpdf\FillPdfBackendManager
      parent: default_plugin_manager
    
      fillpdf.link_manipulator:
      class: Drupal\fillpdf\Service\FillPdfLinkManipulator
    
      fillpdf.admin_form_helper:
      class: Drupal\fillpdf\Service\FillpdfAdminFormHelper
      arguments: ['@module_handler']
    

    Administrative FAPI tables Views in core!

    FillPdfFormViewsData.php

    
    class FillPdfFormViewsData extends EntityViewsData {
    
      /**
       * {@inheritdoc}
       */
      public function getViewsData() {
        $data = parent::getViewsData();
    
        $data['fillpdf_forms']['table']['group'] = $data['fillpdf_forms']['table']['base']['title'] = t('FillPDF forms');
    
        $data['fillpdf_forms']['table']['base']['help'] = t('FillPDF forms are uploaded on the FillPDF administration page and are used by the FillPDF module.');
    
        return $data;
      }
    
    }
    

    Views in core!

    • Then export the View in /admin/config/development/configuration
    • Pick Single Import/Export, Export, configuration type: View, configuration name: The name of the View you created
    • It tells you a filename. Place that in config/install the same way you would regular configuration

    In summary

    • Drupal 8 is not that scary
    • Drupal is competitive again
    • If you can't find docs, ask on Drupal Answers (preferred) or in #drupal-contribute on Freenode IRC
    • Backdrop is not the future
    • Message from @kvantomme: D8 module upgrade progress tailored to your site at d8upgrade.org

    Stay in touch

    Twitter: @wizonesolutions

    E-mail: Drupal.org contact form

    Website/blog: WizOne Solutions

    Continue to next slide for useful links!

    Reference material

    Change records for Drupal 8

    FillPDF homepage (see 8.x-4.x Git branch)

    My notes and links (pretty much everything I read)

    MERCI !

    Veuillez faîtes semblance que cette ligne contient une bonne blague pour que nous puissons tromper les étrangers.