Entities 201 - Creating Custom Entities

Ron Northcutt

Sr. Solutions Architect

Who am i?

  • using Drupal since 4.7
  • freelance developer
  • small dev shop
  • sr. developer & tech lead
  • sr. solutions architect

Find me online:

Agenda

  • Tools & setup
  • Basic entity
    • Generate in under 5 minutes
    • Understanding entity code
    • Using basic entity in UI
  • Complex entity
    • Custom code
    • Using complex entity
  • Next steps

Tools & setup

  • Local dev environment
  • Drupal base site (Lightning)
  • Drupal console
  • IDE or text editor

Basic entity - "Thingy"

  • Default custom entity
  • Steps
    • Generate module
    • Generate entity
    • Review code
    • Enable and test

Drupal console commands

  • drupal list

  • drupal generate:module

  • drupal generate:entity:content

  • drush en thingy -y

Thats it!

Complex entity - "Asset"

  • Similar to css_injector* in D7
  • Great for:
    • Demos
    • Quick changes
    • Rapid prototyping
    • Designers without access to codebase
  • Add, edit, delete css/js assets
    • Revisionable
  • Save the asset code to a file
  • Attach those files to the pages
     

* Use asset_injector in D8

Build plan

  • Generate module
  • Generate entity
  • Custom code
    • Database schema
    • Interface
    • Entity methods
    • Folders at install
    • Create files
    • Attach libraries
  • Enable and test

 

Step 1

Generate module and custom entity.

Step 2.1

Modify Entity - add Base Field Definitions 


$fields['content'] = BaseFieldDefinition::create('string_long')
  ->setLabel(t('Asset file content'))
  ->setDescription(t('Enter the code that will be stored in this asset.'))
  ->setRequired(TRUE)
  ->setRevisionable(TRUE)
  ->setDefaultValue('')
  ->setDisplayOptions('form', [
    'type'     => 'string_textarea',
    'weight'   => -1,
    'settings' => ['rows' => 25,]
  ])
  ->setDisplayConfigurable('view', TRUE);

 

Step 2.2

Modify Entity - add Base Field Definitions 

$fields['type'] = BaseFieldDefinition::create('list_string')
  ->setLabel(t('Asset type'))
  ->setDescription(t('Select the type of Asset that this should be.'))
  ->setRequired(TRUE)
  ->setTranslatable(FALSE)
  ->setRevisionable(FALSE)
  ->setDisplayOptions('form', [
    'type'   => 'options_select',
    'weight' => -3,
  ])
  ->setSetting('allowed_values', [
    'css' => 'CSS',
    'js'  => 'JS',
  ])
  ->setDisplayConfigurable('form', FALSE);

 

Step 2.3

Modify Entity - add Base Field Definitions 

 
$fields['uri'] = BaseFieldDefinition::create('uri')
  ->setLabel(t('URI'))
  ->setDescription(t('The URI to access the  Asset file.'))
  ->setRequired(TRUE)
  ->setTranslatable(FALSE)
  ->setRevisionable(FALSE)
  ->setSetting('max_length', 255)
  ->setSetting('case_sensitive', TRUE)
  ->addConstraint('FileUriUnique');

 

Step 3

Modify Interface - add methods

/**
 * Returns the type of the Asset file
 *
 * @return string
 *   The file type, e.g. css.
 */
public function getType();

/**
 * Returns the name of the Asset file
 *
 * @return string
 *   Name of the file
 */
public function getFileName();

/**
 * Returns the URI of the Asset file
 *
 * @return string
 *   The URI of the file, e.g. public://asset/css/custom.css
 */
public function getFileUri();

 

Step 4

Modify Entity - add interface methods 

/**
 * {@inheritdoc}
 */
public function getType() {
  return $this->get('type')->value;
}

/**
 * {@inheritdoc}
 */
public function getFileName() {
  $name = $this->get('name')->value;
  $extension = '.' . $this->getType();
  $filename = strtolower($name);
  $filename = preg_replace('/[^a-z0-9_]+/', '_', $filename);
  return $filename . $extension;
}

/**
 * {@inheritdoc}
 */
public function getFileUri() {
  $filename = $this->getFileName();
  $type = $this->getType();
  return "public://asset/$type/$filename";
}

 

Step 5.1

 

Make sure folder structure will exist  (or not)

Create asset.install file

Step 5.2

Make sure folder structure will exist  (or not)

/**
 * Implements hook_install().
 *
 * See /core/modules/image/image.install for example
 */
