StackStarter.io/spin
Spin Code: TRAININGDAY

Drupal 8

Module Development
A test drive

Nick Selvaggio / www.segosolutions.com

A little about me.

A little about you.

Play Along!

StackStarter.io/spin
Spin Code: TRAININGDAY

Let's talk Drupal...

Where have we come from?

A world of Drupalisms...

Where are we now?

A whole new world...

This new world includes:

A modernized architecture and consistant APIs, while still being Drupal!

Lets step back...

The larger PHP community has been evolving

Getting off the island.

What components is D8 using?

Symfony Components (HTTP Foundation, Routing, Dependency Injection, YAML, etc)

Doctrine Annotations

Twig theme system

PHP Unit for testing

For a full list:
/core/composer.json

Lets Review our Object Oriented Concepts

The 4 Pillars of OOP

Abstraction

Encapsulation

Inheritance

Polymorphism

Lets review some resources

  • https://api.drupal.org/api/drupal/8
  • https://www.drupal.org/documentation
  • https://www.drupal.org/project/examples
  • https://www.drupal.org/list-changes

Lets dive in!

Stucture of a module

/modules/custom/hello/hello.info.yml


name: Hello
description: A friendly Drupal 8 module
type: module
core: 8.x
            

Modules now live in the /modules directory

We don't need a .module file anymore!

Lets make a Page...

Without hook_menu!

/modules/custom/hello/src/Controller/HelloController.php

/**
 * Our first Drupal 8 controller.
 */
namespace Drupal\hello\Controller;

use Drupal\Core\Controller\ControllerBase;

class HelloController extends ControllerBase {
  public function sayHi() {

    return array(
        '#markup'=>"Why hello Drupal 8",
      );
  }
}
            

/modules/custom/hello/hello.routing.yml

              
              hello.sayHi:
  path: '/hello'
  defaults:
    _controller: '\Drupal\hello\Controller\HelloController::sayHi'
  requirements:
    _permission: 'access content'

              
            

What about passing arguments to a route?

/modules/custom/hello/src/Controller/HelloController.php

/**
 * Our first Drupal 8 controller.
 */
namespace Drupal\hello\Controller;

use Drupal\Core\Controller\ControllerBase;

class HelloController extends ControllerBase {
  public function sayHi($name) {
    $phrase = $this->t("Hello @name", array("@name"=>$name));

    return array(
        '#markup'=>$phrase,
      );
  }
}
            

/modules/custom/hello/hello.routing.yml

							
							hello.sayHi:
  path: '/hello/{name}'
  defaults:
    _controller: '\Drupal\hello\Controller\HelloController::sayHi'
    name: 'Nick'
  requirements:
    _permission: 'access content'

							
						

Awesome!

Now lets get a bit more strict with our route matching

/modules/custom/hello/hello.routing.yml

              
              hello.sayHi:
  path: '/hello/{name}'
  defaults:
    _controller: '\Drupal\hello\Controller\HelloController::sayHi'
    name: 'Nick'
  requirements:
    _permission: 'access content'
    name: ^N[a-z]+
              
            

Our route is now a bit antisocial

Lets add a menu link

/modules/custom/hello/hello.links.menu.yml

              
                hello.sayHi:
  title: "Say Hi"
  route_name: hello.sayHi
  menu_name: tools
              
            

What about permissions?

/modules/custom/hello/hello.permissions.yml

              
                request hello:
  title: 'Request Hello'
              
            

What about theming our content?

This will look familar

/modules/custom/hello/hello.module

            
              /**
 * Implements hook_theme
 */
function hello_theme() {
  $theme['say_hello'] = array(
      'variables' => array('name'=>null),
      'template' => 'say_hello',
  );

  return $theme;
}
            
          

/modules/custom/hello/templates/say_hello.html.twig

            
              <section>
  {% trans %}
    

Hello {{ name }}

{% endtrans %} </section>
/modules/custom/hello/src/Controller/HelloController.php

/**
 * Our first Drupal 8 controller.
 * Lets say HI
 */
namespace Drupal\hello\Controller;

use Drupal\Core\Controller\ControllerBase;

class HelloController extends ControllerBase {
  public function sayHi($name) {
    return array(
        '#theme'=>'say_hello',
        '#name' => $name,
      );
  }
}

          

