devel_wizard-2.x-dev/templates/spell/entity_type/content/controller.php.twig
templates/spell/entity_type/content/controller.php.twig
{%
include '@devel_wizard/php/devel_wizard.php.file.header.php.twig'
with {
'namespace': content.namespace,
}
%}
use Drupal\Core\Config\Entity\ConfigEntityInterface;
use {{ content.interface_fqn }};
{% if goal == 'bundleable' %}
use {{ config.interface_fqn }};
use {{ config.namespace }}\Comparer;
{% endif %}
use {{ content.namespace }}\StorageInterface as {{ content.idUpperCamel }}StorageInterface;
use Drupal\Component\Utility\Xss;
use Drupal\Core\Config\Entity\ConfigEntityType;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Link;
use Drupal\Core\Render\RendererInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
use Symfony\Component\DependencyInjection\ContainerInterface;
class Controller extends ControllerBase {
protected string $entityTypeId = '{{ content.id }}';
protected int $revisionHistoryItemPerPage = 50;
protected function getRevisionHistoryItemPerPage(): int {
return $this->revisionHistoryItemPerPage;
}
/**
* {@inheritdoc}
*
* @return static
*/
public static function create(ContainerInterface $container) {
// @phpstan-ignore-next-line
return new static(
$container->get('entity_type.manager'),
$container->get('date.formatter'),
$container->get('renderer'),
$container->get('entity.{{ content.id }}.permission_provider'),
$container->get('entity.repository'),
);
}
public function __construct(
EntityTypeManagerInterface $entityTypeManager,
protected DateFormatterInterface $dateFormatter,
protected RendererInterface $renderer,
protected PermissionProviderInterface $permissionProvider,
protected EntityRepositoryInterface $entityRepository,
) {
$this->entityTypeManager = $entityTypeManager;
}
/**
* Page callback.
*
* @return array{{ '<' }}string, mixed>|\Symfony\Component\HttpFoundation\Response
* Render array or Response object.
*/
public function addPageContent() {
$etm = $this->entityTypeManager();
/* @noinspection PhpUnhandledExceptionInspection */
$contentEntityType = $etm->getDefinition($this->entityTypeId);
$bundleEntityTypeId = $contentEntityType->getBundleEntityType();
if (!$bundleEntityTypeId) {
return $this->redirect("entity.{{ '{' }}$this->entityTypeId{{ '}' }}.add_form");
}
/* @noinspection PhpUnhandledExceptionInspection */
/** @var \Drupal\Core\Config\Entity\ConfigEntityType $bundleEntityType */
$bundleEntityType = $etm->getDefinition($bundleEntityTypeId);
$build = [
'#theme' => 'entity_add_list',
'#bundles' => [],
'#cache' => [
'tags' => $bundleEntityType->getListCacheTags(),
],
'#add_bundle_message' => $this->addPageBundleMessage($bundleEntityType),
];
$contentAch = $etm->getAccessControlHandler($this->entityTypeId);
/* @noinspection PhpUnhandledExceptionInspection */
/** @var \Drupal\Core\Config\Entity\ConfigEntityInterface[] $bundles */
$bundles = $etm
->getStorage($bundleEntityTypeId)
->loadMultiple();
foreach ($bundles as $bundle) {
$access = $contentAch->createAccess((string) $bundle->id(), NULL, [], TRUE);
if ($access->isAllowed()) {
$build['#bundles'][$bundle->id()] = $this->addPageBundleBuild($bundle);
}
$this->renderer->addCacheableDependency($build, $access);
}
if (count($build['#bundles']) === 1) {
$bundleMeta = reset($build['#bundles']);
$bundle = $bundleMeta['entity'];
$contentEntityTypeId = $bundle->getEntityType()->getBundleOf();
return $this->redirect(
"entity.$contentEntityTypeId.add_form",
[
$bundle->getEntityTypeId() => $bundle->id(),
],
);
}
return $build;
}
protected function addPageBundleMessage(ConfigEntityType $config): TranslatableMarkup {
$configAch = $this
->entityTypeManager()
->getAccessControlHandler($config->id());
/* @noinspection HtmlUnknownTarget */
return $configAch->createAccess() ?
$this->t(
'There are no @entityType.labelPlural. <a href=":entityType.url.add">Add new.</a>',
[
'@entityType.labelPlural' => $config->getPluralLabel(),
':entityType.url.add' => Url::fromRoute("entity.{$config->id()}.add_form")->toString(),
],
)
: $this->t(
'There are no @entityType.labelPlural.',
[
'@entityType.labelPlural' => $config->getPluralLabel(),
],
);
}
/**
* @return array{{ '<' }}string, mixed>
*/
protected function addPageBundleBuild(ConfigEntityInterface $bundle): array {
$contentEntityTypeId = $bundle->getEntityType()->getBundleOf();
return [
'entity' => $bundle,
'weight' => $bundle->get('weight'),
'label' => $bundle->label(),
'add_link' => Link::createFromRoute(
$bundle->label(),
"entity.$contentEntityTypeId.add_form",
[
$bundle->getEntityTypeId() => $bundle->id(),
],
),
'description' => $bundle->get('description'),
];
}
/**
* @return array{{ '<' }}string, mixed>
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function addForm(?EntityInterface $bundle): array {
$etm = $this->entityTypeManager();
/* @noinspection PhpUnhandledExceptionInspection */
$definition = $etm->getDefinition($this->entityTypeId);
$bundleEntityTypeId = $definition->getBundleEntityType();
assert(
($bundleEntityTypeId && $bundle) || (!$bundleEntityTypeId && !$bundle),
sprintf(
'Inconsistent entity type definition: %s; $bundleEntityTypeId=%s; $bundle->id()=%s',
$this->entityTypeId,
(string) ($bundle?->getEntityTypeId() ?: '-missing-'),
(string) ($bundle?->id() ?: '-missing-'),
),
);
assert(
!$bundleEntityTypeId && !$bundle,
sprintf(
'entity type "%s" is bundleable, but no "%s" instance is given as bundle',
$this->entityTypeId,
$bundleEntityTypeId,
),
);
// @phpstan-ignore-next-line
return $bundle && $bundleEntityTypeId
? $this->addFormBundleable($bundle)
: $this->addFormFlat();
}
/**
* @return array{{ '<' }}string, mixed>
*
* @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
* @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
*/
public function addFormFlat(): array {
$etm = $this->entityTypeManager();
/* @noinspection PhpUnhandledExceptionInspection */
$definition = $etm->getDefinition($this->entityTypeId);
$definition->getKey('bundle');
/* @noinspection PhpUnhandledExceptionInspection */
$entity = $etm
->getStorage($this->entityTypeId)
->create([
$definition->getKey('bundle') => $this->entityTypeId,
]);
return $this
->entityFormBuilder()
->getForm($entity);
}
/**
* Returns a form to add a new {{ content.id }}.
*
* @param \Drupal\Core\Entity\EntityInterface ${{ config.id }}
* The object which represent the bundle this content will be added to.
*
* @return array{{ '<' }}string, mixed>
*/
public function addFormBundleable(EntityInterface ${{ config.id }}): array {
$etm = $this->entityTypeManager();
$bundleEntityType = ${{ config.id }}->getEntityType();
/* @noinspection PhpUnhandledExceptionInspection */
$bundleOf = $etm->getDefinition($bundleEntityType->getBundleOf());
/* @noinspection PhpUnhandledExceptionInspection */
$contentEntity = $etm
->getStorage($bundleOf->id())
->create([
$bundleOf->getKey('bundle') => ${{ config.id }}->id(),
]);
return $this
->entityFormBuilder()
->getForm($contentEntity);
}
/**
* Route title callback.
*
* @return string|\Stringable|array{{ '<' }}string, mixed>
*/
public function entityLabel(EntityInterface ${{ content.id }}): array {
return [
'#markup' => ${{ content.id }}->label(),
'#allowed_tags' => Xss::getHtmlTagList(),
];
}
/**
* Route title callback.
*
* @return string|\Stringable|array{{ '<' }}string, mixed>
*/
public function revisionHistoryTitle({{ content.interface }} ${{ content.id }}) {
return [
'#markup' => $this->t(
'Revisions of %entity.label @entityType.label',
[
'%entity.label' => ${{ content.id }}->label(),
'@entityType.label' => ${{ content.id }}->getEntityType()->getLabel(),
]
),
'#allowed_tags' => Xss::getHtmlTagList(),
];
}
/**
* Route content callback.
*
* Generates an overview table of older revisions of a {{ content.label }}.
*
* @return string|int|array{{ '<' }}string, mixed>|\Psr\Http\Message\ResponseInterface
* An array as expected by \Drupal\Core\Render\RendererInterface::render().
*/
public function revisionHistoryContent({{ content.interface }} ${{ content.id }}) {
$account = $this->currentUser();
$langCode = ${{ content.id }}->language()->getId();
$languages = ${{ content.id }}->getTranslationLanguages();
$hasTranslations = (count($languages) > 1);
/* @noinspection PhpUnhandledExceptionInspection */
/** @var \{{ content.namespace }}\StorageInterface $storage */
$storage = $this->entityTypeManager()->getStorage(${{ content.id }}->getEntityTypeId());
$entityTypeId = ${{ content.id }}->getEntityTypeId();
$build = [
'revisions' => [
'#theme' => 'table',
'#attached' => [],
'#attributes' => [
'class' => 'revision-table',
],
'#header' => [
'label' => $this->t('label'),
'details' => $this->t('Details'),
'operations' => $this->t('Operations'),
],
'#rows' => [],
],
'pager' => [
'#type' => 'pager',
],
];
$access = [
'hasRevertPermission' => ${{ content.id }}->access('revision_revert', $account) && ${{ content.id }}->access('update'),
'hasDeletePermission' => ${{ content.id }}->access('revision_delete', $account) && ${{ content.id }}->access('delete'),
];
$defaultRevision = ${{ content.id }}->getRevisionId();
$isCurrentRevisionDisplayed = FALSE;
foreach ($this->getRevisionIds(${{ content.id }}, $storage) as $revisionId) {
$revision = $storage->loadRevision($revisionId);
if (!$revision->hasTranslation($langCode) || !$revision->getTranslation($langCode)->isRevisionTranslationAffected()) {
continue;
}
// We treat also the latest translation-affecting revision as current
// revision, if it was the default revision, as its values for the
// current language will be the same of the current default revision in
// this case.
$isCurrentRevision = $revisionId == $defaultRevision || (!$isCurrentRevisionDisplayed && $revision->wasDefaultRevision());
$linkText = $this->dateFormatter->format($revision->getRevisionCreationTime(), 'short');
$linkRouteName = "entity.$entityTypeId.canonical";
$linkParams = [
$entityTypeId => ${{ content.id }}->id(),
];
if ($isCurrentRevision) {
$isCurrentRevisionDisplayed = TRUE;
}
else {
$linkRouteName = "entity.$entityTypeId.revision_view";
$linkParams["{$entityTypeId}_revision"] = $revisionId;
}
$link = Link::fromTextAndUrl($linkText, new Url($linkRouteName, $linkParams));
$build['revisions']['#rows'][$revisionId] = [
'data' => [
'label' => [
'data' => $this->getRevisionLabelElement($revision),
],
'details' => [
'data' => $this->getRevisionSubmissionElement($revision, $link),
],
'operations' => [
'data' => $this->getRevisionOperationElement($isCurrentRevision, ${{ content.id }}, $revision, $access, $hasTranslations),
],
],
];
if ($isCurrentRevision) {
$build['revisions']['#rows'][$revisionId]['class'][] = 'revision-current';
}
}
return $build;
}
/**
* Route title callback.
*
* @param int|string ${{ content.id }}_revision
*
* @return string|\Stringable|array{{ '<' }}string, mixed>
*/
public function revisionViewTitle(${{ content.id }}_revision) {
$etm = $this->entityTypeManager();
/* @noinspection PhpUnhandledExceptionInspection */
/** @var \{{ content.namespace }}\StorageInterface $storage */
$storage = $etm->getStorage($this->entityTypeId);
$revision = $storage->loadRevision(${{ content.id }}_revision);
return [
'#markup' => $revision->label(),
'#allowed_tags' => Xss::getHtmlTagList(),
];
}
/**
* Route content callback.
*
* @param string|int ${{ content.id }}_revision
*
* @return string|int|array|\Psr\Http\Message\ResponseInterface
*/
public function revisionViewContent(${{ content.id }}_revision) {
$etm = $this->entityTypeManager();
/* @noinspection PhpUnhandledExceptionInspection */
/** @var \{{ content.namespace }}\StorageInterface $storage */
$storage = $etm->getStorage($this->entityTypeId);
$revision = $storage->loadRevision(${{ content.id }}_revision);
$this->entityRepository->getTranslationFromContext($revision);
/* @noinspection PhpUnhandledExceptionInspection */
/** @var \Drupal\Core\Entity\EntityViewBuilderInterface $viewController */
$viewController = $etm->getHandler($revision->getEntityTypeId(), 'view_controller');
// @todo Unset #cache.
$page = $viewController->view($revision);
return $page;
}
/**
* @return int[]
* Revision IDs (in descending order).
*/
protected function getRevisionIds({{ content.interface }} $entity, {{ content.class }}StorageInterface $storage): array {
$result = $storage
->getQuery()
->accessCheck()
->allRevisions()
->condition(
(string) $entity->getEntityType()->getKey('id'),
$entity->id(),
)
->sort(
(string) $entity->getEntityType()->getKey('revision'),
'DESC',
)
->pager($this->getRevisionHistoryItemPerPage())
->execute();
return array_keys($result);
}
/**
* @return array{{ '<' }}string, mixed>
*/
protected function getRevisionLabelElement({{ content.interface }} $revision): array {
{# @todo URL is wrong. -#}
return [
'#type' => 'link',
'#title' => $revision->label(),
'#url' => $revision->toUrl(),
'#attributes' => [
'target' => '_blank',
],
];
}
/**
* @return array{{ '<' }}string, mixed>
*/
protected function getRevisionSubmissionElement({{ content.interface }} $revision, Link $link): array {
$username = [
'#theme' => 'username',
'#account' => $revision->getRevisionUser(),
];
$element = [
'#type' => 'inline_template',
'#template' => $this->getRevisionSubmissionInlineTemplate(),
'#context' => [
'date' => $link->toString(),
'username' => $this->renderer->renderInIsolation($username),
'message' => [
'#markup' => $revision->getRevisionLogMessage(),
'#allowed_tags' => Xss::getHtmlTagList(),
],
],
];
// @todo Simplify once https://www.drupal.org/node/2334319 lands.
$this->renderer->addCacheableDependency($element, $username);
return $element;
}
protected function getRevisionSubmissionInlineTemplate(): string {
return '{% trans %}{{ date }} by {{ username }}{% endtrans %}{% if message %}<p class="revision-log">{{ message }}</p>{% endif %}';
}
/**
* @phpstan-param array{{ '<' }}string, bool> $access
*
* @return array{{ '<' }}string, mixed>
*/
protected function getRevisionOperationElement(
bool $isCurrentRevision,
{{ content.interface }} $current,
{{ content.interface }} $revision,
array $access,
bool $hasTranslations,
): array {
if ($isCurrentRevision) {
return [
'#prefix' => '<em>',
'#markup' => $this->t('Current revision'),
'#suffix' => '</em>',
];
}
return [
'#type' => 'operations',
'#links' => $this->getRevisionOperationLinks($current, $revision, $access, $hasTranslations),
];
}
/**
* @phpstan-param array{{ '<' }}string, bool> $access
*
* @return array{{ '<' }}string, array{{ '<' }}string, mixed>>
*/
protected function getRevisionOperationLinks(
{{ content.interface }} $current,
{{ content.interface }} $revision,
array $access,
bool $hasTranslations,
): array {
$entityTypeId = $current->getEntityTypeId();
$langCode = $current->language()->getId();
$links = [];
if ($access['hasRevertPermission']) {
$routeName = "entity.$entityTypeId.revision_revert" . ($hasTranslations ? '_translation' : '');
$routeParams = [
$entityTypeId => $current->id(),
"{$entityTypeId}_revision" => $revision->getRevisionId(),
];
if ($hasTranslations) {
$routeParams['langcode'] = $langCode;
}
$links['revert'] = [
'title' => $revision->getRevisionId() < $current->getRevisionId() ? $this->t('Revert') : $this->t('Set as current revision'),
'url' => Url::fromRoute($routeName, $routeParams),
];
}
if ($access['hasDeletePermission']) {
$links['delete'] = [
'title' => $this->t('Delete'),
'url' => Url::fromRoute(
"entity.$entityTypeId.revision_delete",
[
$entityTypeId => $current->id(),
"{$entityTypeId}_revision" => $revision->getRevisionId(),
]
),
];
}
return $links;
}
}
