Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,114 @@
<?php
namespace Drupal\block_content;
use Drupal\block_content\Event\BlockContentGetDependencyEvent;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Access\DependentAccessInterface;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityHandlerInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Contracts\EventDispatcher\EventDispatcherInterface;
/**
* Defines the access control handler for the content block entity type.
*
* @see \Drupal\block_content\Entity\BlockContent
*/
class BlockContentAccessControlHandler extends EntityAccessControlHandler implements EntityHandlerInterface {
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* BlockContentAccessControlHandler constructor.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $dispatcher
* The event dispatcher.
*/
public function __construct(EntityTypeInterface $entity_type, EventDispatcherInterface $dispatcher) {
parent::__construct($entity_type);
$this->eventDispatcher = $dispatcher;
}
/**
* {@inheritdoc}
*/
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
return new static(
$entity_type,
$container->get('event_dispatcher')
);
}
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
assert($entity instanceof BlockContentInterface);
$bundle = $entity->bundle();
$forbidIfNotReusable = fn (): AccessResultInterface => AccessResult::forbiddenIf($entity->isReusable() === FALSE, sprintf('Block content must be reusable to use `%s` operation', $operation));
$access = AccessResult::allowedIfHasPermissions($account, ['administer block content']);
if (!$access->isAllowed()) {
$access = match ($operation) {
// Allow view and update access to user with the 'edit any (type) block
// content' permission or the 'administer block content' permission.
'view' => AccessResult::allowedIf($entity->isPublished())
->orIf(AccessResult::allowedIfHasPermission($account, 'access block library')),
'update' => AccessResult::allowedIfHasPermission($account, 'edit any ' . $bundle . ' block content'),
'delete' => AccessResult::allowedIfHasPermission($account, 'delete any ' . $bundle . ' block content'),
// Revisions.
'view revision', 'view all revisions' => AccessResult::allowedIfHasPermission($account, 'view any ' . $bundle . ' block content history'),
'revert' => AccessResult::allowedIfHasPermission($account, 'revert any ' . $bundle . ' block content revisions')
->orIf($forbidIfNotReusable()),
'delete revision' => AccessResult::allowedIfHasPermission($account, 'delete any ' . $bundle . ' block content revisions')
->orIf($forbidIfNotReusable()),
default => parent::checkAccess($entity, $operation, $account),
};
}
// Add the entity as a cacheable dependency because access will at least be
// determined by whether the block is reusable.
$access->addCacheableDependency($entity);
if ($entity->isReusable() === FALSE && $access->isForbidden() !== TRUE) {
if (!$entity instanceof DependentAccessInterface) {
throw new \LogicException("Non-reusable block entities must implement \Drupal\block_content\Access\DependentAccessInterface for access control.");
}
$dependency = $entity->getAccessDependency();
if (empty($dependency)) {
// If an access dependency has not been set let modules set one.
$event = new BlockContentGetDependencyEvent($entity);
$this->eventDispatcher->dispatch($event, BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY);
$dependency = $event->getAccessDependency();
if (empty($dependency)) {
return AccessResult::forbidden("Non-reusable blocks must set an access dependency for access control.");
}
}
/** @var \Drupal\Core\Entity\EntityInterface $dependency */
$access = $access->andIf($dependency->access($operation, $account, TRUE));
}
return $access;
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
return AccessResult::allowedIfHasPermissions($account, [
'create ' . $entity_bundle . ' block content',
'administer block content',
], 'OR');
}
}

View File

@ -0,0 +1,31 @@
<?php
namespace Drupal\block_content;
/**
* Defines events for the block_content module.
*
* @see \Drupal\block_content\Event\BlockContentGetDependencyEvent
*
* @internal
*/
final class BlockContentEvents {
/**
* Name of the event when getting the dependency of a non-reusable block.
*
* This event allows modules to provide a dependency for non-reusable block
* access if
* \Drupal\block_content\Access\DependentAccessInterface::getAccessDependency()
* did not return a dependency during access checking.
*
* @Event
*
* @see \Drupal\block_content\Event\BlockContentGetDependencyEvent
* @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess()
*
* @var string
*/
const BLOCK_CONTENT_GET_DEPENDENCY = 'block_content.get_dependency';
}

View File

@ -0,0 +1,134 @@
<?php
namespace Drupal\block_content;
use Drupal\Component\Utility\Html;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Form handler for the content block edit forms.
*
* @internal
*/
class BlockContentForm extends ContentEntityForm {
/**
* The block content entity.
*
* @var \Drupal\block_content\BlockContentInterface
*/
protected $entity;
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$block = $this->entity;
$form = parent::form($form, $form_state);
if ($this->operation == 'edit') {
$form['#title'] = $this->t('Edit content block %label', ['%label' => $block->label()]);
}
// Override the default CSS class name, since the user-defined content block
// type name in 'TYPE-block-form' potentially clashes with third-party class
// names.
$form['#attributes']['class'][0] = 'block-' . Html::getClass($block->bundle()) . '-form';
return $form;
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state): array {
$element = parent::actions($form, $form_state);
if ($this->getRequest()->query->has('theme')) {
$element['submit']['#value'] = $this->t('Save and configure');
}
if ($this->currentUser()->hasPermission('administer blocks') && !$this->getRequest()->query->has('theme') && $this->entity->isNew()) {
$element['configure_block'] = [
'#type' => 'submit',
'#value' => $this->t('Save and configure'),
'#weight' => 20,
'#submit' => array_merge($element['submit']['#submit'], ['::configureBlock']),
];
}
return $element;
}
/**
* Form submission handler for the 'configureBlock' action.
*
* @param array $form
* An associative array containing the structure of the form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The current state of the form.
*/
public function configureBlock(array $form, FormStateInterface $form_state): void {
$block = $this->entity;
if (!$theme = $block->getTheme()) {
$theme = $this->config('system.theme')->get('default');
}
$form_state->setRedirect(
'block.admin_add',
[
'plugin_id' => 'block_content:' . $block->uuid(),
'theme' => $theme,
]
);
$form_state->setIgnoreDestination();
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$block = $this->entity;
$insert = $block->isNew();
$block->save();
$context = ['@type' => $block->bundle(), '%info' => $block->label()];
$logger = $this->logger('block_content');
$block_type = $this->getBundleEntity();
$t_args = ['@type' => $block_type->label(), '%info' => $block->label()];
if ($insert) {
$logger->info('@type: added %info.', $context);
$this->messenger()->addStatus($this->t('@type %info has been created.', $t_args));
}
else {
$logger->info('@type: updated %info.', $context);
$this->messenger()->addStatus($this->t('@type %info has been updated.', $t_args));
}
if ($block->id()) {
$form_state->setValue('id', $block->id());
$form_state->set('id', $block->id());
$theme = $block->getTheme();
if ($insert && $theme) {
$form_state->setRedirect(
'block.admin_add',
[
'plugin_id' => 'block_content:' . $block->uuid(),
'theme' => $theme,
'region' => $this->getRequest()->query->getString('region'),
]
);
}
else {
$form_state->setRedirectUrl($block->toUrl('collection'));
}
}
else {
// In the unlikely case something went wrong on save, the block will be
// rebuilt and block form redisplayed.
$this->messenger()->addError($this->t('The block could not be saved.'));
$form_state->setRebuild();
}
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Access\RefinableDependentAccessInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\Core\Entity\RevisionLogInterface;
/**
* Provides an interface defining a content block entity.
*/
interface BlockContentInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityPublishedInterface, RefinableDependentAccessInterface {
/**
* Sets the block description.
*
* @param string $info
* The block description.
*
* @return $this
* The class instance that this method is called on.
*/
public function setInfo($info);
/**
* Determines if the block is reusable or not.
*
* @return bool
* Returns TRUE if reusable and FALSE otherwise.
*/
public function isReusable();
/**
* Sets the block to be reusable.
*
* @return $this
*/
public function setReusable();
/**
* Sets the block to be non-reusable.
*
* @return $this
*/
public function setNonReusable();
/**
* Sets the theme value.
*
* When creating a new block content block from the block library, the user is
* redirected to the configure form for that block in the given theme. The
* theme is stored against the block when the block content add form is shown.
*
* @param string $theme
* The theme name.
*
* @return $this
* The class instance that this method is called on.
*/
public function setTheme($theme);
/**
* Gets the theme value.
*
* When creating a new block content block from the block library, the user is
* redirected to the configure form for that block in the given theme. The
* theme is stored against the block when the block content add form is shown.
*
* @return string
* The theme name.
*/
public function getTheme();
/**
* Gets the configured instances of this content block.
*
* @return array
* Array of Drupal\block\Core\Plugin\Entity\Block entities.
*/
public function getInstances();
}

View File

@ -0,0 +1,47 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
/**
* Defines a class to build a listing of content block entities.
*
* @see \Drupal\block_content\Entity\BlockContent
*/
class BlockContentListBuilder extends EntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['label'] = $this->t('Block description');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
protected function getEntityIds() {
$query = $this->getStorage()->getQuery()
->accessCheck(TRUE)
->sort($this->entityType->getKey('id'));
$query->condition('reusable', TRUE);
// Only add the pager if a limit is specified.
if ($this->limit) {
$query->pager($this->limit);
}
return $query->execute();
}
}

