Routing YAML
my_module.my_route:
path: '/custom/path'
defaults:
_controller: '\Drupal\my_module\Controller\MyController::myMethod'
_title: 'My Page Title'
requirements:
_permission: 'access content'
Basic Controller
namespace Drupal\my_module\Controller;
use Drupal\Core\Controller\ControllerBase;
class MyController extends ControllerBase {
#[Route('/custom/path', name: 'my_route')]
public function myMethod() {
return [
'#markup' => $this->t('Hello World!'),
];
}
}
Via attributes
namespace Drupal\my_module\Controller;
use Drupal\Core\Controller\ControllerBase;
class MyController extends ControllerBase {
public function myMethod() {
return [
'#markup' => $this->t('Hello World!'),
];
}
}
Define a service
services:
my_module.my_service:
class: Drupal\my_module\Service\MyService
arguments: ['@entity_type.manager', '@logger.channel.my_module']
Basic Service Class
namespace Drupal\my_module\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;
class MyService {
protected $entityTypeManager;
protected $logger;
public function __construct(EntityTypeManagerInterface $entityTypeManager, LoggerInterface $logger) {
$this->entityTypeManager = $entityTypeManager;
$this->logger = $logger;
}
public function doSomething() {
$this->logger->info('Doing something');
}
}
via attributes
namespace Drupal\my_module\Service;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AsService;
#[AsService(id: 'my_module.my_service')]
class MyService {
protected $entityTypeManager;
protected $logger;
public function __construct(
EntityTypeManagerInterface $entityTypeManager,
#[Autowire(service:'logger.channel.my_module')]
LoggerInterface $logger
) {
$this->entityTypeManager = $entityTypeManager;
$this->logger = $logger;
}
public function doSomething() {
$this->logger->info('Doing something');
}
}
Controller Injection
namespace Drupal\my_module\Controller;
use Drupal\Core\Controller\ControllerBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\my_module\Service\MyService;
class CustomPageController extends ControllerBase {
protected $myService;
public function __construct(CustomService $myService) {
$this->myService = $myService;
}
public static function create(ContainerInterface $container) {
return new static(
$container->get('my_module.my_service')
);
}
public function content() {
$message = $this->myService->doSomething();
// ...
}
}
Basic Plugin (e.g. Block)
namespace Drupal\my_module\Plugin\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
/**
* @Block(
* id = "latest_articles_block",
* admin_label = @Translation("Latest Articles Block")
* )
*/
class LatestArticlesBlock extends BlockBase implements ContainerFactoryPluginInterface {
protected $entityTypeManager;
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->entityTypeManager = $entity_type_manager;
}
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('entity_type.manager')
);
}
public function build() {
$storage = $this->entityTypeManager->getStorage('node');
$nodes = $storage->loadByProperties([
'type' => 'article',
'status' => 1,
]);
$latest_nodes = array_slice($nodes, 0, 5);
$items = [];
foreach ($latest_nodes as $node) {
$items[] = $node->toLink();
}
return [
'#theme' => 'item_list',
'#items' => $items,
];
}
}
namespace Drupal\my_module\EventSubscriber;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
class MyEventSubscriber implements EventSubscriberInterface {
public static function getSubscribedEvents() {
return [
KernelEvents::RESPONSE => 'onResponse',
];
}
public function onResponse(ResponseEvent $event) {
$response = $event->getResponse();
$response->headers->set('X-Custom-Header', 'Value');
}
}
Event Subscriber registration
services:
my_module.event_subscriber:
class: Drupal\my_module\EventSubscriber\MyEventSubscriber
tags:
- { name: event_subscriber }
via attributes
use Symfony\Component\EventDispatcher\Attribute\AsEventListener;
#[AsEventListener(event: 'kernel.request', priority: 10)]
public function handleRequestEvent(RequestEvent $event): void
{
// Logic before controller is called
}
permissions.yml
my_module.custom_permission:
title: 'My Custom Permission'
description: 'Allows users to do something special.'
Usage in Routing
requirements:
_permission: 'my_module.custom_permission'
via hook
function my_module_form_alter(&$form, \Drupal\Core\Form\FormStateInterface $form_state, $form_id) {
if ($form_id === 'user_register_form') {
$form['my_custom_field'] = [
'#type' => 'textfield',
'#title' => t('My Custom Field'),
];
}
}
via attributes
namespace Drupal\mymodule\Hook;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Hook\Attribute\Hook;
class FormHooks {
#[Hook('form_alter')]
public function alterForm(array &$form, FormStateInterface $form_state, $form_id): void {
if ($form_id === 'user_register_form') {
$form['my_custom_field'] = [
'#type' => 'textfield',
'#title' => t('My Custom Field'),
];
}
}
}
$nodes = \Drupal::entityTypeManager()
->getStorage('node')
->getQuery()
->condition('type', 'article')
->condition('status', 1)
->range(0, 5)
->execute();
$loaded_nodes = \Drupal\node\Entity\Node::loadMultiple($nodes);
my_module.links.menu.yml
my_module.custom_link:
title: 'My Link'
route_name: my_module.my_route
parent: main-menu
weight: 5
namespace Drupal\my_module\Plugin\Field\FieldType;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
use Drupal\Core\TypedData\DataDefinition;
/**
* Defines a 'example_field' field type.
*
* @FieldType(
* id = "example_field",
* label = @Translation("Example Field"),
* description = @Translation("An example field implementation."),
* default_widget = "string_textfield",
* default_formatter = "string"
* )
*/
class ExampleFieldItem extends FieldItemBase {
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Value'));
return $properties;
}
public function isEmpty() {
$value = $this->get('value')->getValue();
return $value === NULL || $value === '';
}
}
namespace Drupal\my_module\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
/**
* @FieldFormatter(
* id = "my_custom_formatter",
* label = @Translation("My Custom Formatter"),
* field_types = {"string"}
* )
*/
class MyCustomFormatter extends FormatterBase {
public function viewElements(FieldItemListInterface $items, $langcode) {
$elements = [];
foreach ($items as $delta => $item) {
$elements[$delta] = ['#markup' => '<strong>' . $item->value . '</strong>'];
}
return $elements;
}
}
namespace Drupal\my_module;
use Drupal\node\NodeAccessControlHandler;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Access\AccessResult;
class CustomNodeAccessHandler extends NodeAccessControlHandler {
protected function checkAccess($entity, $operation, AccountInterface $account) {
if ($operation === 'delete' && $entity->bundle() === 'article') {
if (!$account->hasPermission('delete articles')) {
return AccessResult::forbidden();
}
}
return parent::checkAccess($entity, $operation, $account);
}
}
Override access control via services
services:
entity_access.node:
class: Drupal\my_module\CustomNodeAccessHandler
arguments: ['@entity_type.manager', '@module_handler']
Define the Plugin Manager:
namespace Drupal\my_module\Plugin;
use Drupal\Component\Plugin\PluginManagerInterface;
use Drupal\Core\Plugin\DefaultPluginManager;
class CustomPluginManager extends DefaultPluginManager {
public function __construct(\Traversable $namespaces, \Drupal\Core\Cache\CacheBackendInterface $cache_backend, \Drupal\Core\Extension\ModuleHandlerInterface $module_handler) {
parent::__construct(
'Plugin/CustomPlugin',
$namespaces,
$module_handler,
'Drupal\my_module\Plugin\CustomPluginInterface',
'Drupal\my_module\Annotation\CustomPlugin'
);
$this->alterInfo('custom_plugin_info');
$this->setCacheBackend($cache_backend, 'custom_plugins');
}
}
Define the Plugin Interface:
namespace Drupal\my_module\Plugin;
interface CustomPluginInterface {
public function process($value);
}
Define the Plugin Annotation:
namespace Drupal\my_module\Annotation;
use Drupal\Component\Annotation\Plugin;
/**
* Defines a Custom plugin annotation.
*
* @Annotation
*/
class CustomPlugin extends Plugin {
public $id;
public $label;
}
Example Implementation
namespace Drupal\my_module\Plugin\CustomPlugin;
use Drupal\my_module\Plugin\CustomPluginInterface;
/**
* @CustomPluginExample(
* id = "custom_plugin_example",
* label = @Translation("Custom Plugin Example")
* )
*/
class CustomPluginExample implements CustomPluginInterface {
public function process($value) {
// Do something...
}
}
$mailManager = \Drupal::service('plugin.manager.mail');
$params['subject'] = 'Test email';
$params['message'] = 'Hello from Drupal!';
$mailManager->mail('my_module', 'custom_mail', 'example@example.com', \Drupal::currentUser()->getPreferredLangcode(), $params);
Cacheable Metadata
use Drupal\Core\Cache\CacheableMetadata;
$build = [
'#markup' => 'Hello World',
];
$cache_metadata = new CacheableMetadata();
$cache_metadata->addCacheTags(['node_list']);
$cache_metadata->applyTo($build);
Cache Context
namespace Drupal\my_module\Cache;
use Drupal\Core\Cache\CacheContextInterface;
use Drupal\Core\Session\AccountProxyInterface;
class UserRoleCacheContext implements CacheContextInterface {
protected $currentUser;
public function __construct(AccountProxyInterface $current_user) {
$this->currentUser = $current_user;
}
public static function getLabel() {
return t('User role');
}
public function getContext() {
$roles = $this->currentUser->getRoles();
sort($roles);
return implode(',', $roles);
}
public function getCacheableMetadata() {
return [];
}
}
Cache Context registration
services:
my_module.user_role_cache_context:
class: Drupal\my_module\Cache\UserRoleCacheContext
arguments: ['@current_user']
tags:
- { name: cache.context, id: 'user_role' }
namespace Drupal\my_module\Plugin\rest\resource;
use Drupal\rest\Plugin\ResourceBase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* Provides a custom REST resource.
*
* @RestResource(
* id = "custom_rest_resource",
* label = @Translation("Custom REST Resource"),
* uri_paths = {
* "canonical" = "/api/custom-resource"
* }
* )
*/
class CustomRestResource extends ResourceBase {
public function get() {
$data = ['message' => 'Hello World'];
return new ResourceResponse($data);
}
}
use Drupal\node\Entity\Node;
$node = Node::create([
'type' => 'article',
'title' => 'Programmatically created node',
'body' => [
'value' => 'This node was created using code.',
'format' => 'basic_html',
],
'status' => 1,
]);
$node->save();
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\field\Entity\FieldConfig;
// Add field storage (field definition)
FieldStorageConfig::create([
'field_name' => 'custom_field',
'entity_type' => 'node',
'type' => 'string',
])->save();
// Attach field to content type
FieldConfig::create([
'field_name' => 'custom_field',
'entity_type' => 'node',
'bundle' => 'article',
'label' => 'Custom Field',
])->save();