function asset_install() {
  // Create the asset directories and ensure they're writable.
  $directory = file_default_scheme() . '://asset';
  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  $directory = file_default_scheme() . '://asset/css';
  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
  $directory = file_default_scheme() . '://asset/js';
  file_prepare_directory($directory, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS);
}

 

Step 5.3

Make sure folder structure will exist  (or not)


/**
 * Implements hook_uninstall().
 */
function asset_uninstall() {
  // Remove the asset directory and generated images.
  file_unmanaged_delete_recursive(file_default_scheme() . '://asset');
}

 

Step 6.1

Modify Entity - add asset entity methods 

/**
 * Saves the Asset code as a file.
 */
public function saveFile() {
  $new_uri = $this->getFileUri();
  $content = $this->get('content')->value;
  file_unmanaged_save_data($content, $new_uri, FILE_EXISTS_REPLACE);
}

 

Step 6.2

Modify Entity - add asset entity methods 


/**
 * Deletes the Asset file.
 *
 * @param string
 *   The URI for the file to delete, or use the auto generated one.
 */
public function deleteFile($uri = '') {
  if (!$uri) {
    $uri = $this->getFileUri();
  }
  file_unmanaged_delete($uri);
}

 

Step 6.3

Modify Entity - add asset entity methods 

/**
 * Clear the CSS and JS caches
 *
 * See drupal_flush_all_caches() in common.inc.
 */
public function clearFileCache() {
  // Flush Asset file caches.
  \Drupal::service('asset.css.collection_optimizer')->deleteAll();
  \Drupal::service('asset.js.collection_optimizer')->deleteAll();
  _drupal_flush_css_js();
}

 

Step 7.1

Modify Entity - extend base entity methods 

/**
 * {inheritdoc}
 */
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
  parent::postSave($storage, $update);

  $this->saveFile();
  $this->clearFileCache();
}

 

Step 7.2

Modify Entity - extend base entity methods 

/**
 * {inheritdoc}
 */
public static function postDelete(EntityStorageInterface $storage, array $entities) {
  parent::postDelete($storage, $entities);
  foreach ($entities as $entity) {
    $entity->deleteFile();
  }
}

 

Step 8.1

Modify the asset.module file to attach code to pages

use Drupal\asset\Entity\Asset;

 

Step 8.2

Create the library array (no yaml)

/**
 * Implements hook_library_info_build().
 */
function asset_library_info_build(){
  $libraries = [];

  $libraries['asset-files'] = [
    'dependencies' => [],
    'js'           => [],
    'css'          => [
      'theme' => [],
    ],
  ];

  $assets = Asset::loadMultiple();

  foreach ($assets as $asset) {
    $type = $asset->getType();
    $path = $asset->getFileUri();
    switch ($type) {
      case 'css':
        $libraries['asset-files']['css']['theme'] += [$path => []];
        break;
      case 'js':
        $libraries['asset-files']['js'] += [$path => []];
        break;
    }
  }

  return $libraries;
}

Step 8.2

Attach CSS/JS to page 

/**
 * Implements hook_page_attachments_alter().
 */
function asset_page_attachments_alter(array &$page) {
  $page['#attached']['library'][] = 'asset/asset-files';
}

 

Step 9.1

Modify Entity - garbage collection

/**
 * Remove old file if the name changed.
 */
public function dedupeFile() {
  // Check if the filename changed so we can delete the old one
  $old_uri = $this->get('uri')->value;
  $new_uri = $this->getFileUri();

  if ($old_uri !== $new_uri) {
    $this->deleteFile($old_uri);
  }
}

 

Step 9.2

Modify Entity - add to presave

// Run the dedupe in case the file name changed
$this->dedupeFile();
$this->set('uri', $this->getFileUri());

 

Moment of truth.

Why does this make sense as a custom entity?

  • Custom schema - all in one table
  • Additional methods available for the entity
  • Additional workflow steps
    • save to file, clear cache, etc.
  • Extending the default methods
    • presave, post save

Final notes

  • The best way to learn is to jump in and start creating. 

     

  • Get off the fence. All you need is an hour or two.

     

  • Start simple. Build complexity and add functionality as you go along.

Homework

  • Create and test a basic entity

  • Port a module or create your own

  • Review other webinars, tutorials, and videos

  • Use a custom entity in your next project

Questions?

Entities 201

By Ron Northcutt

Entities 201

Creating Custom Entities - DrupalCon 2017 Baltimore

  • 1,763