View File

@ -0,0 +1,84 @@
<?php
namespace Drupal\block_content;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Drupal\Core\Entity\BundlePermissionHandlerTrait;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Provide dynamic permissions for blocks of different types.
*/
class BlockContentPermissions implements ContainerInjectionInterface {
use StringTranslationTrait;
use BundlePermissionHandlerTrait;
/**
* Constructs a BlockContentPermissions instance.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* Entity type manager.
*/
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
) {
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('entity_type.manager'),
);
}
/**
* Build permissions for each block type.
*
* @return array
* The block type permissions.
*/
public function blockTypePermissions() {
return $this->generatePermissions($this->entityTypeManager->getStorage('block_content_type')->loadMultiple(), [$this, 'buildPermissions']);
}
/**
* Return all the permissions available for a block type.
*
* @param \Drupal\block_content\Entity\BlockContentType $type
* The block type.
*
* @return array
* Permissions available for the given block type.
*/
protected function buildPermissions(BlockContentType $type) {
$type_id = $type->id();
$type_params = ['%type_name' => $type->label()];
return [
"create $type_id block content" => [
'title' => $this->t('%type_name: Create new content block', $type_params),
],
"edit any $type_id block content" => [
'title' => $this->t('%type_name: Edit content block', $type_params),
],
"delete any $type_id block content" => [
'title' => $this->t('%type_name: Delete content block', $type_params),
],
"view any $type_id block content history" => [
'title' => $this->t('%type_name: View content block history pages', $type_params),
],
"revert any $type_id block content revisions" => [
'title' => $this->t('%type_name: Revert content block revisions', $type_params),
],
"delete any $type_id block content revisions" => [
'title' => $this->t('%type_name: Delete content block revisions', $type_params),
],
];
}
}

View File