What about Forms?

Very similar pattern!

/modules/custom/hello/src/Form/HelloRequestForm.php

							
/**
 * @file
 * Contains \Drupal\hello\Form\HelloRequestForm.
 */

namespace Drupal\hello\Form;

use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;

class HelloRequestForm extends FormBase {
  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'hello_request';
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {
    $form['phone_number'] = array(
      '#type' => 'tel',
      '#title' => $this->t('Your phone number')
    );
    $form['actions']['#type'] = 'actions';
    $form['actions']['submit'] = array(
      '#type' => 'submit',
      '#value' => $this->t('Give me a call'),
      '#button_type' => 'primary',
    );
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function validateForm(array &$form, FormStateInterface $form_state) {
    if (strlen($form_state->getValue('phone_number')) < 3) {
      $form_state->setErrorByName('phone_number', $this->t('The phone number is too short. Please enter a full phone number.'));
    }
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
    drupal_set_message($this->t('Your phone number is @number', array('@number' => $form_state->getValue('phone_number'))));
  }
}

							
						

/modules/custom/hello/hello.routing.yml

							
							hello.requestHi:
  path: '/hello/request'
  defaults:
    _form: '\Drupal\hello\Form\HelloRequestForm'
  requirements:
    _permission: 'request hello'
							
						

Configuration

Information about your site that is not content and is meant to be more permanent, such as the name of your site, the content types and views you have defined, etc.

Two types: Simple Configuration and Configuration entities

Sites own configuration

/modules/custom/hello/config/install/hello.settings.yml

              
              default_count: 10
phrase: "Why hello"
              
            
Simple Access:
              
                \Drupal::config('hello.settings')->get('default_count');
              
          

Now lets make it configurable

Lets define a ConfigForm

/modules/custom/hello/src/Form/HelloConfigForm.php

              
/**
 * @file
 * Contains \Drupal\hello\Form\HelloConfigForm.
 */

namespace Drupal\hello\Form;

use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;

class HelloConfigForm extends ConfigFormBase {
  /**
   * {@inheritdoc}.
   */
  public function getFormId() {
    return 'hello_config';
  }

  /**
   * Declare what settings this config form will be changing.
   */
  protected function getEditableConfigNames() {
    return ['hello.settings'];
  }

  /**
   * {@inheritdoc}.
   */
  public function buildForm(array $form, FormStateInterface $form_state) {

    $config = $this->config('hello.settings');

    $form['default_count'] = array(
      '#type' => 'number',
      '#title' => $this->t('How many times do we say hi?'),
      '#default_value' => $config->get('default_count'),
    );

    $form['phrase'] = array(
      '#type' => 'textfield',
      '#title' => $this->t('How do you want to say hi?'),
      '#default_value' => $config->get('phrase'),
    );

    return parent::buildForm($form, $form_state);
  }

  /**
   * {@inheritdoc}
   */
  public function submitForm(array &$form, FormStateInterface $form_state) {
      parent::submitForm($form, $form_state);

      //lets load and set our configuration.
      $config = $this->config('hello.settings');
      $config->set('default_count', $form_state->getValue('default_count'));
      $config->set('phrase', $form_state->getValue('phrase'));
      //save the config.
      $config->save();
  }
}
              
            

/modules/custom/hello/hello.routing.yml

              
hello.configHi:
  path: '/hello/config'
  defaults:
    _form: '\Drupal\hello\Form\HelloConfigForm'
  requirements:
    _permission: 'access content'
              
            

Lets make a Block!

Without hook_block_info and hook_block_view!

/modules/custom/hello/src/Plugin/Block/HelloBlock.php

              
namespace Drupal\hello\Plugin\Block;

use Drupal\Core\Block\BlockBase;

/**
 * Provides a 'Hello' block.
 *
 * @Block(
 *   id = "hello_block",
 *   admin_label = @Translation("Hello block"),
 * )
 */
class HelloBlock extends BlockBase {
  public function build() {
    return array(
      '#markup' => $this->t("This is a an awesome block!"),
    );
  }
}

              
            

Lets add some configuration to our block.

            
              /**
 * @file
 * Contains \Drupal\hello\Plugin\Block\HelloBlock
 */

namespace Drupal\hello\Plugin\Block;

use Drupal\Core\Block\BlockBase;
use Drupal\Core\Form\FormStateInterface;

/**
 * Provides a 'Hello' block.
 *
 * @Block(
 *   id = "hello_block",
 *   admin_label = @Translation("Hello block"),
 * )
 */
class HelloBlock extends BlockBase {
  public function defaultConfiguration() {
    return ['count' => 1];
  }

