php

MyModuleViewsHooks.php
#[Hook('views_data_alter')]
public function viewsDataAlter(array &$data): void {
  // Overrides are non-default revisions with a site_id.
  $subquery = 'SELECT 1 FROM {node_field_revision} rev WHERE rev.nid = @base.nid AND rev.langcode = @base.langcode AND rev.site_id IS NOT NULL';
  $data['node_field_data']['is_overridden'] = [
    'title' => $this->t('Is overridden'),
    'field' => ['id' => 'exists_subquery', 'click sortable' => TRUE, 'exists subquery' => $subquery],
    'filter' => ['id' => 'exists_subquery', 'label' => $this->t('Is overridden'), 'exists subquery' => $subquery],
  ];
}
ExistsSubqueryField.php
<?php

declare(strict_types=1);

namespace Drupal\my_module\Plugin\views\field;

use Drupal\views\Attribute\ViewsField;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;

/**
 * Sortable yes/no column backed by an EXISTS subquery from views data.
 *
 * '@base' in the 'exists subquery' definition is replaced with the alias of
 * the base table.
 */
#[ViewsField('exists_subquery')]
class ExistsSubquery extends FieldPluginBase {

  public function query() {
    $this->ensureMyTable();
    $exists = 'EXISTS (' . str_replace('@base', $this->tableAlias, $this->definition['exists subquery']) . ')';
    $this->field_alias = $this->query->addField(NULL, "CASE WHEN $exists THEN 1 ELSE 0 END", $this->realField);
  }

  public function clickSort($order) {
    $this->query->addOrderBy(NULL, NULL, $order, $this->field_alias);
  }

  public function render(ResultRow $values) {
    return $this->getValue($values) ? $this->t('yes') : $this->t('no');
  }

}
ExistsSubqueryFilter.php
<?php

declare(strict_types=1);

namespace Drupal\my_module\Plugin\views\filter;

use Drupal\views\Attribute\ViewsFilter;
use Drupal\views\Plugin\views\filter\BooleanOperator;

/**
 * Yes/no filter backed by an EXISTS subquery from views data.
 */
#[ViewsFilter('exists_subquery')]
class ExistsSubquery extends BooleanOperator {

  public function defaultExposeOptions() {
    parent::defaultExposeOptions();
    // BooleanOperator exposes as required without a stored value, which fails
    // form validation. Default to "- Any -" instead.
    $this->options['expose']['required'] = FALSE;
    $this->options['value'] = 'All';
  }

  public function query() {
    if ($this->value === 'All') {
      return;
    }
    $this->ensureMyTable();
    $exists = 'EXISTS (' . str_replace('@base', $this->tableAlias, $this->definition['exists subquery']) . ')';
    $this->query->addWhereExpression($this->options['group'], empty($this->value) ? "NOT $exists" : $exists);
  }

}
MyModuleViewsHooks.php
<?php

declare(strict_types=1);

namespace Drupal\my_module\Hook;

use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\StringTranslation\StringTranslationTrait;

final class MyModuleViewsHooks {

  use StringTranslationTrait;

  #[Hook('views_data_alter')]
  public function viewsDataAlter(array &$data): void {
    $subquery = 'SELECT 1 FROM {node__field_related_content} ref WHERE ref.field_related_content_target_id = @base.nid';
    $data['node_field_data']['is_referenced'] = [
      'title' => $this->t('Is referenced'),
      'field' => ['id' => 'exists_subquery', 'click sortable' => TRUE, 'exists subquery' => $subquery],
      'filter' => ['id' => 'exists_subquery', 'label' => $this->t('Is referenced'), 'exists subquery' => $subquery],
    ];
  }

}
ECA.php
// web/modules/contrib/eca/src/Entity/Eca.php (simplified excerpt)

protected function isValidationDisabled(): bool {
  return $this->request()->query->get('eca_validation', '') === 'off';
}

public function getPluginCollections(): array {
  if ($this->isValidationDisabled()) {
    return [];
  }
  // Otherwise every referenced event/condition/action plugin is
  // instantiated here, throwing PluginNotFoundException if one
  // of them no longer exists.
  ...
}
hint.txt
# Delete URL — load this directly, don't click through from a listing page
/admin/config/workflow/eca/{eca_id}/delete?eca_validation=off

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.