@ -0,0 +1,27 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Entity\Sql\SqlContentEntityStorageSchema;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
/**
* Defines the block content schema handler.
*/
class BlockContentStorageSchema extends SqlContentEntityStorageSchema {
/**
* {@inheritdoc}
*/
protected function getSharedTableFieldSchema(FieldStorageDefinitionInterface $storage_definition, $table_name, array $column_mapping): array {
$schema = parent::getSharedTableFieldSchema($storage_definition, $table_name, $column_mapping);
$field_name = $storage_definition->getName();
if ($table_name === $this->storage->getDataTable() && $field_name === 'reusable') {
$this->addSharedTableFieldIndex($storage_definition, $schema);
}
return $schema;
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace Drupal\block_content;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Core\Entity\EntityInterface;
use Drupal\content_translation\ContentTranslationHandler;
/**
* Defines the translation handler for content blocks.
*/
class BlockContentTranslationHandler extends ContentTranslationHandler {
/**
* {@inheritdoc}
*/
protected function entityFormTitle(EntityInterface $entity) {
$block_type = BlockContentType::load($entity->bundle());
return $this->t('<em>Edit @type</em> @title', ['@type' => $block_type->label(), '@title' => $entity->label()]);
}
}

View File

@ -0,0 +1,128 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\language\Entity\ContentLanguageSettings;
/**
* The block content type entity form.
*
* @internal
*/
class BlockContentTypeForm extends BundleEntityFormBase {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state) {
$form = parent::form($form, $form_state);
/** @var \Drupal\block_content\BlockContentTypeInterface $block_type */
$block_type = $this->entity;
if ($this->operation == 'add') {
$form['#title'] = $this->t('Add block type');
}
else {
$form['#title'] = $this->t('Edit %label block type', ['%label' => $block_type->label()]);
}
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $block_type->label(),
'#description' => $this->t("The human-readable name for this block type, displayed on the <em>Block types</em> page."),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $block_type->id(),
'#machine_name' => [
'exists' => '\Drupal\block_content\Entity\BlockContentType::load',
],
'#description' => $this->t("Unique machine-readable name: lowercase letters, numbers, and underscores only."),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
];
$form['description'] = [
'#type' => 'textarea',
'#default_value' => $block_type->getDescription(),
'#description' => $this->t('Displays on the <em>Block types</em> page.'),
'#title' => $this->t('Description'),
];
$form['revision'] = [
'#type' => 'checkbox',
'#title' => $this->t('Create new revision'),
'#default_value' => $block_type->shouldCreateNewRevision(),
'#description' => $this->t('Create a new revision by default for this block type.'),
];
if ($this->moduleHandler->moduleExists('language')) {
$form['language'] = [
'#type' => 'details',
'#title' => $this->t('Language settings'),
'#group' => 'additional_settings',
];
$language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('block_content', $block_type->id());
$form['language']['language_configuration'] = [
'#type' => 'language_configuration',
'#entity_information' => [
'entity_type' => 'block_content',
'bundle' => $block_type->id(),
],
'#default_value' => $language_configuration,
];
$form['#submit'][] = 'language_configuration_element_submit';
}
$form['actions'] = ['#type' => 'actions'];
$form['actions']['submit'] = [
'#type' => 'submit',
'#value' => $this->t('Save'),
];
return $this->protectBundleIdElement($form);
}
/**
* {@inheritdoc}
*/
protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
// An empty description violates config schema.
if (trim($form_state->getValue('description', '')) === '') {
$form_state->unsetValue('description');
}
parent::copyFormValuesToEntity($entity, $form, $form_state);
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state) {
$block_type = $this->entity;
$status = $block_type->save();
$edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString();
$logger = $this->logger('block_content');
if ($status == SAVED_UPDATED) {
$this->messenger()->addStatus($this->t('Block type %label has been updated.', ['%label' => $block_type->label()]));
$logger->notice('Block type %label has been updated.', ['%label' => $block_type->label(), 'link' => $edit_link]);
}
else {
block_content_add_body_field($block_type->id());
$this->messenger()->addStatus($this->t('Block type %label has been added.', ['%label' => $block_type->label()]));
$logger->notice('Block type %label has been added.', ['%label' => $block_type->label(), 'link' => $edit_link]);
}
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
/**
* Provides an interface defining a block type entity.
*/
interface BlockContentTypeInterface extends ConfigEntityInterface, RevisionableEntityBundleInterface {
/**
* Returns the description of the block type.
*
* @return string
* The description of the type of this block.
*/
public function getDescription();
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Defines a class to build a listing of block type entities.
*
* @see \Drupal\block_content\Entity\BlockContentType
*/
class BlockContentTypeListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function getDefaultOperations(EntityInterface $entity) {
$operations = parent::getDefaultOperations($entity);
// Place the edit operation after the operations added by field_ui.module
// which have the weights 15, 20, 25.
if (isset($operations['edit'])) {
$operations['edit']['weight'] = 30;
}
return $operations;
}
/**
* {@inheritdoc}
*/
public function buildHeader() {
$header['type'] = $this->t('Block type');
$header['description'] = $this->t('Description');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity) {
$row['type'] = $entity->toLink(NULL, 'edit-form')->toString();
$row['description']['data']['#markup'] = $entity->getDescription();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
protected function getTitle() {
return $this->t('Block types');
}
}

View File

@ -0,0 +1,64 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Cache\CacheCollector;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Lock\LockBackendInterface;
/**
* A cache collector that caches IDs for block_content UUIDs.
*
* As block_content entities are used as block plugin derivatives, it is a
* fairly safe limitation that there are not hundreds of them, a site will
* likely run into problems with too many block content entities in other places
* than a cache that only stores UUID's and IDs. The same assumption is not true
* for other content entities.
*
* @internal
*/
class BlockContentUuidLookup extends CacheCollector {
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* Constructs a BlockContentUuidLookup instance.
*
* @param \Drupal\Core\Cache\CacheBackendInterface $cache
* The cache backend.
* @param \Drupal\Core\Lock\LockBackendInterface $lock
* The lock backend.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
*/
public function __construct(CacheBackendInterface $cache, LockBackendInterface $lock, EntityTypeManagerInterface $entity_type_manager) {
parent::__construct('block_content_uuid', $cache, $lock);
$this->entityTypeManager = $entity_type_manager;
}
/**
* {@inheritdoc}
*/
protected function resolveCacheMiss($key) {
$ids = $this->entityTypeManager->getStorage('block_content')->getQuery()
->accessCheck(FALSE)
->condition('uuid', $key)
->execute();
// Only cache if there is a match, otherwise creating new entities would
// require to invalidate the cache.
$id = reset($ids);
if ($id) {
$this->storage[$key] = $id;
$this->persist($key);
}
return $id;
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityViewBuilder;
/**
* View builder handler for content blocks.
*
* Note: Content blocks (block_content entities) are not designed to be
* displayed outside of blocks! This BlockContentViewBuilder class is designed
* to be used by \Drupal\block_content\Plugin\Block\BlockContentBlock::build()
* and by nothing else.
*
* @see \Drupal\block_content\Plugin\Block\BlockContentBlock
*/
class BlockContentViewBuilder extends EntityViewBuilder {
/**
* {@inheritdoc}
*/
public function view(EntityInterface $entity, $view_mode = 'full', $langcode = NULL) {
return $this->viewMultiple([$entity], $view_mode, $langcode)[0];
}
/**
* {@inheritdoc}
*/
public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) {
$build_list = parent::viewMultiple($entities, $view_mode, $langcode);
// Apply the buildMultiple() #pre_render callback immediately, to make
// bubbling of attributes and contextual links to the actual block work.
// @see \Drupal\block\BlockViewBuilder::buildBlock()
unset($build_list['#pre_render'][0]);
return $this->buildMultiple($build_list);
}
/**
* {@inheritdoc}
*/
protected function getBuildDefaults(EntityInterface $entity, $view_mode) {
$build = parent::getBuildDefaults($entity, $view_mode);
// The content block will be rendered in the wrapped block template already
// and thus has no entity template itself.
unset($build['#theme']);
return $build;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace Drupal\block_content;
use Drupal\views\EntityViewsData;
/**
* Provides the views data for the block_content entity type.
*/
class BlockContentViewsData extends EntityViewsData {
/**
* {@inheritdoc}
*/
public function getViewsData() {
$data = parent::getViewsData();
$data['block_content_field_data']['id']['field']['id'] = 'field';
$data['block_content_field_data']['info']['field']['id'] = 'field';
$data['block_content_field_data']['info']['field']['link_to_entity default'] = TRUE;
$data['block_content_field_data']['type']['field']['id'] = 'field';
$data['block_content_field_data']['table']['wizard_id'] = 'block_content';
$data['block_content']['block_content_listing_empty'] = [
'title' => $this->t('Empty block library behavior'),
'help' => $this->t('Provides a link to add a new block.'),
'area' => [
'id' => 'block_content_listing_empty',
],
];
// Advertise this table as a possible base table.
$data['block_content_field_revision']['table']['base']['help'] = $this->t('Block Content revision is a history of changes to block content.');
$data['block_content_field_revision']['table']['base']['defaults']['title'] = 'info';
return $data;
}
}

View File

@ -0,0 +1,33 @@
<?php
namespace Drupal\block_content;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityAccessControlHandler;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Session\AccountInterface;
/**
* Defines the access control handler for the "Block Type" entity type.
*
* @see \Drupal\block_content\Entity\BlockContentType
*/
class BlockTypeAccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected $viewLabelOperation = TRUE;
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
if ($operation === 'view label') {
return AccessResult::allowedIfHasPermission($account, 'access block library')
->orIf(parent::checkAccess($entity, $operation, $account));
}
return parent::checkAccess($entity, $operation, $account);
}
}

View File

@ -0,0 +1,143 @@
<?php
namespace Drupal\block_content\Controller;
use Drupal\block_content\BlockContentTypeInterface;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Extension\ThemeHandlerInterface;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;
/**
* Controller routines for custom block routes.
*/
class BlockContentController extends ControllerBase {
/**
* The content block storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockContentStorage;
/**
* The content block type storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockContentTypeStorage;
/**
* The theme handler.
*
* @var \Drupal\Core\Extension\ThemeHandlerInterface
*/
protected $themeHandler;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$entity_type_manager->getStorage('block_content'),
$entity_type_manager->getStorage('block_content_type'),
$container->get('theme_handler')
);
}
/**
* Constructs a BlockContent object.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $block_content_storage
* The content block storage.
* @param \Drupal\Core\Entity\EntityStorageInterface $block_content_type_storage
* The block type storage.
* @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
* The theme handler.
*/
public function __construct(EntityStorageInterface $block_content_storage, EntityStorageInterface $block_content_type_storage, ThemeHandlerInterface $theme_handler) {
$this->blockContentStorage = $block_content_storage;
$this->blockContentTypeStorage = $block_content_type_storage;
$this->themeHandler = $theme_handler;
}
/**
* Displays add content block links for available types.
*
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return array
* A render array for a list of the block types that can be added or
* if there is only one block type defined for the site, the function
* returns the content block add page for that block type.
*/
public function add(Request $request) {
// @todo deprecate see https://www.drupal.org/project/drupal/issues/3346394.
$types = [];
// Only use block types the user has access to.
foreach ($this->blockContentTypeStorage->loadMultiple() as $type) {
$access = $this->entityTypeManager()->getAccessControlHandler('block_content')->createAccess($type->id(), NULL, [], TRUE);
if ($access->isAllowed()) {
$types[$type->id()] = $type;
}
}
uasort($types, [$this->blockContentTypeStorage->getEntityType()->getClass(), 'sort']);
if ($types && count($types) == 1) {
$type = reset($types);
$query = $request->query->all();
return $this->redirect('block_content.add_form', ['block_content_type' => $type->id()], ['query' => $query]);
}
if (count($types) === 0) {
return [
'#markup' => $this->t('You have not created any block types yet. Go to the <a href=":url">block type creation page</a> to add a new block type.', [
':url' => Url::fromRoute('block_content.type_add')->toString(),
]),
];
}
return ['#theme' => 'block_content_add_list', '#content' => $types];
}
/**
* Presents the content block creation form.
*
* @param \Drupal\block_content\BlockContentTypeInterface $block_content_type
* The block type to add.
* @param \Symfony\Component\HttpFoundation\Request $request
* The current request object.
*
* @return array
* A form array as expected by
* \Drupal\Core\Render\RendererInterface::render().
*/
public function addForm(BlockContentTypeInterface $block_content_type, Request $request) {
$block = $this->blockContentStorage->create([
'type' => $block_content_type->id(),
]);
if (($theme = $request->query->get('theme')) && in_array($theme, array_keys($this->themeHandler->listInfo()))) {
// We have navigated to this page from the block library and will keep
// track of the theme for redirecting the user to the configuration page
// for the newly created block in the given theme.
$block->setTheme($theme);
}
return $this->entityFormBuilder()->getForm($block);
}
/**
* Provides the page title for this controller.
*
* @param \Drupal\block_content\BlockContentTypeInterface $block_content_type
* The block type being added.
*
* @return string
* The page title.
*/
public function getAddFormTitle(BlockContentTypeInterface $block_content_type) {
return $this->t('Add %type content block', ['%type' => $block_content_type->label()]);
}
}

View File

@ -0,0 +1,285 @@
<?php
namespace Drupal\block_content\Entity;
use Drupal\block_content\BlockContentAccessControlHandler;
use Drupal\block_content\BlockContentForm;
use Drupal\block_content\BlockContentListBuilder;
use Drupal\block_content\BlockContentStorageSchema;
use Drupal\block_content\BlockContentTranslationHandler;
use Drupal\block_content\BlockContentViewBuilder;
use Drupal\block_content\BlockContentViewsData;
use Drupal\block_content\Form\BlockContentDeleteForm;
use Drupal\Core\Access\RefinableDependentAccessTrait;
use Drupal\Core\Entity\Attribute\ContentEntityType;
use Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider;
use Drupal\Core\Entity\Form\RevisionRevertForm;
use Drupal\Core\Entity\Form\RevisionDeleteForm;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Entity\EditorialContentEntityBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\block_content\BlockContentInterface;
/**
* Defines the content block entity class.
*
* Note that render caching of block_content entities is disabled because they
* are always rendered as blocks, and blocks already have their own render
* caching.
* See https://www.drupal.org/node/2284917#comment-9132521 for more information.
*/
#[ContentEntityType(
id: 'block_content',
label: new TranslatableMarkup('Content block'),
label_collection: new TranslatableMarkup('Content blocks'),
label_singular: new TranslatableMarkup('content block'),
label_plural: new TranslatableMarkup('content blocks'),
render_cache: FALSE,
entity_keys: [
'id' => 'id',
'revision' => 'revision_id',
'bundle' => 'type',
'label' => 'info',
'langcode' => 'langcode',
'uuid' => 'uuid',
'published' => 'status',
],
handlers: [
'storage' => SqlContentEntityStorage::class,
'storage_schema' => BlockContentStorageSchema::class,
'access' => BlockContentAccessControlHandler::class,
'list_builder' => BlockContentListBuilder::class,
'view_builder' => BlockContentViewBuilder::class,
'views_data' => BlockContentViewsData::class,
'form' => [
'add' => BlockContentForm::class,
'edit' => BlockContentForm::class,
'delete' => BlockContentDeleteForm::class,
'default' => BlockContentForm::class,
'revision-delete' => RevisionDeleteForm::class,
'revision-revert' => RevisionRevertForm::class,
],
'route_provider' => ['revision' => RevisionHtmlRouteProvider::class],
'translation' => BlockContentTranslationHandler::class,
],
links: [
'canonical' => '/admin/content/block/{block_content}',
'delete-form' => '/admin/content/block/{block_content}/delete',
'edit-form' => '/admin/content/block/{block_content}',
'collection' => '/admin/content/block',
'create' => '/block',
'revision-delete-form' => '/admin/content/block/{block_content}/revision/{block_content_revision}/delete',
'revision-revert-form' => '/admin/content/block/{block_content}/revision/{block_content_revision}/revert',
'version-history' => '/admin/content/block/{block_content}/revisions',
],
admin_permission: 'administer block content',
collection_permission: 'access block library',
bundle_entity_type: 'block_content_type',
bundle_label: new TranslatableMarkup('Block type'),
base_table: 'block_content',
data_table: 'block_content_field_data',
revision_table: 'block_content_revision',
revision_data_table: 'block_content_field_revision',
translatable: TRUE,
show_revision_ui: TRUE,
label_count: [
'singular' => '@count content block',
'plural' => '@count content blocks',
],
field_ui_base_route: 'entity.block_content_type.edit_form',
revision_metadata_keys: [
'revision_user' => 'revision_user',
'revision_created' => 'revision_created',
'revision_log_message' => 'revision_log',
],
)]
class BlockContent extends EditorialContentEntityBase implements BlockContentInterface {
use RefinableDependentAccessTrait;
/**
* The theme the block is being created in.
*
* When creating a new content block from the block library, the user is
* redirected to the configure form for that block in the given theme. The
* theme is stored against the block when the content block add form is shown.
*
* @var string
*/
protected $theme;
/**
* {@inheritdoc}
*/
public function createDuplicate() {
$duplicate = parent::createDuplicate();
$duplicate->revision_id->value = NULL;
$duplicate->id->value = NULL;
return $duplicate;
}
/**
* {@inheritdoc}
*/
public function setTheme($theme) {
$this->theme = $theme;
return $this;
}
/**
* {@inheritdoc}
*/
public function getTheme() {
return $this->theme;
}
/**
* {@inheritdoc}
*/
public function postSave(EntityStorageInterface $storage, $update = TRUE) {
parent::postSave($storage, $update);
if ($this->isReusable() || $this->getOriginal()?->isReusable()) {
static::invalidateBlockPluginCache();
}
}
/**
* {@inheritdoc}
*/
public static function preDelete(EntityStorageInterface $storage, array $entities) {
parent::preDelete($storage, $entities);
/** @var \Drupal\block_content\BlockContentInterface $block */
foreach ($entities as $block) {
foreach ($block->getInstances() as $instance) {
$instance->delete();
}
}
}
/**
* {@inheritdoc}
*/
public static function postDelete(EntityStorageInterface $storage, array $entities) {
parent::postDelete($storage, $entities);
/** @var \Drupal\block_content\BlockContentInterface $block */
foreach ($entities as $block) {
if ($block->isReusable()) {
// If any deleted blocks are reusable clear the block cache.
static::invalidateBlockPluginCache();
return;
}
}
}
/**
* {@inheritdoc}
*/
public function getInstances() {
return \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['plugin' => 'block_content:' . $this->uuid()]);
}
/**
* {@inheritdoc}
*/
public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
parent::preSaveRevision($storage, $record);
if (!$this->isNewRevision() && $this->getOriginal() && empty($record->revision_log_message)) {
// If we are updating an existing block_content without adding a new
// revision and the user did not supply a revision log, keep the existing
// one.
$record->revision_log = $this->getOriginal()->getRevisionLogMessage();
}
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
/** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */
$fields = parent::baseFieldDefinitions($entity_type);
$fields['id']->setLabel(t('Content block ID'))
->setDescription(t('The content block ID.'));
$fields['uuid']->setDescription(t('The content block UUID.'));
$fields['revision_id']->setDescription(t('The revision ID.'));
$fields['langcode']->setDescription(t('The content block language code.'));
$fields['type']->setLabel(t('Block type'))
->setDescription(t('The block type.'));
$fields['revision_log']->setDescription(t('The log entry explaining the changes in this revision.'));
$fields['info'] = BaseFieldDefinition::create('string')
->setLabel(t('Block description'))
->setDescription(t('A brief description of your block.'))
->setRevisionable(TRUE)
->setTranslatable(TRUE)
->setRequired(TRUE)
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE);
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
->setDescription(t('The time that the content block was last edited.'))
->setTranslatable(TRUE)
->setRevisionable(TRUE);
$fields['reusable'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Reusable'))
->setDescription(t('A boolean indicating whether this block is reusable.'))
->setTranslatable(FALSE)
->setRevisionable(FALSE)
->setDefaultValue(TRUE);
return $fields;
}
/**
* {@inheritdoc}
*/
public function setInfo($info) {
$this->set('info', $info);
return $this;
}
/**
* {@inheritdoc}
*/
public function isReusable() {
return (bool) $this->get('reusable')->value;
}
/**
* {@inheritdoc}
*/
public function setReusable() {
return $this->set('reusable', TRUE);
}
/**
* {@inheritdoc}
*/
public function setNonReusable() {
return $this->set('reusable', FALSE);
}
/**
* Invalidates the block plugin cache after changes and deletions.
*/
protected static function invalidateBlockPluginCache() {
// Invalidate the block cache to update content block-based derivatives.
\Drupal::service('plugin.manager.block')->clearCachedDefinitions();
}
}

