At Your Service

Module Development using Services in Drupal 8

@emarchak / @myplanetHQ

Myplanet

@myplanetHQ

Erin Marchak

@emarchak

Overview

  1. Quickly scaffold code using Drupal Console
  2. Managing configuration in your module
  3. Create a custom service in Drupal 8 using Dependency Injection
  4. Import and manipulate content using Services available in core
  5. Create custom theme templates for Front End Developers

Here we go!

This presentation can be found at emarchak.github.io/atyourservice

This module can be found at github.com/emarchak/atyourservice

Create A Module using Console

$ drupal generate:module
          
Enter the new module name:
> Fastpaced videos 
          

Generate content types using console

$ drupal generate:entity:bundle

Enter the module name [admin_toolbar]:
> fastpaced_videos

Enter the machine name of your new content type [default]:
> video

$ drupal module:install fastpaced_videos

Configure nodes and export config

Log into the site and configure the content type as needed, before you export it.

$ drupal config:export:content:type
> video

Export content type in module as an optional configuration (yes/no) [yes]:
> no

$ drupal create:nodes

Add cron task to import videos

/**
 * Implements hook_cron().
 */
function fastpaced_videos_cron() {

  \Drupal::service('fastpaced_videos.import')->import();

}
          

Some parts of Drupal 8 are still procedural like 7,
hook_theme and hook_cron, for example.

Generate import service for videos

$ drupal generate:service
Enter the service name [fastpaced_videos.default]:
> fastpaced_videos.import

Enter the Class name [DefaultService]:
> ImportService

Do you want to load services from the container (yes/no) [no]:
> yes

Enter your service [ ]:
> http_client
> entity.query
> entity_type.manager
> config.factory
> serialization.json

Drupal 8 has lots of new services in core you can explore.

Add logging to import service

logger.channel.fastpaced_videos:
  parent: logger.channel_base
  arguments: ['fastpaced_videos']
    
use Psr\Log\LoggerInterface;

/**
 * Psr\Log\LoggerInterface definition.
 *
 * @var Psr\Log\LoggerInterface;
 */

protected $logger;
// Log how many videos we’ve imported.
$this->logger
  ->info('Imported @count fast paced videos', ['@count' => $imported]);
        

There's a new way to handle logging in Drupal 8.

Create service function stubs

// Get our search parameters.
  
  // Query YouTube based on our search results.
  
  // Check the result of our search request.
  try {
  
    // If we have something, list the results.
    $results = [];
  
    // Loop through the results.
    for ($i = 0; isset($results[$i]); $i++) {
  
      // Increment our imported count on successful import.
      $imported++;
  
    }
  
  } catch (RequestException $e) {
  
    watchdog_exception('fastpaced_videos', $e);
  }

Add configuration for our module

$ drupal generate:form:config
  
  Enter the Form Class name [DefaultForm]:
  > ImportSettingsForm    
  
  Do you want to generate a form structure? (yes/no) [yes]:
  > y
  
  Type:          textfield
  Input label:   Search Terms
  Description:   Feed to import from
  Default value: macaframa
  
  Update routing file (yes/no) [yes]:
  > yes   
  
  
  Generate a menu link (yes/no) [yes]:
  > yes
  
  A title for the menu link [ImportSettingsForm]:
  > Fast Paced Import Settings
  
  Menu parent [system.admin_config_system]:
  > system.admin_config_services
  

Export the configuration settings.

$ drupal config:export:single --directory=modules/custom/fastpaced_videos/config/install
  
Configuration type [Simple configuration]:
> system.simple

Configuration name [automated_cron.settings]:
> fastpaced_videos.importsettings

Add configuration object to service to get search terms