  /**
   * {@inheritdoc}
   */
  public function blockForm($form, FormStateInterface $form_state) {
    $form['hello_count'] = array(
      '#type' => 'number',
      '#title' => t('Hello Count'),
      '#size' => 10,
      '#description' => t('The number of times the block say hi.'),
      '#default_value' => $this->configuration['count'],
    );
    return $form;
  }

  /**
   * {@inheritdoc}
   */
  public function blockSubmit($form, FormStateInterface $form_state) {
    $this->configuration['hello_count']
      = $form_state->getValue('hello_count');
  }

  /**
   * {@inheritdoc}
   */
  public function build() {
    $count = $this->configuration['hello_count'];
    return array(
      '#markup' => $this->t("Hello {$count} times!"),
    );
  }
}
            
          
Plugins Everywhere
  • Blocks
  • Views
  • Text Formats
  • Fields
  • Field Widgets
  • Image effects
  • Mail Backends
  • Migrations
  • And more...

Services

Small (as possible) stateless objects that perform some testable task.

This opens up an interesting and powerful world for us...

The Service Container

A service container (or dependency injection container) is a PHP object that manages the instantiation of services.

/modules/custom/hello/src/HelloService.php

namespace Drupal\hello;

class HelloService {
  //dead simple example.
  public function doSomeWork() {
    //some heavy lifting.
    drupal_set_message("Hello Services");
  }
}
          
          

/modules/custom/hello/hello.services.yml

              
              services:
  hello.hello_service:
    class: Drupal\hello\HelloService
              
            

Now lets use it!

/modules/custom/hello/src/Controller/HelloController.php

/**
 * Our first Drupal 8 controller with a custom service!
 * Lets say HI
 */
namespace Drupal\hello\Controller;

use Drupal\Core\Controller\ControllerBase;
use Drupal\hello\HelloService;
use Symfony\Component\DependencyInjection\ContainerInterface;

class HelloController extends ControllerBase {
  protected $helloServ;

  //our contructor is injected with our service.
  public function __construct(HelloService $s) {
    $this->helloServ = $s;
  }

  //This is the interesting method. ControllerBase will call this.
  public static function create(ContainerInterface $container) {
    //and create an instance of this class, injecting our service!
    return new static($container->get('hello.hello_service'));
  }

  public function sayHi($name) {
    //lets use that bad boy!
    $this->helloServ->doSomeWork();

    return array(
        '#theme'=>'say_hello',
        '#name' => $name,
      );
  }
}

          
          

Services can serve many purposes

Lets take a look at another example

Drupal 8 Events

Leveraging the Symfony Framework Events.

List of core events: http://bit.ly/core_events

/modules/custom/hello/src/EventSubscriber/HelloEventSubscriber.php

namespace Drupal\hello\EventSubscriber;

use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Drupal\Core\Session\AccountInterface;

class HelloEventSubscriber implements EventSubscriberInterface {
  protected $account;

  public function __contruct(AccountInterface $account) {
    $this->account = $account;
  }

  public function checkForSomething(GetResponseEvent $event) {
      drupal_set_message("Big brother is here!");
  }

  /**
   * {@inheritdoc}
   */
  static function getSubscribedEvents() {
    $events[KernelEvents::REQUEST][] = array('checkForSomething');
    return $events;
  }
}
          
          

/modules/custom/hello/hello.services.yml

              
  hello_event_subscriber:
    class: Drupal\hello\EventSubscriber\HelloEventSubscriber
    arguments: ['@current_user']
    tags: #Our Service Tags
      - {name: event_subscriber}
              
            

So much more to explore and learn...

Thank you!

Questions?

Email: nick@segosolutions.com

Twitter: @direct

Slides Available:
http://nickgs.github.io/drupal8-testdrive-module