View File

@ -0,0 +1,107 @@
<?php
namespace Drupal\block_content\Entity;
use Drupal\block_content\BlockContentTypeForm;
use Drupal\block_content\BlockContentTypeListBuilder;
use Drupal\block_content\BlockTypeAccessControlHandler;
use Drupal\block_content\Form\BlockContentTypeDeleteForm;
use Drupal\Core\Entity\Attribute\ConfigEntityType;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
use Drupal\block_content\BlockContentTypeInterface;
use Drupal\user\Entity\EntityPermissionsRouteProvider;
/**
* Defines the block type entity.
*/
#[ConfigEntityType(
id: 'block_content_type',
label: new TranslatableMarkup('Block type'),
label_collection: new TranslatableMarkup('Block types'),
label_singular: new TranslatableMarkup('block type'),
label_plural: new TranslatableMarkup('block types'),
config_prefix: 'type',
entity_keys: [
'id' => 'id',
'label' => 'label',
],
handlers: [
'access' => BlockTypeAccessControlHandler::class,
'form' => [
'default' => BlockContentTypeForm::class,
'add' => BlockContentTypeForm::class,
'edit' => BlockContentTypeForm::class,
'delete' => BlockContentTypeDeleteForm::class,
],
'route_provider' => [
'html' => AdminHtmlRouteProvider::class,
'permissions' => EntityPermissionsRouteProvider::class,
],
'list_builder' => BlockContentTypeListBuilder::class,
],
links: [
'delete-form' => '/admin/structure/block-content/manage/{block_content_type}/delete',
'edit-form' => '/admin/structure/block-content/manage/{block_content_type}',
'entity-permissions-form' => '/admin/structure/block-content/manage/{block_content_type}/permissions',
'collection' => '/admin/structure/block-content',
],
admin_permission: 'administer block types',
bundle_of: 'block_content',
label_count: [
'singular' => '@count block type',
'plural' => '@count block types',
],
config_export: [
'id',
'label',
'revision',
'description',
],
)]
class BlockContentType extends ConfigEntityBundleBase implements BlockContentTypeInterface {
/**
* The block type ID.
*
* @var string
*/
protected $id;
/**
* The block type label.
*
* @var string
*/
protected $label;
/**
* The default revision setting for content blocks of this type.
*
* @var bool
*/
protected $revision = FALSE;
/**
* The description of the block type.
*
* @var string|null
*/
protected $description = NULL;
/**
* {@inheritdoc}
*/
public function getDescription() {
return $this->description ?? '';
}
/**
* {@inheritdoc}
*/
public function shouldCreateNewRevision() {
return $this->revision;
}
}