import() {
  // Get our search parameters.
  $config = $this->config_factory->getEditable('fastpaced_videos.importsettings');
  $search_terms = $config->get('search_terms');
  $this->logger
    ->info('Searching for @terms', ['@terms' => $search_terms]);

Construct URL for search

protected function getSearchURL($search_terms = '') {
$search_url = URL::fromUri(
  'https://www.googleapis.com/youtube/v3/search',
  [ 'query' => [
    'q'           => urlencode($search_terms),
    'part'        => 'snippet',
    'type'        => 'video',
    'safeSearch'  => 'strict',
    'maxResults'  => '50',
    'key'         => $_SERVER['GGL_API_KEY']
  ]])->toUriString();
  return $search_url;
}
import() {
  // Query YouTube based on our search results.
  $search_url = $this->getSearchURL($search_terms);
  $response = $this->http_client->request('GET', $search_url);

Take a look at the results

// Check the result of our search request.
try {

  if ($response->getReasonPhrase() != 'OK') {
    throw new Exception(t(
      'Received status @status',
      array('$status' => $response->getReasonPhrase())
    ));
  }
    // If we have something, list the results.
  $data = $this->serialization_json->decode($response->getBody());
  $results = $data['items']; 

Generate the video URL method

// Create the video URL.
$video_url = $this->getVideoURl($results[$i]);
/**
 * Helper method to return the video URL
 */

protected function getVideoURL($item = []) {
  $video_url = '';

  $video_url = Url::fromUri(
    'https://www.youtube.com/watch',
    [ 'query' => [
      'v' => $item['id']['videoId']
    ]])->toUriString();

  return $video_url;
}

Check to see if we have the video URL imported

// Check to see if we have the video URL imported
$is_new = $this->uniqueField('field_video', $video_url);
/**
 * Check if file is unique among nodes.
 */
protected function uniqueField($field = '', $value = '') {
  $result = $this->entity_query->get('node')
    ->condition($field, $value, '=')
    ->execute();

  $unique = empty($result);

  return $unique;
}

Finally import the node

if ($is_new) {
  $item = $results[$i]['snippet'];
  $was_saved = $this->createNode($item['title'], [
    'body'        => $item['description'],
    'field_video' => $video_url,
  ]);
  
  // Increment our imported count on successful import.
  if ($was_saved) {
    $imported++;
  }
/**
 * Create node and populate fields for video content types.
 */
protected function createNode($title = '', $fields = []) {
  $node_storage = $this->entity_type_manager->getStorage('node');

  // Create the node.
  $node = $node_storage->create([
    'type'  => 'video',
    'title' => $title,
    'uid'   => 1,
  ]);
  // Populate the fields
  foreach ($fields as $field => $value) {
    $node->set($field, $value);
  }

  // Save the node
  return $node->save();
}

Create custom front page

$ drupal generate:controller

Enter the Controller class name [DefaultController]:
> FrontPageController
Enter the Controller method title (leave empty and press enter when done) [ ]:
> Front
Enter the action method name [hello]:
> load
Enter the route path [fastpaced_videos/hello/{name}]:
> front
Enter your service [ ]:
> entity_type.manager

Create Custom Theme Function

Add our theme function to fastpaced_videos.module.

This looks for fastpaced-gallery.html.twig.

function fastpaced_videos_theme() {
  return [
    'fastpaced_gallery' => [
      'variables' => [
        'videos' => NULL
      ]
    ]
  ];
}

Some parts of Drupal 8 are still procedural like 7,
hook_theme and hook_cron, for example.

Add the variables to src/Controller/FrontPageController.php.

load() {
  // Get our libraries to make this easier.
  $storage = $this->entity_type_manager->getStorage('node');
  $query = $storage->getQuery();
  $view = $this->entity_type_manager->getViewBuilder('node');
  
  // Fetch the 10 most recent nodes
  $nids = $query
    ->condition('status', NODE_PUBLISHED)
    ->condition('type', 'video')
    ->sort('created', 'DESC')
    ->pager(10)
    ->execute();
  $nodes = $storage->loadMultiple($nids);
  foreach($nodes as &$node) {
    $node = $view->view($node, 'teaser');
  }
  
  // Return the nodes to the template.
  return [
    '#theme' => 'fastpaced_gallery',
    '#videos' => $nodes
  ];

Thank you!

@emarchak / @myplanetHQ