The following is a selection of code samples from a Drupal 8 custom module I created for managing CDN image ID values.
A custom field widget is used to save and manage CDN image information, and a custom field formatter works in conjunction with a simple twig template to render the images to html.
Completion Date
Code Snippet
# cdn_manager.info.yml name: CDN Manager description: Handles CDN image data package: Custom type: module core: 8.x core_version_requirement: ^8 || ^9
<?php /** * @file * cdn_manager.module * * Module: CDN Management Module. */ use Drupal\cdn_manager\CdnImg; /** * Implements hook_theme(). */ function cdn_manager_theme() { return [ 'cdn_image' => [ 'base hook' => 'image', 'variables' => ['img' => []], ], ]; }
<?php namespace Drupal\cdn_manager\Plugin\Field\FieldType; use Drupal\Core\Field\FieldItemBase; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\TypedData\DataDefinition; /** * Plugin implementation of the 'cdn_img' field type. * * @FieldType( * id = "cdn_img", * label = @Translation("CDN Manager field"), * description = @Translation("An entity field containing CDN image content"), * category = @Translation("Custom"), * default_formatter = "cdn_img_formatter", * default_widget = "cdn_img_widget", * ) */ class CdnImg extends FieldItemBase { /** * {@inheritdoc} */ public static function schema(FieldStorageDefinitionInterface $field_definition) { return [ // Columns contains the values that the field will store. 'columns' => [ // List the values that the field will save. This // field will only save a single value, 'value'. 'cdn_id' => [ 'type' => 'varchar', 'size' => 'normal', 'length' => 255, 'not null' => FALSE, ], 'filename' => [ 'type' => 'varchar', 'size' => 'normal', 'length' => 255, 'not null' => FALSE, ], 'alt_tag' => [ 'type' => 'varchar', 'size' => 'normal', 'length' => 255, 'not null' => FALSE, ], ], ]; } /** * {@inheritdoc} */ public function isEmpty() { $cdn_id = $this->get('cdn_id')->getValue(); $filename = $this->get('filename')->getValue(); $alt_tag = $this->get('alt_tag')->getValue(); return (empty($cdn_id) && empty($filename) && empty($alt_tag)); } /** * {@inheritdoc} */ public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) { // Add our properties. $properties['cdn_id'] = DataDefinition::create('string') ->setLabel(t('CDN ID')); $properties['filename'] = DataDefinition::create('string') ->setLabel(t('Filename')); $properties['alt_tag'] = DataDefinition::create('string') ->setLabel(t('Alt Tag')); return $properties; } }
<?php namespace Drupal\cdn_manager\Plugin\Field\FieldWidget; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\WidgetBase; use Drupal\Core\Form\FormStateInterface; /** * Plugin implementation of the 'cdn_img' widget. * * @FieldWidget( * id = "cdn_img_widget", * label = @Translation("CDN Manager widget"), * field_types = { * "cdn_img" * }, * ) */ class CdnImgWidget extends WidgetBase { /** * {@inheritdoc} */ public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state ) { $element += [ 'cdn_id' => [ '#type' => 'textfield', '#title' => $this->t('CDN ID'), '#default_value' => empty($items[$delta]->cdn_id) ? '' : $items[$delta]->cdn_id, '#size' => 60, '#maxlength' => 255, ], 'filename' => [ '#type' => 'textfield', '#title' => $this->t('Filename'), '#default_value' => empty($items[$delta]->filename) ? '' : $items[$delta]->filename, '#size' => 60, '#maxlength' => 255, ], 'alt_tag' => [ '#type' => 'textfield', '#title' => $this->t('Alt Tag'), '#default_value' => empty($items[$delta]->alt_tag) ? '' : $items[$delta]->alt_tag, '#size' => 60, '#maxlength' => 255, '#attributes' => ['class' => ['container-inline']], ], '#type' => 'fieldset', ]; return $element; } }
<?php namespace Drupal\cdn_manager\Plugin\Field\FieldFormatter; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Field\FormatterBase; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\cdn_manager\CdnImg; /** * Plugin implementation of the 'cdn_img' formatter. * * @FieldFormatter( * id = "cdn_img_formatter", * label = @Translation("CDN Manager Image"), * field_types = { * "cdn_img", * }, * ) */ class CdnImgFormatter extends FormatterBase implements ContainerFactoryPluginInterface { /** * {@inheritdoc} */ public function settingsSummary() { $summary = []; $summary[] = $this->t('Displays the CDN Manager item.'); return $summary; } /** * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items, $langcode = NULL) { $elems = []; foreach ($items as $delta => $item) { // Render each element as a cdn_img array. $cdn_img = new CdnImg(); $cdn_img->setImagePropertiesFromObject($item); $elems[$delta] = [ '#theme' => 'cdn_image', '#img' => $cdn_img->getImageUrlComponents(), ]; } return $elems; } }
<?php namespace Drupal\cdn_manager; use Drupal\Core\Url; use Drupal\Component\Utility\UrlHelper; use Drupal\Component\Utility\Xss; /** * Provides a cdn image object for rendering images. */ class CdnImg { /** * CDN Base Url. * * @var string */ protected static $baseUrl = '//embed.widencdn.net/img/accountname'; /** * CDN User ID. * * @var string */ protected static $cdnUserId = 'user_id'; /** * CDN ID. * * @var string */ protected $cdnId = ''; /** * Filename. * * @var string */ protected $filename = ''; /** * Alt tag. * * @var string */ protected $altTag = ''; /** * URL Query Parameters. * * @var array */ protected $urlQueryArray = []; /** * CDN Image item Constructor. * * @param string $cdn_id * CDN ID. * @param string $filename * File name. * @param string $alt_tag * Alt tag. * @param string $intended_use * Intended CDN use code for tracking usage analytics. */ public function __construct($cdn_id = '', $filename = '', $alt_tag = '', $intended_use = '') { $this->cdnId = $cdn_id; $this->filename = $filename; $this->altTag = $alt_tag; $this->setUrlQueryArray($intended_use); if (func_num_args()) { $this->sanitizeInputs(); } } /** * Gets any arbitrary static or dynamic property. * * @param string $property * The property to retrieve. * * @return mixed * The value for that property, or NULL if the property does not exist. */ public function get($property) { $value = $this->{$property} ?? (self::${$property} ?? NULL); return $value; } /** * Sets a value to an arbitrary property. * * @param string $property * The property to use for the value. * @param mixed $value * The value to set. * * @return $this */ public function set($property, $value) { if (property_exists($this, $property)) { $this->{$property} = $value; } return $this; } /** * Determines whether the cdn object contains any non-empty items. * * @return bool * TRUE if cdn id, filename, and alt tag values are empty, FALSE otherwise. */ public function isEmpty() { return ( empty($this->get('cdnId')) && empty($this->get('filename')) && empty($this->get('altTag')) ); } /** * Set CDN Image properties based on passed-in object. * * @param object $obj * Object being passed should have the following properties: * - cdn_id or cdnId. * - filename. * - alt_tag or altTag. * @param bool $not_avail * Boolean indicating whether to insert default 'not-available' values if * the cdn id or filename values are missing. */ public function setImagePropertiesFromObject($obj, $not_avail = TRUE) { $this->cdnId = $obj->cdn_id ?? ($obj->cdnId ?? ''); $this->filename = $obj->filename ?? ''; $this->altTag = $obj->alt_tag ?? ($obj->altTag ?? ''); $this->sanitizeInputs(); if ($not_avail) { if ($this->isEmpty()) { // Only set the alt tag value to not-available if the other fields are // also empty. $this->altTag = $this->t('No photo available.'); } if (empty($this->filename)) { $this->filename = 'photo-not-available.png'; } if (empty($this->cdnId)) { $this->cdnId = 'notavail'; } } } /** * Set CDN Image Properties for "No photo available". */ public function setImageNotAvailable() { $this->cdnId = 'notavail'; $this->filename = 'photo-not-available.png'; $this->altTag = $this->t('No photo available.'); } /** * Returns a render array for rendering the cdn image in Drupal 8. * * @param string $size * Should be a text string, specifying "WIDTHxHEIGHT" * e.g., 640x480, 120x120, 1024x768. * $size set to a single number, e.g., '300' will specify the width, but not * the height, and scale it appropriately. * $size set to begin with an 'x' and with a single number will specify the * height but not the width., e.g. ('x150'). * @param string $scale * This is for retina images, and can be any numeric value. * e.g., an image requested at "460x520px@2x" will be delivered with * final pixel dimensions of 920x1040 px. * * @return array * D8 render array. */ public function getRenderArray($size = '576x324', $scale = NULL) { /* return '<img src="' . $this->getImageUrl($size, $intended_use) . '"' . * (empty($this->altTag) ? '' : '" alt="' . $this->altTag) . '">'; */ return [ '#theme' => 'image', '#uri' => $this->getImageUrl($size, $scale), '#alt' => $this->altTag, ]; } /** * Returns a properly formatted URL for the CDN image, given the pixel size. * * Required parameters are $cdn_img_array and $size. The rest are optional. * * The 'intended use' parameter in the link is a code used to track how the * images is being used in the CDN system. * To find out more about intended use: * https://support.widencollective.com/support/solutions/articles/6000028390-how-do-i-collect-intended-use- * * More information: * https://support.widencollective.com/support/solutions/articles/6000028387-can-i-customize-share-links-and-embed-codes-for-images- * * @param string $size * Should be a text string, specifying "WIDTHxHEIGHT" * e.g., 640x480, 120x120, 1024x768. * $size set to a single number, e.g., '300' will specify the width, but not * the height, and scale it appropriately. * $size set to begin with an 'x' and with a single number will specify the * height but not the width., e.g. ('x150'). * @param string $scale * This is for retina images, and can be any numeric value. * e.g., an image requested at "460x520px@2x" will be delivered with * final pixel dimensions of 920x1040 px. * * @return string * Returns formatted image url. */ public function getImageUrl($size = '576x324', $scale = NULL) { $output = FALSE; $px_size = $size . 'px'; if (!empty($scale)) { $px_size .= '@' . $scale . 'x'; } if (!empty($this->cdnId) && !empty($this->filename)) { $path = self::$baseUrl . '/' . $this->cdnId . '/' . $px_size . '/' . $this->filename; // GIF images require slightly different handling. $path = $this::correctGifs($path); $options = [ 'query' => $this->urlQueryArray, ]; // Build external url object. $url = Url::fromUri($path, $options); $output = $url->toString(); } return $output; } /** * Set url query parameters according to various preferences. * * @param string $intended_use * Intended Widen CDN use code for tracking usage analytics. * @param int $quality * Integer from 0 (lowest quality) to 100 (highest quality), * default is 80. * @param bool $crop * Do you want to crop the image? Default is TRUE. * @param string $keep_area * Specifies which area of the image you'd like to keep. * "c" = center, "n" = north, "ne" = northeast, "e" = east, * "se" = southeast, "s" = south, "sw" = southwest, "w" = west, * "nw" = northwest. * @param string $background_color * Hex value of the background color you want to use if and only if cropping * is set to FALSE. */ public function setUrlQueryArray($intended_use = '', $quality = 80, $crop = TRUE, $keep_area = 'c', $background_color = 'ffffff') { $query = [ 'u' => self::$cdnUserId, 'k' => $keep_area, ]; if (!empty($intended_use)) { $query['use'] = $intended_use; } if (!$crop) { $query['c'] = '0'; $query['co'] = $background_color; } if ($quality <> 80) { $query['q'] = $quality; } $this->urlQueryArray = $query; } /** * Parses the url query array into a valid, rawurlencoded query string. * * @return string * A rawurlencoded string which can be used as or appended to the URL query * string. */ public function getUrlQueryString() { return(UrlHelper::buildQuery($this->urlQueryArray)); } /** * Helper function to sanitize all values of an object. */ protected function sanitizeInputs() { // We're trimming values to make sure any stray spaces at the beginning or // end of the array values don't get included in the embedding code. // Per Atrium #19617. // @TODO - this should also happen on node save so that we store clean data in the first place. foreach (get_object_vars($this) as $key => $val) { if (!empty($val) && is_string($val)) { $this->$key = $this::sanitize($val); } } } /** * {@inheritdoc} */ public static function sanitize($text, array $allowed_tags = []) { // Prevent cross-site-scripting (XSS) vulnerabilities. $text_clean = Xss::filter($text, $allowed_tags); // HTML attribute cannot contain quotes. $text_clean = str_replace('"', """, $text_clean); // Remove any stray spaces. $text_clean = trim($text_clean); return $text_clean; } /** * Correct CDN URLs for GIF images. * * This runs automatically in the getImageUrl() function. */ private static function correctGifs($cdn_url) { // Correct URLs for GIF images. // From this: https://embed.widencdn.net/img/accountname/idvalue/576x324px/example_image.gif?u=at8tiu&use=idsla&k=c // To this: https://embed.widencdn.net/original/accountname/idvalue/example_image.gif?u=hqh5il&use=otpm1 if (preg_match('/\.gif/i', $cdn_url)) { $cdn_url = preg_replace('/\/img\//i', '/original/', $cdn_url); $cdn_url = preg_replace('/\/(\d+)x(\d+)px/i', '', $cdn_url); } return $cdn_url; } /** * Returns an array of CDN image url components. * * Useful for constructing an image url in templates. * * @param string $size * Should be a text string, specifying "WIDTHxHEIGHT" * e.g., 640x480, 120x120, 1024x768. * $size set to a single number, e.g., '300' will specify the width, but not * the height, and scale it appropriately. * $size set to begin with an 'x' and with a single number will specify the * height but not the width., e.g. ('x150'). * * @return array * The array will look like: * [ * 'base_url' => '', * 'cdn_id' => '', * 'filename' => '', * 'url_query' => [], * 'alt_tag' => '', * ] */ public function getImageUrlComponents($size = '') { $arr = [ 'base_url' => self::$baseUrl, 'cdn_id' => $this->cdnId, 'filename' => $this->filename, 'url_query' => $this->getUrlQueryString(), 'alt_tag' => $this->altTag, ]; if (!empty($size)) { $arr['size'] = $size . 'px'; } return $arr; } }