php
Factory.php
class SitesAliasPrefixListFactory implements AliasPrefixListInterface {
/**
* Cache of AliasPrefixList instances keyed by site ID.
*
* @var array
*/
protected array $instances = [];
/**
* Constructs a SitesAliasPrefixListFactory.
*
* @param \Drupal\sites\SiteProxyInterface $currentSite
* The current site service.
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\State\StateInterface $state
* The state service.
* @param \Drupal\path_alias\AliasRepositoryInterface $aliasRepository
* The alias repository.
*/
public function __construct(
protected readonly SiteProxyInterface $currentSite,
protected readonly CacheBackendInterface $cache,
protected readonly LockBackendInterface $lock,
protected readonly StateInterface $state,
protected readonly AliasRepositoryInterface $aliasRepository,
) {}
/**
* Gets the AliasPrefixList instance for the current site.
*
* @return \Drupal\path_alias\AliasPrefixList
* The site-specific prefix list instance.
*/
public function getInstance(): AliasPrefixList {
$site_id = $this->currentSite->id();
if (!isset($this->instances[$site_id])) {
$this->instances[$site_id] = new AliasPrefixList(
'path_alias_prefix_list.' . $site_id,
$this->cache,
$this->lock,
$this->state,
$this->aliasRepository,
);
}
return $this->instances[$site_id];
}
24.07.2026 | Dominik Wille
What the hell are factories in drupal?
Set a new value to a variable if it's currently NULL.
some.php
$a ??= $b
15.05.2026 | Dominik Wille
Null Coalesce
8_5_new_features.php
<?php
/*** The Pipe Operator ***/
// Old way
strtolower(trim($input));
// New way
$input |> trim(...) |> strtolower(...);
/*** array_first() and array_last() ***/
$items = ['a' => 10, 'b' => 20, 'c' => 30];
echo array_first($items); // 10
echo array_last($items); // 30
/*** The #[NoDiscard] Attribute ***/
#[NoDiscard]
function validateToken($token) { ... }
validateToken($input); // PHP Warning: Return value of validateToken() must be used.
13.03.2026 | Peter Majmesku
PHP 8.5: New features
The New URI Extension
php_8.5_examples_uri_extension.php
<?php
##### URI Extension #####
// PHP 8.4 way
$components = parse_url('https://php.net/releases/8.4/en.php');
var_dump($components['host']);
// string(7) "php.net"
// PHP 8.5 URI Extension
use Uri\Rfc3986\Uri;
$uri = new Uri('https://php.net/releases/8.5/en.php');
var_dump($uri->getHost());
// string(7) "php.net"
The New Pipe Operator
php_8.5_pipe_operator.php
<?php
##### Pipe Operator #####
// PHP 8.4 way
$title = ' PHP 8.5 Released ';
$slug = strtolower(
str_replace('.', '',
str_replace(' ', '-',
trim($title)
)
)
);
var_dump($slug);
// string(15) "php-85-released"
// PHP 8.5 Pipe Operator
$title = ' PHP 8.5 Released ';
$slug = $title
|> trim(...)
|> (fn($str) => str_replace(' ', '-', $str))
|> (fn($str) => str_replace('.', '', $str))
|> strtolower(...);
var_dump($slug);
// string(15) "php-85-released"
21.11.2025 | Peter Majmesku
PHP 8.5 New Features: Pipe Operator and URI Extension
my_module.module
use Drupal\views\ViewExecutable;
use Drupal\my_module\Access\NodePageAccessControlHandler;
use Drupal\my_module\Access\RestrictedMediaAccessControlHandler;
/**
* Implements hook_entity_type_alter().
*/
function my_module_entity_type_alter(array &$entity_types) {
if (isset($entity_types['node'])) {
$entity_types['node']->setAccessClass(NodePageAccessControlHandler::class);
}
if (isset($entity_types['media'])) {
$entity_types['media']->setAccessClass(RestrictedMediaAccessControlHandler::class);
}
}
/**
* Implements hook_views_post_execute().
*/
function my_module_views_post_execute(ViewExecutable $view) {
// Only execute if we are on the right view display.
if ($view->id() === 'MY_VIEW_ID' && $view->current_display === 'MY_VIEW_DISPLAY' ) {
// Mark this view results as token-allowed in the session for a short time.
$ttl = 3600; // 1 hour; adjust to taste.
$now = \Drupal::time()->getRequestTime();
$session = \Drupal::service('request_stack')->getCurrentRequest()->getSession();
$grants = (array) $session->get('my_module.token_mids', []);
foreach ($view->result as $item) {
$grants[(int) $item->mid] = $now + $ttl;
}
$session->set('my_module.token_mids', $grants);
}
}NodePageAccessControlHandler.php
namespace Drupal\my_module\Access;
/**
* Override Node Access Control Handler.
*/
class NodePageAccessControlHandler extends NodeAccessControlHandler {
...
/**
* {@inheritdoc}
*/
public function access(EntityInterface $entity, $operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
if ($operation === 'view') {
// Mark this node as token-allowed in the session for a short time.
$ttl = 3600; // 1 hour; adjust to taste.
$now = $this->time->getRequestTime();
$session = $this->requestStack->getSession();
$grants = (array) $session->get('my_module.token_nids', []);
$grants[(int) $entity->id()] = $now + $ttl;
$session->set('my_module.token_nids', $grants);
}
}
RestrictedMediaAccessControlHandler.php
namespace Drupal\my_module\Access;
/**
* Override Media Access Control Handler.
*/
class RestrictedMediaAccessControlHandler extends MediaAccessControlHandler {
/**
* {@inheritdoc
*
* Restrict anonymous view of Media (bundle "restricted_file") to cases where
* the user can view the host Node (via paragraph field
* 'field_download_restricted' or a view listing media entities),
* OR they have a valid short-lived token grant for that Node stored in session.
*/
public function access(EntityInterface $entity, $operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) {
if (
$operation !== 'view'
|| !$account
|| $entity->bundle() !== 'restricted_file'
) {
return parent::access($entity, $operation, $account, $return_as_object);
}
$paragraphs = $this->entityTypeManager
->getStorage('paragraph')
->loadByProperties(['field_download_restricted' => $entity->id()]);
// Check if we have a media entity id and a cookie for it does exist.
$media_entity = NULL;
if (array_key_exists($entity->id(), $mgrants = (array) $this->requestStack->getSession()->get('my_module.token_mids', []))) {
$media_entity = $this->entityTypeManager
->getStorage('media')
->loadByProperties(['mid' => $entity->id()]);
$now = $this->time->getRequestTime();
// Session-granted access via token.
$mid = (int) $entity->id();
if (!empty($mgrants[$mid]) && $mgrants[$mid] >= $now) {
return AccessResult::allowed()
->addCacheableDependency($entity)
->addCacheableDependency(reset($media_entity))
// Important for anon/session-based grants.
->mergeCacheMaxAge(0);
}
}
$result = AccessResult::forbidden()
->addCacheableDependency($entity)
->cachePerPermissions();
if (!$paragraphs && !$media_entity) {
return $result;
}
// Read session grants set by my_module_node_access().
$now = $this->time->getRequestTime();
$grants = (array) $this->requestStack->getSession()->get('my_module.token_nids', []);
foreach ($paragraphs as $paragraph) {
if ($paragraph instanceof ParagraphInterface) {
$result = $result->addCacheableDependency($paragraph);
}
$node = $this->getAncestorEntity($paragraph);
if (!$node instanceof NodeInterface) {
continue;
}
// Always add the node as a dependency.
$result = $result->addCacheableDependency($node);
// Session-granted access via token.
$nid = (int) $node->id();
if (!empty($grants[$nid]) && $grants[$nid] >= $now) {
return AccessResult::allowed()
->addCacheableDependency($entity)
->addCacheableDependency($node)
// Important for anon/session-based grants.
->mergeCacheMaxAge(0);
}
// Existing rule: inherit from node view access.
if ($node->access('view', $account)) {
return AccessResult::allowed()
->addCacheableDependency($entity)
->addCacheableDependency($node)
->cachePerPermissions();
}
return $result;
}
}
}
13.11.2025 | Michael Ebert
Manage access to private files in paragraphs or views on restricted nodes
CustomWidget.php
public static function isApplicable(FieldDefinitionInterface $field_definition) {
if (isset($field_definition->getDisplayOptions('form')['type'])) {
return $field_definition->getDisplayOptions('form')['type'] == 'slot_content_weights';
}
return FALSE;
}
13.11.2025 | Dominik Wille
Don't list a widget for a field type.
19.12.2025 | Michael Ebert
How to run a PHPUnit test for custom Drupal code
Example.php
<?php
declare(strict_types=1);
namespace Drupal\my_module;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
class Example {
public function __construct(
#[Autowire(service: 'current_user')]
private readonly AccountInterface $currentUser,
) {
}
public function getCurrentUser(): AccountInterface {
return $this->currentUser;
}
}my_module.services.yml
services:
my_module.example:
class: Drupal\my_module\Service\Example
autowire: true
Drupal\my_module\Service\Example: '@my_module.example'
13.11.2025 | Peter Majmesku
Autowire services with the Autowire attribute in Drupal
query_manipulate.php
<?php
namespace Drupal\my_module_migrate\Plugin\migrate\source\d8;
use Drupal\migrate\Row;
/**
* Base class for D8 source plugins to collect field values from Field API.
*
* Available configuration keys:
* - id: (optional) The id of the content which should get migrated. This can
* be useful to only migrate a selected set of nodes. Excepts multiple ids.
*
* @MigrateSource(
* id = "my_module_d8_gallery",
* source_provider = "my_module_migrate"
* )
*/
class MyModuleGallery extends ContentEntity {
/**
* {@inheritdoc}
*/
public function query() {
$query = parent::query();
if (isset($this->configuration['id'])) {
$entityDefinition = $this->entityTypeManager->getDefinition($this->configuration['entity_type']);
$idKey = $entityDefinition->getKey('id');
$query->condition("d.{$idKey}", $this->configuration['id'], 'IN');
}
// Left join to get the nid of the old image node.
$query->leftJoin('paragraph__field_referenzfeld_galerie', 'pfrg', 'pfrg.entity_id = b.id AND pfrg.revision_id = b.revision_id');
$query->leftJoin('node__field_gallery_bild', 'nfgb', 'nfgb.entity_id = pfrg.field_referenzfeld_galerie_target_id');
$query->leftJoin('node__field_base_public_title', 'nfbpt', 'nfbpt.entity_id = pfrg.field_referenzfeld_galerie_target_id');
$query->addExpression('GROUP_CONCAT(nfgb.field_gallery_bild_target_id)', 'gallery_image_ids');
// Add public title field.
$query->addField('nfbpt', 'field_base_public_title_value', 'field_base_public_title_value');
$query->groupBy('b.id');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
$return = parent::prepareRow($row);
$nids = [];
$ids_string = $row->getSourceProperty('gallery_image_ids');
if (!empty($ids_string)) {
$ids = explode(',', $ids_string);
foreach ($ids as $id) {
$nids[] = ['nid' => (int) $id];
}
// Add new source property to skip old gallery node within migration.
$row->setSourceProperty('field_referenzfeld_galerie', $nids);
}
$row->setSourceProperty('field_base_public_title', $row->getSourceProperty('field_base_public_title_value'));
return $return;
}
}
query.sql
SELECT
GROUP_CONCAT(nfgb.field_gallery_bild_target_id),
nfbpt.field_base_public_title_value
FROM paragraph__field_referenzfeld_galerie AS pfrg
LEFT JOIN node__field_gallery_bild AS nfgb
ON nfgb.entity_id = pfrg.field_referenzfeld_galerie_target_id
LEFT JOIN node__field_base_public_title AS nfbpt
ON nfbpt.entity_id = pfrg.field_referenzfeld_galerie_target_id
WHERE pfrg.entity_id = 100810 AND pfrg.revision_id = 1919384
10.07.2025 | Lothar Ferreira Neumann