View File

@ -0,0 +1,70 @@
<?php
namespace Drupal\block_content\Event;
use Drupal\block_content\BlockContentInterface;
use Drupal\Core\Access\AccessibleInterface;
use Drupal\Component\EventDispatcher\Event;
/**
* Block content event to allow setting an access dependency.
*
* @internal
*/
class BlockContentGetDependencyEvent extends Event {
/**
* The block content entity.
*
* @var \Drupal\block_content\BlockContentInterface
*/
protected $blockContent;
/**
* The dependency.
*
* @var \Drupal\Core\Access\AccessibleInterface
*/
protected $accessDependency;
/**
* BlockContentGetDependencyEvent constructor.
*
* @param \Drupal\block_content\BlockContentInterface $blockContent
* The block content entity.
*/
public function __construct(BlockContentInterface $blockContent) {
$this->blockContent = $blockContent;
}
/**
* Gets the block content entity.
*
* @return \Drupal\block_content\BlockContentInterface
* The block content entity.
*/
public function getBlockContentEntity() {
return $this->blockContent;
}
/**
* Gets the access dependency.
*
* @return \Drupal\Core\Access\AccessibleInterface
* The access dependency.
*/
public function getAccessDependency() {
return $this->accessDependency;
}
/**
* Sets the access dependency.
*
* @param \Drupal\Core\Access\AccessibleInterface $access_dependency
* The access dependency.
*/
public function setAccessDependency(AccessibleInterface $access_dependency) {
$this->accessDependency = $access_dependency;
}
}

View File

