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"

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;
    }
  }

}

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.

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'

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

 

redirect.install
/**
 * Save the bulk delete action to config.
 */
function redirect_update_8104() {
  if (!Action::load('redirect_delete_action')) {
    $entity_type_manager = \Drupal::entityTypeManager();
    $module_handler = \Drupal::moduleHandler();

    // Save the bulk delete action to config.
    $config_install_path = $module_handler->getModule('redirect')->getPath() . '/' . InstallStorage::CONFIG_INSTALL_DIRECTORY;
    $storage = new FileStorage($config_install_path);
    $entity_type_manager
      ->getStorage('action')
      ->create($storage->read('system.action.redirect_delete_action'))
      ->trustData()
      ->save();
  }
}