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,971