@ -0,0 +1,25 @@
<?php
namespace Drupal\block_content\Form;
use Drupal\Core\Entity\ContentEntityDeleteForm;
/**
* Provides a confirmation form for deleting a content block entity.
*
* @internal
*/
class BlockContentDeleteForm extends ContentEntityDeleteForm {
/**
* {@inheritdoc}
*/
public function getDescription() {
$instances = $this->entity->getInstances();
if (!empty($instances)) {
return $this->formatPlural(count($instances), 'This will also remove 1 placed block instance. This action cannot be undone.', 'This will also remove @count placed block instances. This action cannot be undone.');
}
return parent::getDescription();
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace Drupal\block_content\Form;
use Drupal\Core\Entity\EntityDeleteForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a confirmation form for deleting a block type entity.
*
* @internal
*/
class BlockContentTypeDeleteForm extends EntityDeleteForm {
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state) {
$block_count = $this->entityTypeManager->getStorage('block_content')->getQuery()
->accessCheck(FALSE)
->condition('type', $this->entity->id())
->count()
->execute();
if ($block_count) {
$caption = '<p>' . $this->formatPlural($block_count, '%label is used by 1 content block on your site. You can not remove this block type until you have removed all of the %label blocks.', '%label is used by @count content blocks on your site. You may not remove %label until you have removed all of the %label content blocks.', ['%label' => $this->entity->label()]) . '</p>';
$form['description'] = ['#markup' => $caption];
return $form;
}
else {
return parent::buildForm($form, $form_state);
}
}
}

View File

@ -0,0 +1,163 @@
<?php
namespace Drupal\block_content\Hook;
use Drupal\block\BlockInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\block_content\BlockContentInterface;
use Drupal\Core\Database\Query\SelectInterface;
use Drupal\Core\Database\Query\AlterableInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Hook\Attribute\Hook;
/**
* Hook implementations for block_content.
*/
class BlockContentHooks {
use StringTranslationTrait;
/**
* Implements hook_help().
*/
#[Hook('help')]
public function help($route_name, RouteMatchInterface $route_match): ?string {
switch ($route_name) {
case 'help.page.block_content':
$field_ui = \Drupal::moduleHandler()->moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#';
$output = '';
$output .= '<h2>' . $this->t('About') . '</h2>';
$output .= '<p>' . $this->t('The Block Content module allows you to create and manage custom <em>block types</em> and <em>content-containing blocks</em>. For more information, see the <a href=":online-help">online documentation for the Block Content module</a>.', [':online-help' => 'https://www.drupal.org/documentation/modules/block_content']) . '</p>';
$output .= '<h2>' . $this->t('Uses') . '</h2>';
$output .= '<dl>';
$output .= '<dt>' . $this->t('Creating and managing block types') . '</dt>';
$output .= '<dd>' . $this->t('Users with the <em>Administer blocks</em> permission can create and edit block types with fields and display settings, from the <a href=":types">Block types</a> page under the Structure menu. For more information about managing fields and display settings, see the <a href=":field-ui">Field UI module help</a> and <a href=":field">Field module help</a>.', [
':types' => Url::fromRoute('entity.block_content_type.collection')->toString(),
':field-ui' => $field_ui,
':field' => Url::fromRoute('help.page', [
'name' => 'field',
])->toString(),
]) . '</dd>';
$output .= '<dt>' . $this->t('Creating content blocks') . '</dt>';
$output .= '<dd>' . $this->t('Users with the <em>Administer blocks</em> permission can create, edit, and delete content blocks of each defined block type, from the <a href=":block-library">Content blocks page</a>. After creating a block, place it in a region from the <a href=":blocks">Block layout page</a>, just like blocks provided by other modules.', [
':blocks' => Url::fromRoute('block.admin_display')->toString(),
':block-library' => Url::fromRoute('entity.block_content.collection')->toString(),
]) . '</dd>';
$output .= '</dl>';
return $output;
}
return NULL;
}
/**
* Implements hook_theme().
*/
#[Hook('theme')]
public function theme($existing, $type, $theme, $path) : array {
return [
'block_content_add_list' => [
'variables' => [
'content' => NULL,
],
'file' => 'block_content.pages.inc',
],
];
}
/**
* Implements hook_entity_type_alter().
*/
#[Hook('entity_type_alter')]
public function entityTypeAlter(array &$entity_types) : void {
/** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */
// Add a translation handler for fields if the language module is enabled.
if (\Drupal::moduleHandler()->moduleExists('language')) {
$translation = $entity_types['block_content']->get('translation');
$translation['block_content'] = TRUE;
$entity_types['block_content']->set('translation', $translation);
}
// Swap out the default EntityChanged constraint with a custom one with
// different logic for inline blocks.
$constraints = $entity_types['block_content']->getConstraints();
unset($constraints['EntityChanged']);
$constraints['BlockContentEntityChanged'] = NULL;
$entity_types['block_content']->setConstraints($constraints);
}
/**
* Implements hook_query_TAG_alter().
*
* Alters any 'entity_reference' query where the entity type is
* 'block_content' and the query has the tag 'block_content_access'.
*
* These queries should only return reusable blocks unless a condition on
* 'reusable' is explicitly set.
*
* Block_content entities that are not reusable should by default not be
* selectable as entity reference values. A module can still create an
* instance of \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface
* that will allow selection of non-reusable blocks by explicitly setting a
* condition on the 'reusable' field.
*
* @see \Drupal\block_content\BlockContentAccessControlHandler
*/
#[Hook('query_entity_reference_alter')]
public function queryEntityReferenceAlter(AlterableInterface $query): void {
if ($query instanceof SelectInterface && $query->getMetaData('entity_type') === 'block_content' && $query->hasTag('block_content_access')) {
$data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable();
if (array_key_exists($data_table, $query->getTables()) && !_block_content_has_reusable_condition($query->conditions(), $query->getTables())) {
$query->condition("{$data_table}.reusable", TRUE);
}
}
}
/**
* Implements hook_theme_suggestions_HOOK_alter() for block templates.
*/
#[Hook('theme_suggestions_block_alter')]
public function themeSuggestionsBlockAlter(array &$suggestions, array $variables): void {
$suggestions_new = [];
$content = $variables['elements']['content'];
$block_content = $variables['elements']['content']['#block_content'] ?? NULL;
if ($block_content instanceof BlockContentInterface) {
$bundle = $content['#block_content']->bundle();
$view_mode = strtr($variables['elements']['content']['#view_mode'], '.', '_');
$suggestions_new[] = 'block__block_content__view__' . $view_mode;
$suggestions_new[] = 'block__block_content__type__' . $bundle;
$suggestions_new[] = 'block__block_content__view_type__' . $bundle . '__' . $view_mode;
if (!empty($variables['elements']['#id'])) {
$suggestions_new[] = 'block__block_content__id__' . $variables['elements']['#id'];
$suggestions_new[] = 'block__block_content__id_view__' . $variables['elements']['#id'] . '__' . $view_mode;
}
// Remove duplicate block__block_content.
$suggestions = array_unique($suggestions);
array_splice($suggestions, 1, 0, $suggestions_new);
}
}
/**
* Implements hook_entity_operation().
*/
#[Hook('entity_operation')]
public function entityOperation(EntityInterface $entity) : array {
$operations = [];
if ($entity instanceof BlockInterface) {
$plugin = $entity->getPlugin();
if ($plugin->getBaseId() === 'block_content') {
$custom_block = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties(['uuid' => $plugin->getDerivativeId()]);
$custom_block = reset($custom_block);
if ($custom_block && $custom_block->access('update')) {
$operations['block-edit'] = [
'title' => $this->t('Edit block'),
'url' => $custom_block->toUrl('edit-form')->setOptions([]),
'weight' => 50,
];
}
}
}
return $operations;
}
}

View File

@ -0,0 +1,222 @@
<?php
namespace Drupal\block_content\Plugin\Block;
use Drupal\block_content\BlockContentUuidLookup;
use Drupal\block_content\Plugin\Derivative\BlockContent;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Block\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\Block\BlockManagerInterface;
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Routing\UrlGeneratorInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a generic block type.
*/
#[Block(
id: "block_content",
admin_label: new TranslatableMarkup("Content block"),
category: new TranslatableMarkup("Content block"),
deriver: BlockContent::class
)]
class BlockContentBlock extends BlockBase implements ContainerFactoryPluginInterface {
/**
* The Plugin Block Manager.
*
* @var \Drupal\Core\Block\BlockManagerInterface
*/
protected $blockManager;
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The Drupal account to use for checking for access to block.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $account;
/**
* The block content entity.
*
* @var \Drupal\block_content\BlockContentInterface
*/
protected $blockContent;
/**
* The URL generator.
*
* @var \Drupal\Core\Routing\UrlGeneratorInterface
*/
protected $urlGenerator;
/**
* The block content UUID lookup service.
*
* @var \Drupal\block_content\BlockContentUuidLookup
*/
protected $uuidLookup;
/**
* The entity display repository.
*
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
*/
protected $entityDisplayRepository;
/**
* Constructs a new BlockContentBlock.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Block\BlockManagerInterface $block_manager
* The Plugin Block Manager.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Session\AccountInterface $account
* The account for which view access should be checked.
* @param \Drupal\Core\Routing\UrlGeneratorInterface $url_generator
* The URL generator.
* @param \Drupal\block_content\BlockContentUuidLookup $uuid_lookup
* The block content UUID lookup service.
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
* The entity display repository.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, BlockManagerInterface $block_manager, EntityTypeManagerInterface $entity_type_manager, AccountInterface $account, UrlGeneratorInterface $url_generator, BlockContentUuidLookup $uuid_lookup, EntityDisplayRepositoryInterface $entity_display_repository) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->blockManager = $block_manager;
$this->entityTypeManager = $entity_type_manager;
$this->account = $account;
$this->urlGenerator = $url_generator;
$this->uuidLookup = $uuid_lookup;
$this->entityDisplayRepository = $entity_display_repository;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('plugin.manager.block'),
$container->get('entity_type.manager'),
$container->get('current_user'),
$container->get('url_generator'),
$container->get('block_content.uuid_lookup'),
$container->get('entity_display.repository')
);
}
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return [
'status' => TRUE,
'info' => '',
'view_mode' => 'full',
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state) {
$block = $this->getEntity();
if (!$block) {
return $form;
}
$options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle());
$form['view_mode'] = [
'#type' => 'select',
'#options' => $options,
'#title' => $this->t('View mode'),
'#description' => $this->t('Output the block in this view mode.'),
'#default_value' => $this->configuration['view_mode'],
'#access' => (count($options) > 1),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state) {
// Invalidate the block cache to update content block-based derivatives.
$this->configuration['view_mode'] = $form_state->getValue('view_mode');
$this->blockManager->clearCachedDefinitions();
}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account) {
if ($this->getEntity()) {
return $this->getEntity()->access('view', $account, TRUE);
}
return AccessResult::forbidden();
}
/**
* {@inheritdoc}
*/
public function build() {
if ($block = $this->getEntity()) {
return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']);
}
else {
return [
'#markup' => $this->t('Block with uuid %uuid does not exist. <a href=":url">Add content block</a>.', [
'%uuid' => $this->getDerivativeId(),
':url' => $this->urlGenerator->generate('block_content.add_page'),
]),
'#access' => $this->account->hasPermission('administer blocks'),
];
}
}
/**
* {@inheritdoc}
*/
public function createPlaceholder(): bool {
return TRUE;
}
/**
* Loads the block content entity of the block.
*
* @return \Drupal\block_content\BlockContentInterface|null
* The block content entity.
*/
protected function getEntity() {
if (!isset($this->blockContent)) {
$uuid = $this->getDerivativeId();
if ($id = $this->uuidLookup->get($uuid)) {
$this->blockContent = $this->entityTypeManager->getStorage('block_content')->load($id);
}
}
return $this->blockContent;
}
}

View File

