9.10.3. Working with fields in Drupal. Create your own field type, widget, formatter for inserting videos from Youtube.
In previous articles we have seen how the Link field type works: Storage, Widget, Formatter. In this article we will make our own bone field type for outputting video from youtube on a page with two different formats and settings.
This article focuses on the Fields API, and if you need to add the Yotube video field to your site, it is better to use the ready-made module:
https://www.drupal.org/project/video_embed_field
I've added all the code on github to drupalbook_youtube module, you can download the module and add it to your site:
https://github.com/levmyshkin/drupalbook8
Let's look at the listing of this module and I will try to describe how this type of field works:
modules/custom/drupalbook_youtube/drupalbook_youtube.info.yml
name: DrupalBook Youtube
type: module
description: Youtube embed field
core: 8.x
package: Custom
Determine the metadata for the module.
modules/custom/drupalbook_youtube/src/Plugin/Field/FieldType/DrupalbookYoutubeItem.php
<?php
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Plugin implementation of the 'drupalbook_youtube' field type.
*
* @FieldType(
* id = "drupalbook_youtube",
* label = @Translation("Embed Youtube video"),
* module = "drupalbook_youtube",
* description = @Translation("Output video from Youtube."),
* default_widget = "drupalbook_youtube",
* default_formatter = "drupalbook_youtube_thumbnail"
* )
*/
class DrupalbookYoutubeItem extends FieldItemBase {
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return array(
'columns' => array(
'value' => array(
'type' => 'text',
'size' => 'tiny',
'not null' => FALSE,
),
),
);
}
/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Youtube video URL'));
return $properties;
}
}
Create a field type so that Drupal knows what we will store in the table for this field.
<?php
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
Define namespaces for our type of field.
/**
* Plugin implementation of the 'drupalbook_youtube' field type.
*
* @FieldType(
* id = "drupalbook_youtube",
* label = @Translation("Embed Youtube video"),
* module = "drupalbook_youtube",
* description = @Translation("Output video from Youtube."),
* default_widget = "drupalbook_youtube",
* default_formatter = "drupalbook_youtube_thumbnail"
* )
*/
Writing an abstract for our class, from this abstract Drupal will take the name of our type of field and its machine name.
class DrupalbookYoutubeItem extends FieldItemBase {
The name of the class is best written with the Item at the end.
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition) {
return array(
'columns' => array(
'value' => array(
'type' => 'text',
'size' => 'tiny',
'not null' => FALSE,
),
),
);
}
We define that we will store the value field of the text type.
/**
* {@inheritdoc}
*/
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
In case we have a call to the field from a third-party code, we output fallback with an empty result for the case of an empty field.
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Youtube video URL'));
return $properties;
}
Describe our columns for MySQL table and entity object. As a result, we will store the whole link:
Now that we have added a field type, let's create a Widget to enter data:
modules/custom/drupalbook_youtube/src/Plugin/Field/FieldWidget/DrupalbookYoutubeWidget.php
<?php
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'drupalbook_youtube' widget.
*
* @FieldWidget(
* id = "drupalbook_youtube",
* module = "drupalbook_youtube",
* label = @Translation("Youtube video URL"),
* field_types = {
* "drupalbook_youtube"
* }
* )
*/
class DrupalbookYoutubeWidget extends WidgetBase {
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$value = isset($items[$delta]->value) ? $items[$delta]->value : '';
$element += array(
'#type' => 'textfield',
'#default_value' => $value,
'#size' => 32,
'#maxlength' => 256,
'#element_validate' => array(
array($this, 'validate'),
),
);
return array('value' => $element);
}
/**
* Validate the color text field.
*/
public function validate($element, FormStateInterface $form_state) {
$value = $element['#value'];
if (strlen($value) == 0) {
$form_state->setValueForElement($element, '');
return;
}
if(!preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $value, $matches)) {
$form_state->setError($element, t("Youtube video URL is not correct."));
}
}
}
The widget will allow us to enter data on the form of entity editing.
/**
* Plugin implementation of the 'drupalbook_youtube' widget.
*
* @FieldWidget(
* id = "drupalbook_youtube",
* module = "drupalbook_youtube",
* label = @Translation("Youtube video URL"),
* field_types = {
* "drupalbook_youtube"
* }
* )
*/
In the annotation to the class we have to specify the field_type which was created above, i.e. drupalbook_youtube.
class DrupalbookYoutubeWidget extends WidgetBase {
At the end of the class name we add Widget to show that this class is needed for Field Widget.
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
$value = isset($items[$delta]->value) ? $items[$delta]->value : '';
$element += array(
'#type' => 'textfield',
'#default_value' => $value,
'#size' => 32,
'#maxlength' => 256,
'#element_validate' => array(
array($this, 'validate'),
),
);
return array('value' => $element);
}
Create a simple text field through the Form API, where we will enter a link to Youtube video.
/**
* Validate the color text field.
*/
public function validate($element, FormStateInterface $form_state) {
$value = $element['#value'];
if (strlen($value) == 0) {
$form_state->setValueForElement($element, '');
return;
}
if(!preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $value, $matches)) {
$form_state->setError($element, t("Youtube video URL is not correct."));
}
}
The Validation callback we specified above in #element_validate. This is to ensure that the user has entered the correct link to the youtube video. The regular expression I took from the stackoverflow, you can replace it if it does not work for you.
Now we can enter the data for our field, the only thing left to do is to add the Field Formatter, to output the data.
modules/custom/drupalbook_youtube/src/Plugin/FieldFieldFormatter/DrupalbookYoutubeThumbnailFormatter.php
We will have two formatters, starting with a simple one, which will display a picture with a link to the Youtube video page.
<?php
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
/**
* Plugin implementation of the 'drupalbook_youtube_thumbnail' formatter.
*
* @FieldFormatter(
* id = "drupalbook_youtube_thumbnail",
* module = "drupalbook_youtube",
* label = @Translation("Displays video thumbnail"),
* field_types = {
* "drupalbook_youtube"
* }
* )
*/
class DrupalbookYoutubeThumbnailFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = array();
foreach ($items as $delta => $item) {
preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $item->value, $matches);
if (!empty($matches)) {
$content = '<a href="' . $item->value . '" target="_blank"><img src="http://img.youtube.com/vi/' . $matches[0] . '/0.jpg"></a>';
$elements[$delta] = array(
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $content,
);
}
}
return $elements;
}
}
Specify the field type in the annotation to the class:
/**
* Plugin implementation of the 'drupalbook_youtube_thumbnail' formatter.
*
* @FieldFormatter(
* id = "drupalbook_youtube_thumbnail",
* module = "drupalbook_youtube",
* label = @Translation("Displays video thumbnail"),
* field_types = {
* "drupalbook_youtube"
* }
* )
*/
We will use #type html_tag and output each field item in the . We pass the value of the #value key to the array already ready-made HTML, which will be displayed on the page:
$content = '<a href="' . $item->value . '" target="_blank"><img src="http://img.youtube.com/vi/' . $matches[0] . '/0.jpg"></a>';
$elements[$delta] = array(
'#type' => 'html_tag',
'#tag' => 'p',
'#value' => $content,
);
View images are already generated by Youtube and we can access them directly, knowing the ID of the video.
We use $delta to support multiple field values so that we can output more than one video per page through one field.
Now let's look at a more complex format with a template and settings:
modules/custom/drupalbook_youtube/src/Plugin/FieldFieldFormatter/DrupalbookYoutubeVideoFormatter.php
<?php
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'drupalbook_youtube_video' formatter.
*
* @FieldFormatter(
* id = "drupalbook_youtube_video",
* module = "drupalbook_youtube",
* label = @Translation("Displays Youtube video"),
* field_types = {
* "drupalbook_youtube"
* }
* )
*/
class DrupalbookYoutubeVideoFormatter extends FormatterBase {
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'width' => '600',
'height' => '450',
) + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements['width'] = array(
'#type' => 'textfield',
'#title' => t('Youtube video width'),
'#default_value' => $this->getSetting('width'),
);
$elements['height'] = array(
'#type' => 'textfield',
'#title' => t('Youtube video height'),
'#default_value' => $this->getSetting('height'),
);
return $elements;
}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = array();
$width = $this->getSetting('width');
$height = $this->getSetting('height');
foreach ($items as $delta => $item) {
preg_match("#(?<=v=)[a-zA-Z0-9-]+(?=&)|(?<=v\/)[^&\n]+(?=\?)|(?<=v=)[^&\n]+|(?<=youtu.be/)[^&\n]+#", $item->value, $matches);
if (!empty($matches)) {
$elements[$delta] = array(
'#theme' => 'drupalbook_youtube_video_formatter',
'#width' => $width,
'#height' => $height,
'#video_id' => $matches[0],
);
}
}
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary() {
$summary = [];
$settings = $this->getSettings();
if (!empty($settings['width']) && !empty($settings['height'])) {
$summary[] = t('Video size: @width x @height', ['@width' => $settings['width'], '@height' => $settings['height']]);
}
else {
$summary[] = t('Define video size');
}
return $summary;
}
}
At the beginning of the file there are standard namespaces connections and annotations:
<?php
namespace Drupal\drupalbook_youtube\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Form\FormStateInterface;
/**
* Plugin implementation of the 'drupalbook_youtube_video' formatter.
*
* @FieldFormatter(
* id = "drupalbook_youtube_video",
* module = "drupalbook_youtube",
* label = @Translation("Displays Youtube video"),
* field_types = {
* "drupalbook_youtube"
* }
* )
*/
The class name ends with a Formatter to show that it is a Field Formatter:
class DrupalbookYoutubeVideoFormatter extends FormatterBase {
Define default settings for image size:
/**
* {@inheritdoc}
*/
public static function defaultSettings() {
return array(
'width' => '600',
'height' => '450',
) + parent::defaultSettings();
}
Next, we define the form of settings for the field formatter:
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state) {
$elements['width'] = array(
'#type' => 'textfield',
'#title' => t('Youtube video width'),
'#default_value' => $this->getSetting('width'),
);
$elements['height'] = array(
'#type' => 'textfield',
'#title' => t('Youtube video height'),
'#default_value' => $this->getSetting('height'),
);
return $elements;
}
This will look like this on the Manage display page:
In the settingsSummary() method we return what will be shown in the field description on the Manage display page:
public function settingsSummary() {
$summary = [];
$settings = $this->getSettings();
if (!empty($settings['width']) && !empty($settings['height'])) {
$summary[] = t('Video size: @width x @height', ['@width' => $settings['width'], '@height' => $settings['height']]);
}
else {
$summary[] = t('Define video size');
}
return $summary;
}
And now let's consider the most important method of viewElements() formatter.
$width = $this->getSetting('width');
$height = $this→getSetting('height');
Load the settings, which are defined through the settings form or class properties as fallback.
foreach ($items as $delta => $item) {
We support multiple fields, so we're going through all the elements of the field.
$elements[$delta] = array(
'#theme' => 'drupalbook_youtube_video_formatter',
'#width' => $width,
'#height' => $height,
'#video_id' => $matches[0],
);
Unlike the previous format, we specify the #theme key in this format and specify in it which template to use to process the field elements. For our formatter we will create a new template drupalbook_youtube_video_formatter. We will define this template in the file drupalbook_youtube.module:
modules/custom/drupalbook_youtube/drupalbook_youtube.module
/**
* Implements hook_theme().
*/
function drupalbook_youtube_theme() {
return array(
'drupalbook_youtube_video_formatter' => array(
'variables' => array('width' => 600, 'height' => 450, 'video_id' => NULL),
),
);
}
We will also take default width and height values here, and video_id, we will get from '#video_id' => $matches[0] of the $elements array.
And now we need to add the template file itself:
modules/custom/drupalbook_youtube/templates/drupalbook-youtube-video-formatter.html.twig
{#
/**
* @file
* Default theme implementation of a simple Youtube video.
*
* Available variables:
* - width: Youtube video width.
* - height: Youtube video height.
* - video_id: Youtube video ID.
*
* @see template_preprocess()
* @see template_drupalbook_youtube_video_formatter()
*
* @ingroup themeable
*/
#}
{% spaceless %}
<iframe width="{{ width }}" height="{{ height }}" src="https://www.youtube.com/embed/{{ video_id }}" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen>iframe>
{% endspaceless %}
And now you can create the Youtube video field:
That's where we'll finish the custom fields creation and move on to Entity API, where we'll create custom types of entities.