@ -0,0 +1,60 @@
<?php
namespace Drupal\block_content\Plugin\Derivative;
use Drupal\Component\Plugin\Derivative\DeriverBase;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Retrieves block plugin definitions for all content blocks.
*/
class BlockContent extends DeriverBase implements ContainerDeriverInterface {
/**
* The content block storage.
*
* @var \Drupal\Core\Entity\EntityStorageInterface
*/
protected $blockContentStorage;
/**
* Constructs a BlockContent object.
*
* @param \Drupal\Core\Entity\EntityStorageInterface $block_content_storage
* The content block storage.
*/
public function __construct(EntityStorageInterface $block_content_storage) {
$this->blockContentStorage = $block_content_storage;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $base_plugin_id) {
$entity_type_manager = $container->get('entity_type.manager');
return new static(
$entity_type_manager->getStorage('block_content')
);
}
/**
* {@inheritdoc}
*/
public function getDerivativeDefinitions($base_plugin_definition) {
$block_contents = $this->blockContentStorage->loadByProperties(['reusable' => TRUE]);
// Reset the discovered definitions.
$this->derivatives = [];
/** @var \Drupal\block_content\Entity\BlockContent $block_content */
foreach ($block_contents as $block_content) {
$this->derivatives[$block_content->uuid()] = $base_plugin_definition;
$this->derivatives[$block_content->uuid()]['admin_label'] = $block_content->label() ?? ($block_content->type->entity->label() . ': ' . $block_content->id());
$this->derivatives[$block_content->uuid()]['config_dependencies']['content'] = [
$block_content->getConfigDependencyName(),
];
}
return parent::getDerivativeDefinitions($base_plugin_definition);
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace Drupal\block_content\Plugin\Menu\LocalAction;
use Drupal\Core\Menu\LocalActionDefault;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\Routing\RouteProviderInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
/**
* Modifies the 'Add content block' local action.
*/
class BlockContentAddLocalAction extends LocalActionDefault {
/**
* Constructs a BlockContentAddLocalAction object.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
RouteProviderInterface $routeProvider,
protected RequestStack $requestStack,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $routeProvider);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('router.route_provider'),
$container->get('request_stack'),
);
}
/**
* {@inheritdoc}
*/
public function getOptions(RouteMatchInterface $route_match) {
$options = parent::getOptions($route_match);
// If the route specifies a theme, append it to the query string.
if ($theme = $route_match->getParameter('theme')) {
$options['query']['theme'] = $theme;
}
// If the current request has a region, append it to the query string.
if ($region = $this->requestStack->getCurrentRequest()->query->getString('region')) {
$options['query']['region'] = $region;
}
return $options;
}
}

View File

@ -0,0 +1,18 @@
<?php
namespace Drupal\block_content\Plugin\Validation\Constraint;
use Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
/**
* Validation constraint for the block content entity changed timestamp.
*/
#[Constraint(
id: 'BlockContentEntityChanged',
label: new TranslatableMarkup('Block content entity changed', [], ['context' => 'Validation']),
type: ['entity']
)]
class BlockContentEntityChangedConstraint extends EntityChangedConstraint {
}

View File

@ -0,0 +1,30 @@
<?php
namespace Drupal\block_content\Plugin\Validation\Constraint;
use Drupal\block_content\BlockContentInterface;
use Drupal\Core\Entity\Plugin\Validation\Constraint\EntityChangedConstraintValidator;
use Symfony\Component\Validator\Constraint;
/**
* Validates the BlockContentEntityChanged constraint.
*/
class BlockContentEntityChangedConstraintValidator extends EntityChangedConstraintValidator {
/**
* {@inheritdoc}
*/
public function validate($entity, Constraint $constraint): void {
// This prevents saving an update to the block via a host entity's form if
// the host entity has had other changes made via the API instead of the
// entity form, such as a revision revert. This is safe, for example, in the
// Layout Builder the inline blocks are not saved until the whole layout is
// saved, in which case Layout Builder forces a new revision for the block.
// @see \Drupal\layout_builder\InlineBlockEntityOperations::handlePreSave.
if ($entity instanceof BlockContentInterface && !$entity->isReusable()) {
return;
}
parent::validate($entity, $constraint);
}
}

View File

@ -0,0 +1,53 @@
<?php
namespace Drupal\block_content\Plugin\migrate\source\d6;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 6 block source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_box",
* source_module = "block"
* )
*/
class Box extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
$query = $this->select('boxes', 'b')
->fields('b', ['bid', 'body', 'info', 'format']);
$query->orderBy('b.bid');
return $query;
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The numeric identifier of the block/box'),
'body' => $this->t('The block/box content'),
'info' => $this->t('Admin title of the block/box.'),
'format' => $this->t('Input format of the content block/box content.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['bid']['type'] = 'integer';
return $ids;
}
}

View File

@ -0,0 +1,28 @@
<?php
namespace Drupal\block_content\Plugin\migrate\source\d6;
use Drupal\block_content\Plugin\migrate\source\d7\BlockCustomTranslation as D7BlockCustomTranslation;
/**
* Drupal 6 i18n content block translations source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d6_box_translation",
* source_module = "i18nblocks"
* )
*/
class BoxTranslation extends D7BlockCustomTranslation {
/**
* Drupal 6 table names.
*/
const CUSTOM_BLOCK_TABLE = 'boxes';
const I18N_STRING_TABLE = 'i18n_strings';
}

View File

@ -0,0 +1,49 @@
<?php
namespace Drupal\block_content\Plugin\migrate\source\d7;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
/**
* Drupal 7 content block source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_block_custom",
* source_module = "block"
* )
*/
class BlockCustom extends DrupalSqlBase {
/**
* {@inheritdoc}
*/
public function query() {
return $this->select('block_custom', 'b')->fields('b');
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The numeric identifier of the block/box'),
'body' => $this->t('The block/box content'),
'info' => $this->t('Admin title of the block/box.'),
'format' => $this->t('Input format of the content block/box content.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['bid']['type'] = 'integer';
return $ids;
}
}

View File

@ -0,0 +1,105 @@
<?php
namespace Drupal\block_content\Plugin\migrate\source\d7;
use Drupal\migrate\Row;
use Drupal\migrate_drupal\Plugin\migrate\source\DrupalSqlBase;
use Drupal\migrate_drupal\Plugin\migrate\source\I18nQueryTrait;
/**
* Drupal 7 i18n content block translations source from database.
*
* For available configuration keys, refer to the parent classes.
*
* @see \Drupal\migrate\Plugin\migrate\source\SqlBase
* @see \Drupal\migrate\Plugin\migrate\source\SourcePluginBase
*
* @MigrateSource(
* id = "d7_block_custom_translation",
* source_module = "i18n_block"
* )
*/
class BlockCustomTranslation extends DrupalSqlBase {
use I18nQueryTrait;
/**
* Drupal 7 table names.
*/
const CUSTOM_BLOCK_TABLE = 'block_custom';
const I18N_STRING_TABLE = 'i18n_string';
/**
* {@inheritdoc}
*/
public function query() {
// Build a query based on blockCustomTable table where each row has the
// translation for only one property, either title or description. The
// method prepareRow() is then used to obtain the translation for the
// other property.
$query = $this->select(static::CUSTOM_BLOCK_TABLE, 'b')
->fields('b', ['bid', 'format', 'body'])
->fields('i18n', ['property'])
->fields('lt', ['lid', 'translation', 'language'])
->orderBy('b.bid');
// Use 'title' for the info field to match the property name in
// i18nStringTable.
$query->addField('b', 'info', 'title');
// Add in the property, which is either title or body. Cast the bid to text
// so PostgreSQL can make the join.
$query->leftJoin(static::I18N_STRING_TABLE, 'i18n', '[i18n].[objectid] = CAST([b].[bid] AS CHAR(255))');
$query->condition('i18n.type', 'block');
// Add in the translation for the property.
$query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]');
return $query;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row) {
if (!parent::prepareRow($row)) {
return FALSE;
}
// Set the i18n string table for use in I18nQueryTrait.
$this->i18nStringTable = static::I18N_STRING_TABLE;
// Save the translation for this property.
$property_in_row = $row->getSourceProperty('property');
// Get the translation for the property not already in the row and save it
// in the row.
$property_not_in_row = ($property_in_row === 'title') ? 'body' : 'title';
return $this->getPropertyNotInRowTranslation($row, $property_not_in_row, 'bid', $this->idMap);
}
/**
* {@inheritdoc}
*/
public function fields() {
return [
'bid' => $this->t('The block numeric identifier.'),
'format' => $this->t('Input format of the content block/box content.'),
'lid' => $this->t('i18n_string table id'),
'language' => $this->t('Language for this field.'),
'property' => $this->t('Block property'),
'translation' => $this->t('The translation of the value of "property".'),
'title' => $this->t('Block title.'),
'title_translated' => $this->t('Block title translation.'),
'body' => $this->t('Block body.'),
'body_translated' => $this->t('Block body translation.'),
];
}
/**
* {@inheritdoc}
*/
public function getIds() {
$ids['bid']['type'] = 'integer';
$ids['bid']['alias'] = 'b';
$ids['language']['type'] = 'string';
return $ids;
}
}

View File

@ -0,0 +1,89 @@
<?php
namespace Drupal\block_content\Plugin\views\area;
use Drupal\Core\Access\AccessManagerInterface;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
use Drupal\views\Attribute\ViewsArea;
use Drupal\views\Plugin\views\area\AreaPluginBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines an area plugin to display a block add link.
*
* @ingroup views_area_handlers
*/
#[ViewsArea("block_content_listing_empty")]
class ListingEmpty extends AreaPluginBase {
/**
* The access manager.
*
* @var \Drupal\Core\Access\AccessManagerInterface
*/
protected $accessManager;
/**
* The current user.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $currentUser;
/**
* Constructs a new ListingEmpty.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param mixed $plugin_definition
* The plugin implementation definition.
* @param \Drupal\Core\Access\AccessManagerInterface $access_manager
* The access manager.
* @param \Drupal\Core\Session\AccountInterface $current_user
* The current user.
*/
public function __construct(array $configuration, $plugin_id, $plugin_definition, AccessManagerInterface $access_manager, AccountInterface $current_user) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
$this->accessManager = $access_manager;
$this->currentUser = $current_user;
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
return new static(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('access_manager'),
$container->get('current_user')
);
}
/**
* {@inheritdoc}
*/
public function render($empty = FALSE) {
if (!$empty || !empty($this->options['empty'])) {
/** @var \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Cache\CacheableDependencyInterface $access_result */
$access_result = $this->accessManager->checkNamedRoute('block_content.add_page', [], $this->currentUser, TRUE);
$element = [
'#markup' => $this->t('Add a <a href=":url">content block</a>.', [':url' => Url::fromRoute('block_content.add_page')->toString()]),
'#access' => $access_result->isAllowed(),
'#cache' => [
'contexts' => $access_result->getCacheContexts(),
'tags' => $access_result->getCacheTags(),
'max-age' => $access_result->getCacheMaxAge(),
],
];
return $element;
}
return [];
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Drupal\block_content\Plugin\views\wizard;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsWizard;
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
/**
* Used for creating 'block_content' views with the wizard.
*/
#[ViewsWizard(
id: 'block_content',
title: new TranslatableMarkup('Content Block'),
base_table: 'block_content_field_data'
)]
class BlockContent extends WizardPluginBase {
/**
* {@inheritdoc}
*/
public function getFilters() {
$filters = parent::getFilters();
$filters['reusable'] = [
'id' => 'reusable',
'plugin_id' => 'boolean',
'table' => $this->base_table,
'field' => 'reusable',
'value' => '1',
'entity_type' => $this->entityTypeId,
'entity_field' => 'reusable',
];
return $filters;
}
}

View File

@ -0,0 +1,237 @@
<?php
namespace Drupal\block_content\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Routing\RouteBuildEvent;
use Drupal\Core\Routing\RouteSubscriberBase;
use Drupal\Core\Routing\RoutingEvents;
use Symfony\Component\Routing\RouteCollection;
/**
* Subscriber for Block content BC routes.
*/
class RouteSubscriber extends RouteSubscriberBase {
/**
* The entity type manager service.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The route collection for adding routes.
*
* @var \Symfony\Component\Routing\RouteCollection
*/
protected $collection;
/**
* The current base path.
*
* @var string
*/
protected $basePath;
/**
* The BC base path.
*
* @var string
*/
protected $basePathBc;
/**
* The redirect controller.
*
* @var string
*/
protected $controller;
/**
* Constructs a RouteSubscriber object.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager service.
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler) {
$this->entityTypeManager = $entity_type_manager;
$this->moduleHandler = $module_handler;
}
/**
* {@inheritdoc}
*/
protected function alterRoutes(RouteCollection $collection) {
$this->collection = $collection;
// @see block_content.routing.yml
if ($this->setUpBaseRoute('entity.block_content_type.collection')) {
$this->addRedirectRoute('block_content.type_add');
}
$entity_type = $this->entityTypeManager->getDefinition('block_content');
if ($this->setUpBaseRoute($entity_type->get('field_ui_base_route'))) {
foreach ($this->childRoutes($entity_type) as $route_name) {
$this->addRedirectRoute($route_name);
}
}
}
/**
* Gets parameters from a base route and saves them in class variables.
*
* @param string $base_route_name
* The name of a base route that already has a BC variant.
*
* @return bool
* TRUE if all parameters are set, FALSE if not.
*/
protected function setUpBaseRoute(string $base_route_name): bool {
$base_route = $this->collection->get($base_route_name);
$base_route_bc = $this->collection->get("$base_route_name.bc");
if (empty($base_route) || empty($base_route_bc)) {
return FALSE;
}
$this->basePath = $base_route->getPath();
$this->basePathBc = $base_route_bc->getPath();
$this->controller = $base_route_bc->getDefault('_controller');
if (empty($this->basePath) || empty($this->basePathBc) || empty($this->controller) || $this->basePathBc === $this->basePath) {
return FALSE;
}
return TRUE;
}
/**
* Adds a redirect route.
*
* @param string $route_name
* The name of a route whose path has changed.
*/
protected function addRedirectRoute(string $route_name): void {
// Exit early if the BC route is already there.
if (!empty($this->collection->get("$route_name.bc"))) {
return;
}
$route = $this->collection->get($route_name);
if (empty($route)) {
return;
}
$new_path = $route->getPath();
if (!str_starts_with($new_path, $this->basePath)) {
return;
}
$bc_route = clone $route;
// Set the path to what it was in earlier versions of Drupal.
$bc_route->setPath($this->basePathBc . substr($new_path, strlen($this->basePath)));
if ($bc_route->getPath() === $route->getPath()) {
return;
}
// Replace the handler with the stored redirect controller.
$defaults = array_diff_key($route->getDefaults(), array_flip([
'_entity_form',
'_entity_list',
'_entity_view',
'_form',
]));
$defaults['_controller'] = $this->controller;
$bc_route->setDefaults($defaults);
$this->collection->add("$route_name.bc", $bc_route);
}
/**
* Creates a list of routes that need BC redirects.
*
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
* The entity type.
*
* @return string[]
* A list of route names.
*/
protected function childRoutes(EntityTypeInterface $entity_type): array {
$route_names = [];
if ($field_ui_base_route = $entity_type->get('field_ui_base_route')) {
$updated_routes = new RouteCollection();
$updated_routes->add($field_ui_base_route, $this->collection->get($field_ui_base_route));
$event = new RouteBuildEvent($updated_routes);
// Apply route subscribers that add routes based on field_ui_base_route,
// in the order of their weights.
$subscribers = [
'field_ui' => 'field_ui.subscriber',
'content_translation' => 'content_translation.subscriber',
];
foreach ($subscribers as $module_name => $service_name) {
if ($this->moduleHandler->moduleExists($module_name)) {
\Drupal::service($service_name)->onAlterRoutes($event);
}
}
$updated_routes->remove($field_ui_base_route);
$route_names = array_merge($route_names, array_keys($updated_routes->all()));
$route_names = array_merge($route_names, [
// @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes()
"config_translation.item.add.{$field_ui_base_route}",
"config_translation.item.edit.{$field_ui_base_route}",
"config_translation.item.delete.{$field_ui_base_route}",
]);
}
if ($entity_type_id = $entity_type->getBundleEntityType()) {
$route_names = array_merge($route_names, [
// @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getRoutes()
"entity.{$entity_type_id}.delete_form",
// @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes()
"entity.{$entity_type_id}.config_translation_overview",
// @see \Drupal\user\Entity\EntityPermissionsRouteProvider::getRoutes()
"entity.{$entity_type_id}.entity_permissions_form",
]);
}
if ($entity_id = $entity_type->id()) {
$route_names = array_merge($route_names, [
// @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes()
"entity.field_config.config_translation_overview.{$entity_id}",
"config_translation.item.add.entity.field_config.{$entity_id}_field_edit_form",
"config_translation.item.edit.entity.field_config.{$entity_id}_field_edit_form",
"config_translation.item.delete.entity.field_config.{$entity_id}_field_edit_form",
// @see \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage::buildRoutes()
"layout_builder.defaults.{$entity_id}.disable",
"layout_builder.defaults.{$entity_id}.discard_changes",
"layout_builder.defaults.{$entity_id}.view",
]);
}
return $route_names;
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
$events = parent::getSubscribedEvents();
// Go after ContentTranslationRouteSubscriber.
$events[RoutingEvents::ALTER] = ['onAlterRoutes', -300];
return $events;
}
}