first commit

This commit is contained in:
2024-07-15 12:33:27 +02:00
commit ce50ae282b
22084 changed files with 2623791 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Command;
{% apply sort_namespaces %}
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
{% if services %}
{{ di.use(services) }}
{% endif %}
{% endapply %}
// phpcs:disable Drupal.Commenting.ClassComment.Missing
#[AsCommand(
name: '{{ command.name }}',
description: '{{ command.description }}',
aliases: ['{{ command.alias }}'],
)]
final class {{ class }} extends Command {
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {
{# Parent constructor configures the command. #}
parent::__construct();
}
{% endif %}
/**
* {@inheritdoc}
*/
protected function execute(InputInterface $input, OutputInterface $output): int {
// @todo Place your code here.
$output->writeln('<info>It works!</info>');
return self::SUCCESS;
}
}

View File

@@ -0,0 +1,9 @@
{% import '@lib/di.twig' as di %}
services:
{{ service_name }}:
class: Drupal\{{ machine_name }}\Command\{{ class }}
{% if services %}
arguments: [{{ di.arguments(services) }}]
{% endif %}
tags:
- { name: console.command }

View File

@@ -0,0 +1,5 @@
entity.{{ entity_type_id }}.add_form:
route_name: 'entity.{{ entity_type_id }}.add_form'
title: 'Add {{ entity_type_label|lower }}'
appears_on:
- entity.{{ entity_type_id }}.collection

View File

@@ -0,0 +1,5 @@
entity.{{ entity_type_id }}.overview:
title: {{ entity_type_label|pluralize }}
parent: system.admin_structure
description: 'List of {{ entity_type_label|lower|pluralize }} to extend site functionality.'
route_name: entity.{{ entity_type_id }}.collection

View File

@@ -0,0 +1,2 @@
administer {{ entity_type_id }}:
title: 'Administer {{ entity_type_label|lower }}'

View File

@@ -0,0 +1,31 @@
entity.{{ entity_type_id }}.collection:
path: '/admin/structure/{{ entity_type_id|u2h }}'
defaults:
_entity_list: '{{ entity_type_id }}'
_title: '{{ entity_type_label }} configuration'
requirements:
_permission: 'administer {{ entity_type_id }}'
entity.{{ entity_type_id }}.add_form:
path: '/admin/structure/{{ entity_type_id }}/add'
defaults:
_entity_form: '{{ entity_type_id }}.add'
_title: 'Add {{ entity_type_label|article|lower }}'
requirements:
_permission: 'administer {{ entity_type_id }}'
entity.{{ entity_type_id }}.edit_form:
path: '/admin/structure/{{ entity_type_id|u2h }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}'
defaults:
_entity_form: '{{ entity_type_id }}.edit'
_title: 'Edit {{ entity_type_label|article|lower }}'
requirements:
_permission: 'administer {{ entity_type_id }}'
entity.{{ entity_type_id }}.delete_form:
path: '/admin/structure/{{ entity_type_id|u2h }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/delete'
defaults:
_entity_form: '{{ entity_type_id }}.delete'
_title: 'Delete {{ entity_type_label|article|lower }}'
requirements:
_permission: 'administer {{ entity_type_id }}'

View File

@@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Entity;
{% apply sort_namespaces %}
use Drupal\Core\Config\Entity\ConfigEntityBase;
use Drupal\{{ machine_name }}\{{ class_prefix }}Interface;
{% endapply %}
/**
* Defines the {{ entity_type_label|lower }} entity type.
*
* @ConfigEntityType(
* id = "{{ entity_type_id }}",
* label = @Translation("{{ entity_type_label }}"),
* label_collection = @Translation("{{ entity_type_label|pluralize }}"),
* label_singular = @Translation("{{ entity_type_label|lower }}"),
* label_plural = @Translation("{{ entity_type_label|pluralize|lower }}"),
* label_count = @PluralTranslation(
* singular = "@count {{ entity_type_label|lower }}",
* plural = "@count {{ entity_type_label|pluralize|lower }}",
* ),
* handlers = {
* "list_builder" = "Drupal\{{ machine_name }}\{{ class_prefix }}ListBuilder",
* "form" = {
* "add" = "Drupal\{{ machine_name }}\Form\{{ class_prefix }}Form",
* "edit" = "Drupal\{{ machine_name }}\Form\{{ class_prefix }}Form",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* },
* },
* config_prefix = "{{ entity_type_id }}",
* admin_permission = "administer {{ entity_type_id }}",
* links = {
* "collection" = "/admin/structure/{{ entity_type_id|u2h }}",
* "add-form" = "/admin/structure/{{ entity_type_id|u2h }}/add",
* "edit-form" = "/admin/structure/{{ entity_type_id|u2h }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}",
* "delete-form" = "/admin/structure/{{ entity_type_id|u2h }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/delete",
* },
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* },
* config_export = {
* "id",
* "label",
* "description",
* },
* )
*/
final class {{ class_prefix }} extends ConfigEntityBase implements {{ class_prefix }}Interface {
/**
* The example ID.
*/
protected string $id;
/**
* The example label.
*/
protected string $label;
/**
* The example description.
*/
protected string $description;
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
use Drupal\Core\Config\Entity\ConfigEntityInterface;
/**
* Provides an interface defining {{ entity_type_label|article|lower }} entity type.
*/
interface {{ class_prefix }}Interface extends ConfigEntityInterface {
}

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
/**
* Provides a listing of {{ entity_type_label|lower|pluralize }}.
*/
final class {{ class_prefix }}ListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader(): array {
$header['label'] = $this->t('Label');
$header['id'] = $this->t('Machine name');
$header['status'] = $this->t('Status');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity): array {
/** @var \Drupal\{{ machine_name }}\{{ class_prefix }}Interface $entity */
$row['label'] = $entity->label();
$row['id'] = $entity->id();
$row['status'] = $entity->status() ? $this->t('Enabled') : $this->t('Disabled');
return $row + parent::buildRow($entity);
}
}

View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
use Drupal\Core\Entity\EntityForm;
use Drupal\Core\Form\FormStateInterface;
use Drupal\{{ machine_name }}\Entity\{{ class_prefix }};
/**
* {{ entity_type_label }} form.
*/
final class {{ class_prefix }}Form extends EntityForm {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state): array {
$form = parent::form($form, $form_state);
$form['label'] = [
'#type' => 'textfield',
'#title' => $this->t('Label'),
'#maxlength' => 255,
'#default_value' => $this->entity->label(),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $this->entity->id(),
'#machine_name' => [
'exists' => [{{ class_prefix }}::class, 'load'],
],
'#disabled' => !$this->entity->isNew(),
];
$form['status'] = [
'#type' => 'checkbox',
'#title' => $this->t('Enabled'),
'#default_value' => $this->entity->status(),
];
$form['description'] = [
'#type' => 'textarea',
'#title' => $this->t('Description'),
'#default_value' => $this->entity->get('description'),
];
return $form;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state): int {
$result = parent::save($form, $form_state);
$message_args = ['%label' => $this->entity->label()];
$this->messenger()->addStatus(
match($result) {
\SAVED_NEW => $this->t('Created new example %label.', $message_args),
\SAVED_UPDATED => $this->t('Updated example %label.', $message_args),
}
);
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
return $result;
}
}

View File

@@ -0,0 +1,19 @@
{% if bundle %}
{{ entity_type_id }}.type_add:
title: 'Add {{ entity_type_label|lower }} type'
route_name: entity.{{ entity_type_id }}_type.add_form
appears_on:
- entity.{{ entity_type_id }}_type.collection
{{ entity_type_id }}.add_page:
title: 'Add {{ entity_type_label|lower }}'
route_name: entity.{{ entity_type_id }}.add_page
appears_on:
- entity.{{ entity_type_id }}.collection
{% else %}
{{ entity_type_id }}.add_form:
title: 'Add {{ entity_type_label|lower }}'
route_name: entity.{{ entity_type_id }}.add_form
appears_on:
- entity.{{ entity_type_id }}.collection
{% endif %}

View File

@@ -0,0 +1,10 @@
entity.{{ entity_type_id }}.edit_form:
route_name: entity.{{ entity_type_id }}.edit_form
group: {{ entity_type_id }}
title: 'Edit'
entity.{{ entity_type_id }}.delete_form:
route_name: entity.{{ entity_type_id }}.delete_form
group: {{ entity_type_id }}
title: 'Delete'
weight: 10

View File

@@ -0,0 +1,20 @@
{% if fieldable_no_bundle %}
entity.{{ entity_type_id }}.settings:
title: '{{ entity_type_label }}'
description: 'Configure {{ entity_type_label|article }} entity type.'
route_name: entity.{{ entity_type_id }}.settings
parent: system.admin_structure
{% elseif bundle %}
entity.{{ entity_type_id }}_type.collection:
title: '{{ entity_type_label }} types'
description: 'Manage and CRUD actions on {{ entity_type_label }} type.'
parent: system.admin_structure
route_name: entity.{{ entity_type_id }}_type.collection
{% endif %}
entity.{{ entity_type_id }}.collection:
title: '{{ entity_type_label|pluralize }}'
description: 'List of {{ entity_type_label|pluralize|lower }}.'
route_name: entity.{{ entity_type_id }}.collection
parent: system.admin_content

View File

@@ -0,0 +1,37 @@
{% if fieldable_no_bundle %}
entity.{{ entity_type_id }}.settings:
title: 'Settings'
route_name: entity.{{ entity_type_id }}.settings
base_route: entity.{{ entity_type_id }}.settings
{% endif %}
{# Tabs are not needed when there is no canonical view page. #}
{% if canonical %}
entity.{{ entity_type_id }}.view:
title: 'View'
route_name: entity.{{ entity_type_id }}.canonical
base_route: entity.{{ entity_type_id }}.canonical
entity.{{ entity_type_id }}.edit_form:
title: 'Edit'
route_name: entity.{{ entity_type_id }}.edit_form
base_route: entity.{{ entity_type_id }}.canonical
entity.{{ entity_type_id }}.delete_form:
title: 'Delete'
route_name: entity.{{ entity_type_id }}.delete_form
base_route: entity.{{ entity_type_id }}.canonical
weight: 10
{% endif %}
entity.{{ entity_type_id }}.collection:
title: '{{ entity_type_label|pluralize }}'
route_name: entity.{{ entity_type_id }}.collection
base_route: system.admin_content
weight: 10
{% if bundle %}
entity.{{ entity_type_id }}_type.edit_form:
title: 'Edit'
route_name: entity.{{ entity_type_id }}_type.edit_form
base_route: entity.{{ entity_type_id }}_type.edit_form
entity.{{ entity_type_id }}_type.collection:
title: 'List'
route_name: entity.{{ entity_type_id }}_type.collection
base_route: entity.{{ entity_type_id }}_type.collection
{% endif %}

View File

@@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
/**
* @file
* Provides {{ entity_type_label|article|lower }} entity type.
*/
{% apply sort_namespaces %}
use Drupal\Core\Render\Element;
{% if author_base_field %}
use Drupal\user\UserInterface;
{% endif %}
{% endapply %}
/**
* Implements hook_theme().
*/
function {{ machine_name }}_theme(): array {
return [
'{{ entity_type_id }}' => ['render element' => 'elements'],
];
}
/**
* Prepares variables for {{ entity_type_label|lower }} templates.
*
* Default template: {{ template_name }}.
*
* @param array $variables
* An associative array containing:
* - elements: An associative array containing the {{ entity_type_label|lower }} information and any
* fields attached to the entity.
* - attributes: HTML attributes for the containing element.
*/
function template_preprocess_{{ entity_type_id }}(array &$variables): void {
$variables['view_mode'] = $variables['elements']['#view_mode'];
foreach (Element::children($variables['elements']) as $key) {
$variables['content'][$key] = $variables['elements'][$key];
}
}
{% if author_base_field %}
/**
* Implements hook_user_cancel().
*/
function {{ machine_name }}_user_cancel($edit, UserInterface $account, $method): void {
switch ($method) {
{% if status_base_field %}
case 'user_cancel_block_unpublish':
// Unpublish {{ entity_type_label|lower|pluralize }}.
$storage = \Drupal::entityTypeManager()->getStorage('{{ entity_type_id }}');
${{ entity_type_id }}_ids = $storage->getQuery()
->condition('uid', $account->id())
->condition('status', 1)
->accessCheck(FALSE)
->execute();
foreach ($storage->loadMultiple(${{ entity_type_id }}_ids) as ${{ entity_type_id }}) {
${{ entity_type_id }}->set('status', FALSE)->save();
}
break;
{% endif %}
case 'user_cancel_reassign':
// Anonymize {{ entity_type_label|lower|pluralize }}.
$storage = \Drupal::entityTypeManager()->getStorage('{{ entity_type_id }}');
${{ entity_type_id }}_ids = $storage->getQuery()
->condition('uid', $account->id())
->accessCheck(FALSE)
->execute();
foreach ($storage->loadMultiple(${{ entity_type_id }}_ids) as ${{ entity_type_id }}) {
${{ entity_type_id }}->setOwnerId(0)->save();
}
break;
}
}
/**
* Implements hook_ENTITY_TYPE_predelete() for user entities.
*/
function {{ machine_name }}_user_predelete(UserInterface $account): void {
// Delete {{ entity_type_label|lower|pluralize }} that belong to this account.
$storage = \Drupal::entityTypeManager()->getStorage('{{ entity_type_id }}');
${{ entity_type_id }}_ids = $storage->getQuery()
->condition('uid', $account->id())
->accessCheck(FALSE)
->execute();
$storage->delete(
$storage->loadMultiple(${{ entity_type_id }}_ids)
);
{% if revisionable %}
// Delete old revisions.
${{ entity_type_id }}_ids = $storage->getQuery()
->allRevisions()
->condition('uid', $account->id())
->accessCheck(FALSE)
->execute();
foreach (array_keys(${{ entity_type_id }}_ids) as $revision_id) {
$storage->deleteRevision($revision_id);
}
{% endif %}
}
{% endif %}

View File

@@ -0,0 +1,26 @@
{{ permissions.administer }}:
{% if bundle %}
title: 'Administer {{ entity_type_label|lower }} types'
description: 'Maintain the types of {{ entity_type_id_short|replace({'_': ' '}) }} entity.'
{% else %}
title: 'Administer {{ entity_type_label|lower|pluralize }}'
{% endif %}
restrict access: true
{% if access_controller %}
{{ permissions.view }}:
title: 'View {{ entity_type_label|lower }}'
{{ permissions.edit }}:
title: 'Edit {{ entity_type_label|lower }}'
{{ permissions.delete }}:
title: 'Delete {{ entity_type_label|lower }}'
{{ permissions.create }}:
title: 'Create {{ entity_type_label|lower }}'
{% endif %}
{% if revisionable and access_controller %}
{{ permissions.view_revision }}:
title: 'View {{ entity_type_label|lower }} revision'
{{ permissions.revert_revision }}:
title: 'Revert {{ entity_type_label|lower }} revision'
{{ permissions.delete_revision }}:
title: 'Delete {{ entity_type_label|lower }} revision'
{% endif %}

View File

@@ -0,0 +1,9 @@
{% if fieldable_no_bundle %}
entity.{{ entity_type_id }}.settings:
path: 'admin/structure/{{ entity_type_id|u2h }}'
defaults:
_form: '\Drupal\{{ machine_name }}\Form\{{ class }}SettingsForm'
_title: '{{ entity_type_label }}'
requirements:
_permission: '{{ permissions.administer }}'
{% endif %}

View File

@@ -0,0 +1,316 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Entity;
{% apply sort_namespaces %}
{% if not revisionable %}
use Drupal\Core\Entity\ContentEntityBase;
{% endif %}
{% if author_base_field %}
use Drupal\Core\Entity\EntityStorageInterface;
{% endif %}
{% if has_base_fields %}
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
{% endif %}
{% if revisionable %}
use Drupal\Core\Entity\RevisionableContentEntityBase;
{% endif %}
use Drupal\{{ machine_name }}\{{ class }}Interface;
{% if author_base_field %}
use Drupal\user\EntityOwnerTrait;
{% endif %}
{% if changed_base_field %}
use Drupal\Core\Entity\EntityChangedTrait;
{% endif %}
{% endapply %}
/**
* Defines the {{ entity_type_label|lower }} entity class.
*
* @ContentEntityType(
* id = "{{ entity_type_id }}",
* label = @Translation("{{ entity_type_label }}"),
* label_collection = @Translation("{{ entity_type_label|pluralize }}"),
* label_singular = @Translation("{{ entity_type_label|lower }}"),
* label_plural = @Translation("{{ entity_type_label|pluralize|lower }}"),
* label_count = @PluralTranslation(
* singular = "@count {{ entity_type_label|pluralize|lower }}",
* plural = "@count {{ entity_type_label|pluralize|lower }}",
* ),
{% if bundle %}
* bundle_label = @Translation("{{ entity_type_label }} type"),
{% endif %}
* handlers = {
* "list_builder" = "Drupal\{{ machine_name }}\{{ class }}ListBuilder",
* "views_data" = "Drupal\views\EntityViewsData",
{% if access_controller %}
* "access" = "Drupal\{{ machine_name }}\{{ class }}AccessControlHandler",
{% endif %}
* "form" = {
* "add" = "Drupal\{{ machine_name }}\Form\{{ class }}Form",
* "edit" = "Drupal\{{ machine_name }}\Form\{{ class }}Form",
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
* "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm",
{% if revisionable %}
* "revision-delete" = \Drupal\Core\Entity\Form\RevisionDeleteForm::class,
* "revision-revert" = \Drupal\Core\Entity\Form\RevisionRevertForm::class,
{% endif %}
* },
* "route_provider" = {
{% if canonical %}
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
{% else %}
* "html" = "Drupal\{{ machine_name }}\Routing\{{ class }}HtmlRouteProvider",
{% endif %}
{% if revisionable %}
* "revision" = \Drupal\Core\Entity\Routing\RevisionHtmlRouteProvider::class,
{% endif %}
* },
* },
* base_table = "{{ entity_type_id }}",
{% if translatable %}
* data_table = "{{ entity_type_id }}_field_data",
{% endif %}
{% if revisionable %}
* revision_table = "{{ entity_type_id }}_revision",
{% endif %}
{% if revisionable and translatable %}
* revision_data_table = "{{ entity_type_id }}_field_revision",
{% endif %}
{% if revisionable %}
* show_revision_ui = TRUE,
{% endif %}
{% if translatable %}
* translatable = TRUE,
{% endif %}
* admin_permission = "{{ permissions.administer }}",
* entity_keys = {
* "id" = "id",
{% if revisionable %}
* "revision" = "revision_id",
{% endif %}
{% if translatable %}
* "langcode" = "langcode",
{% endif %}
{% if bundle %}
* "bundle" = "bundle",
{% endif %}
* "label" = "{{ label_base_field ? 'label' : 'id' }}",
{% if author_base_field %}
* "uuid" = "uuid",
* "owner" = "uid",
{% else %}
* "uuid" = "uuid",
{% endif %}
* },
{% if revisionable %}
* revision_metadata_keys = {
* "revision_user" = "revision_uid",
* "revision_created" = "revision_timestamp",
* "revision_log_message" = "revision_log",
* },
{% endif %}
* links = {
* "collection" = "/admin/content/{{ entity_type_id_short|u2h }}",
{% if bundle %}
* "add-form" = "{{ entity_base_path }}/add/{{ '{' }}{{ entity_type_id }}{{ '_type}' }}",
* "add-page" = "{{ entity_base_path }}/add",
{% else %}
* "add-form" = "{{ entity_base_path }}/add",
{% endif %}
* "canonical" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}",
{% if canonical %}
* "edit-form" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/edit",
{% else %}
* "edit-form" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}",
{% endif %}
* "delete-form" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/delete",
* "delete-multiple-form" = "/admin/content/{{ entity_type_id_short|u2h }}/delete-multiple",
{% if revisionable %}
* "revision" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/revision/{{ '{' }}{{ entity_type_id ~ '_revision' }}{{ '}' }}/view",
* "revision-delete-form" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/revision/{{ '{' }}{{ entity_type_id ~ '_revision' }}{{ '}' }}/delete",
* "revision-revert-form" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/revision/{{ '{' }}{{ entity_type_id ~ '_revision' }}{{ '}' }}/revert",
* "version-history" = "{{ entity_base_path }}/{{ '{' }}{{ entity_type_id }}{{ '}' }}/revisions",
{% endif %}
* },
{% if bundle %}
* bundle_entity_type = "{{ entity_type_id }}_type",
* field_ui_base_route = "entity.{{ entity_type_id }}_type.edit_form",
{% elseif fieldable %}
* field_ui_base_route = "entity.{{ entity_type_id }}.settings",
{% endif %}
* )
*/
final class {{ class }} extends {% if revisionable %}Revisionable{% endif %}ContentEntityBase implements {{ class }}Interface {
{% if changed_base_field or author_base_field %}
{% if changed_base_field %}
use EntityChangedTrait;
{% endif %}
{# use EntityCreatedTrait once it is added to Drupal core #}
{# @see https://www.drupal.org/node/2833378 #}
{% if author_base_field %}
use EntityOwnerTrait;
{% endif %}
{% endif %}
{% if author_base_field %}
/**
* {@inheritdoc}
*/
public function preSave(EntityStorageInterface $storage): void {
parent::preSave($storage);
if (!$this->getOwnerId()) {
// If no owner has been set explicitly, make the anonymous user the owner.
$this->setOwnerId(0);
}
}
{% endif %}
{% if has_base_fields %}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
$fields = parent::baseFieldDefinitions($entity_type);
{% if label_base_field %}
$fields['label'] = BaseFieldDefinition::create('string')
{% if revisionable %}
->setRevisionable(TRUE)
{% endif %}
{% if translatable %}
->setTranslatable(TRUE)
{% endif %}
->setLabel(t('Label'))
->setRequired(TRUE)
->setSetting('max_length', 255)
->setDisplayOptions('form', [
'type' => 'string_textfield',
'weight' => -5,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('view', [
'label' => 'hidden',
'type' => 'string',
'weight' => -5,
])
->setDisplayConfigurable('view', TRUE);
{% endif %}
{% if status_base_field %}
$fields['status'] = BaseFieldDefinition::create('boolean')
{% if revisionable %}
->setRevisionable(TRUE)
{% endif %}
->setLabel(t('Status'))
->setDefaultValue(TRUE)
->setSetting('on_label', 'Enabled')
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'settings' => [
'display_label' => FALSE,
],
'weight' => 0,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('view', [
'type' => 'boolean',
'label' => 'above',
'weight' => 0,
'settings' => [
'format' => 'enabled-disabled',
],
])
->setDisplayConfigurable('view', TRUE);
{% endif %}
{% if description_base_field %}
$fields['description'] = BaseFieldDefinition::create('text_long')
{% if revisionable %}
->setRevisionable(TRUE)
{% endif %}
{% if translatable %}
->setTranslatable(TRUE)
{% endif %}
->setLabel(t('Description'))
->setDisplayOptions('form', [
'type' => 'text_textarea',
'weight' => 10,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('view', [
'type' => 'text_default',
'label' => 'above',
'weight' => 10,
])
->setDisplayConfigurable('view', TRUE);
{% endif %}
{% if author_base_field %}
$fields['uid'] = BaseFieldDefinition::create('entity_reference')
{% if revisionable %}
->setRevisionable(TRUE)
{% endif %}
{% if translatable %}
->setTranslatable(TRUE)
{% endif %}
->setLabel(t('Author'))
->setSetting('target_type', 'user')
->setDefaultValueCallback(self::class . '::getDefaultEntityOwner')
->setDisplayOptions('form', [
'type' => 'entity_reference_autocomplete',
'settings' => [
'match_operator' => 'CONTAINS',
'size' => 60,
'placeholder' => '',
],
'weight' => 15,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'author',
'weight' => 15,
])
->setDisplayConfigurable('view', TRUE);
{% endif %}
{% if created_base_field %}
$fields['created'] = BaseFieldDefinition::create('created')
->setLabel(t('Authored on'))
{% if translatable %}
->setTranslatable(TRUE)
{% endif %}
->setDescription(t('The time that the {{ entity_type_label|lower }} was created.'))
->setDisplayOptions('view', [
'label' => 'above',
'type' => 'timestamp',
'weight' => 20,
])
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'datetime_timestamp',
'weight' => 20,
])
->setDisplayConfigurable('view', TRUE);
{% endif %}
{% if changed_base_field %}
$fields['changed'] = BaseFieldDefinition::create('changed')
->setLabel(t('Changed'))
{% if translatable %}
->setTranslatable(TRUE)
{% endif %}
->setDescription(t('The time that the {{ entity_type_label|lower }} was last edited.'));
{% endif %}
return $fields;
}
{% endif %}
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Entity;
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
/**
* Defines the {{ entity_type_label }} type configuration entity.
*
* @ConfigEntityType(
* id = "{{ entity_type_id }}_type",
* label = @Translation("{{ entity_type_label }} type"),
* label_collection = @Translation("{{ entity_type_label }} types"),
* label_singular = @Translation("{{ entity_type_label|lower }} type"),
* label_plural = @Translation("{{ entity_type_label|pluralize|lower }} types"),
* label_count = @PluralTranslation(
* singular = "@count {{ entity_type_label|pluralize|lower }} type",
* plural = "@count {{ entity_type_label|pluralize|lower }} types",
* ),
* handlers = {
* "form" = {
* "add" = "Drupal\{{ machine_name }}\Form\{{ class }}TypeForm",
* "edit" = "Drupal\{{ machine_name }}\Form\{{ class }}TypeForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* },
* "list_builder" = "Drupal\{{ machine_name }}\{{ class }}TypeListBuilder",
* "route_provider" = {
* "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
* },
* },
* admin_permission = "{{ permissions.administer }}",
* bundle_of = "{{ entity_type_id }}",
* config_prefix = "{{ entity_type_id }}_type",
* entity_keys = {
* "id" = "id",
* "label" = "label",
* "uuid" = "uuid",
* },
* links = {
* "add-form" = "/admin/structure/{{ entity_type_id }}_types/add",
* "edit-form" = "/admin/structure/{{ entity_type_id }}_types/manage/{{ '{' ~ entity_type_id ~ '_type}' }}",
* "delete-form" = "/admin/structure/{{ entity_type_id }}_types/manage/{{ '{' ~ entity_type_id ~ '_type}' }}/delete",
* "collection" = "/admin/structure/{{ entity_type_id }}_types",
* },
* config_export = {
* "id",
* "label",
* "uuid",
* },
* )
*/
final class {{ class }}Type extends ConfigEntityBundleBase {
/**
* The machine name of this {{ entity_type_label|lower }} type.
*/
protected string $id;
/**
* The human-readable name of the {{ entity_type_label|lower }} type.
*/
protected string $label;
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
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 {{ entity_type_label|lower }} entity type.
*
* phpcs:disable Drupal.Arrays.Array.LongLineDeclaration
*
* @see https://www.drupal.org/project/coder/issues/3185082
*/
final class {{ class }}AccessControlHandler extends EntityAccessControlHandler {
/**
* {@inheritdoc}
*/
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account): AccessResult {
if ($account->hasPermission($this->entityType->getAdminPermission())) {
return AccessResult::allowed()->cachePerPermissions();
}
return match($operation) {
'view' => AccessResult::allowedIfHasPermission($account, '{{ permissions.view }}'),
'update' => AccessResult::allowedIfHasPermission($account, '{{ permissions.edit }}'),
'delete' => AccessResult::allowedIfHasPermission($account, '{{ permissions.delete }}'),
{% if revisionable %}
'delete revision' => AccessResult::allowedIfHasPermission($account, '{{ permissions.delete_revision }}'),
'view all revisions', 'view revision' => AccessResult::allowedIfHasPermissions($account, ['{{ permissions.view_revision }}', '{{ permissions.view }}']),
'revert' => AccessResult::allowedIfHasPermissions($account, ['{{ permissions.revert_revision }}', '{{ permissions.edit }}']),
{% endif %}
default => AccessResult::neutral(),
};
}
/**
* {@inheritdoc}
*/
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL): AccessResult {
return AccessResult::allowedIfHasPermissions($account, ['{{ permissions.create }}', '{{ permissions.administer }}'], 'OR');
}
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
use Drupal\Core\Entity\ContentEntityInterface;
{% if changed_base_field %}
use Drupal\Core\Entity\EntityChangedInterface;
{% endif %}
{% if author_base_field %}
use Drupal\user\EntityOwnerInterface;
{% endif %}
/**
* Provides an interface defining {{ entity_type_label|article|lower }} entity type.
*/
interface {{ class }}Interface extends ContentEntityInterface{% if author_base_field %}, EntityOwnerInterface{% endif %}{% if changed_base_field %}, EntityChangedInterface{% endif %} {
}

View File

@@ -0,0 +1,66 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityListBuilder;
/**
* Provides a list controller for the {{ entity_type_label|lower }} entity type.
*/
final class {{ class }}ListBuilder extends EntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader(): array {
$header['id'] = $this->t('ID');
{% if label_base_field %}
$header['label'] = $this->t('Label');
{% endif %}
{% if status_base_field %}
$header['status'] = $this->t('Status');
{% endif %}
{% if author_base_field %}
$header['uid'] = $this->t('Author');
{% endif %}
{% if created_base_field %}
$header['created'] = $this->t('Created');
{% endif %}
{% if changed_base_field %}
$header['changed'] = $this->t('Updated');
{% endif %}
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity): array {
/** @var \Drupal\{{ machine_name }}\{{ class }}Interface $entity */
$row['id'] = $entity->{{ label_base_field or not canonical ? 'id' : 'toLink' }}();
{% if label_base_field %}
$row['label'] = $entity->{{ canonical ? 'toLink' : 'label' }}();
{% endif %}
{% if status_base_field %}
$row['status'] = $entity->get('status')->value ? $this->t('Enabled') : $this->t('Disabled');
{% endif %}
{% if author_base_field %}
$username_options = [
'label' => 'hidden',
'settings' => ['link' => $entity->get('uid')->entity->isAuthenticated()],
];
$row['uid']['data'] = $entity->get('uid')->view($username_options);
{% endif %}
{% if created_base_field %}
$row['created']['data'] = $entity->get('created')->view(['label' => 'hidden']);
{% endif %}
{% if changed_base_field %}
$row['changed']['data'] = $entity->get('changed')->view(['label' => 'hidden']);
{% endif %}
return $row + parent::buildRow($entity);
}
}

View File

@@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
/**
* Defines a class to build a listing of {{ entity_type_label|lower }} type entities.
*
* @see \Drupal\{{ machine_name }}\Entity\{{ class }}Type
*/
final class {{ class }}TypeListBuilder extends ConfigEntityListBuilder {
/**
* {@inheritdoc}
*/
public function buildHeader(): array {
$header['label'] = $this->t('Label');
return $header + parent::buildHeader();
}
/**
* {@inheritdoc}
*/
public function buildRow(EntityInterface $entity): array {
$row['label'] = $entity->label();
return $row + parent::buildRow($entity);
}
/**
* {@inheritdoc}
*/
public function render(): array {
$build = parent::render();
$build['table']['#empty'] = $this->t(
'No {{ entity_type_label|lower }} types available. <a href=":link">Add {{ entity_type_label|lower }} type</a>.',
[':link' => Url::fromRoute('entity.{{ entity_type_id }}_type.add_form')->toString()],
);
return $build;
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
use Drupal\Core\Entity\ContentEntityForm;
use Drupal\Core\Form\FormStateInterface;
/**
* Form controller for the {{ entity_type_label|lower }} entity edit forms.
*/
final class {{ class }}Form extends ContentEntityForm {
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state): int {
$result = parent::save($form, $form_state);
$message_args = ['%label' => $this->entity->toLink()->toString()];
$logger_args = [
'%label' => $this->entity->label(),
'link' => $this->entity->toLink($this->t('View'))->toString(),
];
switch ($result) {
case SAVED_NEW:
$this->messenger()->addStatus($this->t('New {{ entity_type_label|lower }} %label has been created.', $message_args));
$this->logger('{{ machine_name }}')->notice('New {{ entity_type_label|lower }} %label has been created.', $logger_args);
break;
case SAVED_UPDATED:
$this->messenger()->addStatus($this->t('The {{ entity_type_label|lower }} %label has been updated.', $message_args));
$this->logger('{{ machine_name }}')->notice('The {{ entity_type_label|lower }} %label has been updated.', $logger_args);
break;
default:
throw new \LogicException('Could not save the entity.');
}
{% if canonical %}
$form_state->setRedirectUrl($this->entity->toUrl());
{% else %}
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
{% endif %}
return $result;
}
}

View File

@@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configuration form for {{ entity_type_label|article|lower }} entity type.
*/
final class {{ class }}SettingsForm extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return '{{ entity_type_id }}_settings';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['settings'] = [
'#markup' => $this->t('Settings form for {{ entity_type_label|article|lower }} entity type.'),
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Save'),
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->messenger()->addStatus($this->t('The configuration has been updated.'));
}
}

View File

@@ -0,0 +1,79 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
{% apply sort_namespaces %}
use Drupal\Core\Entity\BundleEntityFormBase;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\{{ machine_name }}\Entity\{{ class }}Type;
{% endapply %}
/**
* Form handler for {{ entity_type_label|lower }} type forms.
*/
final class {{ class }}TypeForm extends BundleEntityFormBase {
/**
* {@inheritdoc}
*/
public function form(array $form, FormStateInterface $form_state): array {
$form = parent::form($form, $form_state);
if ($this->operation === 'edit') {
$form['#title'] = $this->t('Edit %label {{ entity_type_label|lower }} type', ['%label' => $this->entity->label()]);
}
$form['label'] = [
'#title' => $this->t('Label'),
'#type' => 'textfield',
'#default_value' => $this->entity->label(),
'#description' => $this->t('The human-readable name of this {{ entity_type_label|lower }} type.'),
'#required' => TRUE,
];
$form['id'] = [
'#type' => 'machine_name',
'#default_value' => $this->entity->id(),
'#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH,
'#machine_name' => [
'exists' => [{{ class }}Type::class, 'load'],
'source' => ['label'],
],
'#description' => $this->t('A unique machine-readable name for this {{ entity_type_label|lower }} type. It must only contain lowercase letters, numbers, and underscores.'),
];
return $this->protectBundleIdElement($form);
}
/**
* {@inheritdoc}
*/
protected function actions(array $form, FormStateInterface $form_state): array {
$actions = parent::actions($form, $form_state);
$actions['submit']['#value'] = $this->t('Save {{ entity_type_label|lower }} type');
$actions['delete']['#value'] = $this->t('Delete {{ entity_type_label|lower }} type');
return $actions;
}
/**
* {@inheritdoc}
*/
public function save(array $form, FormStateInterface $form_state): int {
$result = parent::save($form, $form_state);
$message_args = ['%label' => $this->entity->label()];
$this->messenger()->addStatus(
match($result) {
SAVED_NEW => $this->t('The {{ entity_type_label|lower }} type %label has been added.', $message_args),
SAVED_UPDATED => $this->t('The {{ entity_type_label|lower }} type %label has been updated.', $message_args),
}
);
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
return $result;
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Routing;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
use Symfony\Component\Routing\Route;
/**
* Provides HTML routes for entities with administrative pages.
*/
final class {{ class }}HtmlRouteProvider extends AdminHtmlRouteProvider {
/**
* {@inheritdoc}
*/
protected function getCanonicalRoute(EntityTypeInterface $entity_type): ?Route {
return $this->getEditFormRoute($entity_type);
}
}

View File

@@ -0,0 +1,24 @@
{{ '{#' }}
/**
* @file
* Default theme implementation to present {{ entity_type_label|article|lower }} entity.
*
* This template is used when viewing a canonical {{ entity_type_label|lower }} page,
*
* Available variables:
* - content: A list of content items. Use 'content' to print all content, or
* print a subset such as 'content.label'.
* - attributes: HTML attributes for the container element.
*
* @see template_preprocess_{{ entity_type_id }}()
*/
{{ '#}' }}{% verbatim %}
<article{{ attributes }}>
{% if view_mode != 'full' %}
{{ title_prefix }}
{{ title_suffix }}
{% endif %}
{% if content %}
{{- content -}}
{% endif %}
</article>{% endverbatim %}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace {{ namespace }};
use {{ entity_class_fqn }};
/**
* A base bundle class for {{ entity_type_id }} entities.
*/
abstract class {{ base_class }} extends {{ entity_class }} {
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace {{ namespace }};
{% if not base_class %}
use {{ entity_class_fqn }};
{% endif %}
/**
* A bundle class for {{ entity_type_id }} entities.
*/
final class {{ class }} extends {{ base_class ?: entity_class }} {
}

View File

@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* @file
* Primary module hooks for {{ name }} module.
*/
/**
* Implements hook_entity_bundle_info_alter().
*/
function {{ machine_name }}_entity_bundle_info_alter(array &$bundles): void {
{% for bundle_id, class_fqn in classes_fqn %}
if (isset($bundles['{{ entity_type_id }}']['{{ bundle_id }}'])) {
// phpcs:ignore Drupal.Classes.FullyQualifiedNamespace.UseStatementMissing
$bundles['{{ entity_type_id }}']['{{ bundle_id }}']['class'] = {{ class_fqn }}::class;
}
{% endfor %}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
use Drupal\Core\Form\ConfigFormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Configure {{ name }} settings for this site.
*/
final class {{ class }} extends ConfigFormBase {
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return '{{ form_id }}';
}
/**
* {@inheritdoc}
*/
protected function getEditableConfigNames(): array {
return ['{{ machine_name }}.settings'];
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['example'] = [
'#type' => 'textfield',
'#title' => $this->t('Example'),
'#default_value' => $this->config('{{ machine_name }}.settings')->get('example'),
];
return parent::buildForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
// @todo Validate the form here.
// Example:
// @code
// if ($form_state->getValue('example') === 'wrong') {
// $form_state->setErrorByName(
// 'message',
// $this->t('The value is not correct.'),
// );
// }
// @endcode
parent::validateForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->config('{{ machine_name }}.settings')
->set('example', $form_state->getValue('example'))
->save();
parent::submitForm($form, $form_state);
}
}

View File

@@ -0,0 +1,10 @@
{{ route_name }}:
title: {{ link_title }}
{% if link_description %}
description: {{ link_description }}
{% endif %}
{% if link_parent %}
parent: {{ link_parent }}
{% endif %}
route_name: {{ route_name }}
weight: 10

View File

@@ -0,0 +1,7 @@
{{ route_name }}:
path: '{{ route_path }}'
defaults:
_title: '{{ route_title }}'
_form: 'Drupal\{{ machine_name }}\Form\{{ class }}'
requirements:
_permission: '{{ route_permission }}'

View File

@@ -0,0 +1,8 @@
# Schema for the configuration files of the {{ name }} module.
{{ machine_name }}.settings:
type: config_object
label: '{{ name }} settings'
mapping:
example:
type: string
label: 'Example'

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
use Drupal\Core\Form\ConfirmFormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Url;
/**
* @todo Add a description for the form.
*/
final class {{ class }} extends ConfirmFormBase {
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return '{{ form_id }}';
}
/**
* {@inheritdoc}
*/
public function getQuestion(): TranslatableMarkup {
return $this->t('Are you sure you want to do this?');
}
/**
* {@inheritdoc}
*/
public function getCancelUrl(): Url {
return new Url('system.admin_config');
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
// @todo Place your code here.
$this->messenger()->addStatus($this->t('Done!'));
$form_state->setRedirectUrl(new Url('system.admin_config'));
}
}

View File

@@ -0,0 +1,7 @@
{{ route_name }}:
path: '{{ route_path }}'
defaults:
_title: '{{ route_title }}'
_form: 'Drupal\{{ machine_name }}\Form\{{ class }}'
requirements:
_permission: '{{ route_permission }}'

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Form;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
/**
* Provides a {{ name }} form.
*/
final class {{ class }} extends FormBase {
/**
* {@inheritdoc}
*/
public function getFormId(): string {
return '{{ form_id }}';
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state): array {
$form['message'] = [
'#type' => 'textarea',
'#title' => $this->t('Message'),
'#required' => TRUE,
];
$form['actions'] = [
'#type' => 'actions',
'submit' => [
'#type' => 'submit',
'#value' => $this->t('Send'),
],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function validateForm(array &$form, FormStateInterface $form_state): void {
// @todo Validate the form here.
// Example:
// @code
// if (mb_strlen($form_state->getValue('message')) < 10) {
// $form_state->setErrorByName(
// 'message',
// $this->t('Message should be at least 10 characters.'),
// );
// }
// @endcode
}
/**
* {@inheritdoc}
*/
public function submitForm(array &$form, FormStateInterface $form_state): void {
$this->messenger()->addStatus($this->t('The message has been sent.'));
$form_state->setRedirect('<front>');
}
}

View File

@@ -0,0 +1,7 @@
{{ route_name }}:
path: '{{ route_path }}'
defaults:
_title: '{{ route_title }}'
_form: 'Drupal\{{ machine_name }}\Form\{{ class }}'
requirements:
_permission: '{{ route_permission }}'

View File

@@ -0,0 +1,23 @@
<IfModule mod_ssl.c>
<VirtualHost *:443>
ServerName {{ hostname }}
ServerAlias www.{{ hostname }}
DocumentRoot {{ docroot }}
ErrorLog ${APACHE_LOG_DIR}/error.log
CustomLog ${APACHE_LOG_DIR}/access.log combined
<Directory {{ docroot }}>
Options Indexes FollowSymLinks MultiViews
AllowOverride All
Order allow,deny
allow from all
</Directory>
SSLEngine on
SSLCertificateFile /etc/ssl/certs/localhost.crt
SSLCertificateKeyFile /etc/ssl/private/localhost.key
</VirtualHost>
</IfModule>

View File

@@ -0,0 +1,13 @@
<VirtualHost *:80>
ServerName {{ hostname }}
ServerAlias www.{{ hostname }}
DocumentRoot {{ docroot }}
<Directory {{ docroot }}>
Options All
AllowOverride All
Order allow,deny
Allow from all
</Directory>
</VirtualHost>

View File

@@ -0,0 +1,102 @@
#
# @DCG
# The configuration is based on official Nginx recipe.
# See https://www.nginx.com/resources/wiki/start/topics/recipes/drupal/
# Check out Perusio's config for more delicate configuration.
# See https://github.com/perusio/drupal-with-nginx
#
server {
server_name {{ hostname }};
root {{ docroot }};
client_max_body_size 16m;
location = /favicon.ico {
log_not_found off;
access_log off;
}
location = /robots.txt {
allow all;
log_not_found off;
access_log off;
}
# Very rarely should these ever be accessed.
location ~* \.(make|txt|log|engine|inc|info|install|module|profile|po|pot|sh|sql|test|theme)$ {
return 404;
}
location ~ \..*/.*\.php$ {
return 404;
}
{% if file_private_path %}
location ~ ^/{{ file_private_path }}/ {
return 403;
}
{% endif %}
# Allow "Well-Known URIs" as per RFC 5785.
location ~* ^/.well-known/ {
allow all;
}
# Block access to "hidden" files and directories whose names begin with a
# period. This includes directories used by version control systems such
# as Subversion or Git to store control files.
location ~ (^|/)\. {
return 404;
}
location / {
try_files $uri /index.php?$query_string;
}
location @rewrite {
rewrite ^/(.*)$ /index.php?q=$1;
}
# Don't allow direct access to PHP files in the vendor directory.
location ~ /vendor/.*\.php$ {
deny all;
return 404;
}
# Since Drupal 8, we must also match new paths where the '.php' appears in
# the middle, such as update.php/selection. The rule we use is strict,
# and only allows this pattern with the update.php front controller.
# This allows legacy path aliases in the form of
# blog/index.php/legacy-path to continue to route to Drupal nodes. If
# you do not have any paths like that, then you might prefer to use a
# laxer rule, such as:
# location ~ \.php(/|$) {
# The laxer rule will continue to work if Drupal uses this new URL
# pattern with front controllers other than update.php in a future
# release.
location ~ '\.php$|^/update.php' {
fastcgi_split_path_info ^(.+?\.php)(|/.*)$;
include fastcgi_params;
# Block httpoxy attacks. See https://httpoxy.org/.
fastcgi_param HTTP_PROXY "";
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_intercept_errors on;
fastcgi_pass {{ fastcgi_pass }};
}
# Fighting with Styles? This little gem is amazing.
location ~ ^/{{ file_public_path }}/styles/ {
try_files $uri @rewrite;
}
# Handle private files through Drupal.
location ~ ^/system/files/ {
try_files $uri /index.php?$query_string;
}
location ~* \.(js|css|png|jpg|jpeg|gif|ico)$ {
expires max;
log_not_found off;
}
}

View File

@@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Field\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
/**
* Plugin implementation of the '{{ plugin_label }}' formatter.
*
* @FieldFormatter(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* field_types = {"string"},
* )
*/
final class {{ class }} extends FormatterBase {
{% if configurable %}
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
$setting = ['foo' => 'bar'];
return $setting + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$elements['foo'] = [
'#type' => 'textfield',
'#title' => $this->t('Foo'),
'#default_value' => $this->getSetting('foo'),
];
return $elements;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
return [
$this->t('Foo: @foo', ['@foo' => $this->getSetting('foo')]),
];
}
{% endif %}
/**
* {@inheritdoc}
*/
public function viewElements(FieldItemListInterface $items, $langcode): array {
$element = [];
foreach ($items as $delta => $item) {
$element[$delta] = [
'#markup' => $item->value,
];
}
return $element;
}
}

View File

@@ -0,0 +1,7 @@
field.formatter.settings.{{ plugin_id }}:
type: mapping
label: {{ plugin_label }} formatter settings
mapping:
foo:
type: string
label: Foo

View File

@@ -0,0 +1,27 @@
{% if configurable_storage %}
field.storage_settings.{{ plugin_id }}:
type: mapping
label: {{ plugin_label }} storage settings
mapping:
foo:
type: string
label: Foo
{% endif %}
{% if configurable_instance %}
field.field_settings.{{ plugin_id }}:
type: mapping
label: {{ plugin_label }} field settings
mapping:
bar:
type: string
label: Bar
{% endif %}
field.value.{{ plugin_id }}:
type: mapping
label: Default value
mapping:
value:
type: label
label: Value

View File

@@ -0,0 +1,148 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Field\FieldType;
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\Core\Field\FieldItemBase;
use Drupal\Core\Field\FieldStorageDefinitionInterface;
{% if configurable_storage or configurable_instance %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
use Drupal\Core\TypedData\DataDefinition;
/**
* Defines the '{{ plugin_id }}' field type.
*
* @FieldType(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* description = @Translation("Some description."),
* default_widget = "string_textfield",
* default_formatter = "string",
* )
*/
final class {{ class }} extends FieldItemBase {
{% if configurable_storage %}
/**
* {@inheritdoc}
*/
public static function defaultStorageSettings(): array {
$settings = ['foo' => ''];
return $settings + parent::defaultStorageSettings();
}
/**
* {@inheritdoc}
*/
public function storageSettingsForm(array &$form, FormStateInterface $form_state, $has_data): array {
$element['foo'] = [
'#type' => 'textfield',
'#title' => $this->t('Foo'),
'#default_value' => $this->getSetting('foo'),
'#disabled' => $has_data,
];
return $element;
}
{% endif %}
{% if configurable_instance %}
/**
* {@inheritdoc}
*/
public static function defaultFieldSettings(): array {
$settings = ['bar' => ''];
return $settings + parent::defaultFieldSettings();
}
/**
* {@inheritdoc}
*/
public function fieldSettingsForm(array $form, FormStateInterface $form_state): array {
$element['bar'] = [
'#type' => 'textfield',
'#title' => $this->t('Bar'),
'#default_value' => $this->getSetting('bar'),
];
return $element;
}
{% endif %}
/**
* {@inheritdoc}
*/
public function isEmpty(): bool {
return match ($this->get('value')->getValue()) {
NULL, '' => TRUE,
default => FALSE,
};
}
/**
* {@inheritdoc}
*/
public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition): array {
// @DCG
// See /core/lib/Drupal/Core/TypedData/Plugin/DataType directory for
// available data types.
$properties['value'] = DataDefinition::create('string')
->setLabel(t('Text value'))
->setRequired(TRUE);
return $properties;
}
/**
* {@inheritdoc}
*/
public function getConstraints(): array {
$constraints = parent::getConstraints();
$constraint_manager = $this->getTypedDataManager()->getValidationConstraintManager();
// @DCG Suppose our value must not be longer than 10 characters.
$options['value']['Length']['max'] = 10;
// @DCG
// See /core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint
// directory for available constraints.
$constraints[] = $constraint_manager->create('ComplexData', $options);
return $constraints;
}
/**
* {@inheritdoc}
*/
public static function schema(FieldStorageDefinitionInterface $field_definition): array {
$columns = [
'value' => [
'type' => 'varchar',
'not null' => FALSE,
'description' => 'Column description.',
'length' => 255,
],
];
$schema = [
'columns' => $columns,
// @todo Add indexes here if necessary.
];
return $schema;
}
/**
* {@inheritdoc}
*/
public static function generateSampleValue(FieldDefinitionInterface $field_definition): array {
$random = new Random();
$values['value'] = $random->word(mt_rand(1, 50));
return $values;
}
}

View File

@@ -0,0 +1,7 @@
field.widget.settings.{{ plugin_id }}:
type: mapping
label: {{ plugin_label }} widget settings
mapping:
foo:
type: string
label: Foo

View File

@@ -0,0 +1,98 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Field\FieldWidget;
{% apply sort_namespaces %}
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Defines the '{{ plugin_id }}' field widget.
*
* @FieldWidget(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* field_types = {"string"},
* )
*/
final class {{ class }} extends WidgetBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs the plugin instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
{% if configurable %}
/**
* {@inheritdoc}
*/
public static function defaultSettings(): array {
$setting = ['foo' => 'bar'];
return $setting + parent::defaultSettings();
}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$element['foo'] = [
'#type' => 'textfield',
'#title' => $this->t('Foo'),
'#default_value' => $this->getSetting('foo'),
];
return $element;
}
/**
* {@inheritdoc}
*/
public function settingsSummary(): array {
return [
$this->t('Foo: @foo', ['@foo' => $this->getSetting('foo')]),
];
}
{% endif %}
/**
* {@inheritdoc}
*/
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state): array {
$element['value'] = $element + [
'#type' => 'textfield',
'#default_value' => $items[$delta]->value ?? NULL,
];
return $element;
}
}

View File

@@ -0,0 +1,97 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\migrate\destination;
{% apply sort_namespaces %}
use Drupal\migrate\Plugin\migrate\destination\DestinationBase;
use Drupal\migrate\Plugin\MigrationInterface;
use Drupal\migrate\Row;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* The '{{ plugin_id }}' destination plugin.
*
* @MigrateDestination(id = "{{ plugin_id }}")
*/
final class {{ class }} extends DestinationBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs the plugin instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
MigrationInterface $migration,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $migration);
}
/**
* {@inheritdoc}
*/
public static function create(
ContainerInterface $container,
array $configuration,
$plugin_id,
$plugin_definition,
MigrationInterface $migration = NULL,
): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$migration,
{{ di.container(services) }}
);
}
{% endif %}
/**
* {@inheritdoc}
*/
public function getIds(): array {
$ids['id']['type'] = [
'type' => 'integer',
'unsigned' => TRUE,
'size' => 'big',
];
return $ids;
}
/**
* {@inheritdoc}
*/
public function fields(MigrationInterface $migration = NULL): array {
return [
'id' => $this->t('The row ID.'),
// @todo Describe row fields here.
];
}
/**
* {@inheritdoc}
*/
public function import(Row $row, array $old_destination_id_values = []): array|bool {
// @todo Import the row here.
return [$row->getDestinationProperty('id')];
}
/**
* {@inheritdoc}
*/
public function rollback(array $destination_identifier): void {
// @todo Rollback the row here.
}
}

View File

@@ -0,0 +1,69 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\migrate\process;
{% apply sort_namespaces %}
use Drupal\migrate\MigrateExecutableInterface;
use Drupal\migrate\ProcessPluginBase;
use Drupal\migrate\Row;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Provides {{ plugin_id|article }} plugin.
*
* Usage:
*
* @code
* process:
* bar:
* plugin: {{ plugin_id }}
* source: foo
* @endcode
*
* @MigrateProcessPlugin(id = "{{ plugin_id }}")
*/
final class {{ class }} extends ProcessPluginBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs the plugin instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
/**
* {@inheritdoc}
*/
public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property): mixed {
// @todo Transform the value here.
return $value;
}
}

View File

@@ -0,0 +1,113 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\migrate\source;
{% if source_type == 'sql' %}
use Drupal\Core\Database\Query\SelectInterface;
{% endif %}
use Drupal\migrate\Plugin\migrate\source\{{ base_class }};
use Drupal\migrate\Row;
/**
* The '{{ plugin_id }}' source plugin.
*
* @MigrateSource(
* id = "{{ plugin_id }}",
* source_module = "{{ machine_name }}",
* )
*/
final class {{ class }} extends {{ base_class }} {
{% if source_type == 'sql' %}
/**
* {@inheritdoc}
*/
public function query(): SelectInterface {
return $this->select('example', 'e')
->fields('e', ['id', 'name', 'status']);
}
{% else %}
/**
* {@inheritdoc}
*/
public function __toString(): string {
// @DCG You may return something meaningful here.
return '';
}
/**
* {@inheritdoc}
*/
protected function initializeIterator(): \ArrayIterator {
// @DCG
// In this example we return a hardcoded set of records.
//
// For large sets of data consider using generators like follows:
// @code
// foreach ($foo->nextRecord() as $record) {
// yield $record;
// }
// @endcode
$records = [
[
'id' => 1,
'name' => 'Alpha',
'status' => TRUE,
],
[
'id' => 2,
'name' => 'Beta',
'status' => FALSE,
],
[
'id' => 3,
'name' => 'Gamma',
'status' => TRUE,
],
];
return new \ArrayIterator($records);
}
{% endif %}
/**
* {@inheritdoc}
*/
public function fields(): array {
return [
'id' => $this->t('The record ID.'),
'name' => $this->t('The record name.'),
'status' => $this->t('The record status'),
];
}
/**
* {@inheritdoc}
*/
public function getIds(): array {
$ids['id'] = [
'type' => 'integer',
'unsigned' => TRUE,
'size' => 'big',
];
return $ids;
}
/**
* {@inheritdoc}
*/
public function prepareRow(Row $row): bool {
// @DCG
// Modify the row here if needed.
// Example:
// @code
// $name = $row->getSourceProperty('name');
// $row->setSourceProperty('name', Html::escape('$name'));
// @endcode
return parent::prepareRow($row);
}
}

View File

@@ -0,0 +1,7 @@
views.argument_default.{{ plugin_id }}:
type: mapping
label: '{{ plugin_label }}'
mapping:
example:
type: string
label: 'Example'

View File

@@ -0,0 +1,112 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\views\argument_default;
{% apply sort_namespaces %}
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableDependencyInterface;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
use Drupal\views\Plugin\views\argument_default\ArgumentDefaultPluginBase;
{% if services %}
{{ di.use(services) }}
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* @todo Add plugin description here.
*
* @ViewsArgumentDefault(
* id = "{{ plugin_id }}",
* title = @Translation("{{ plugin_label }}"),
* )
*/
final class {{ class }} extends ArgumentDefaultPluginBase implements CacheableDependencyInterface {
{% if services %}
/**
* Constructs a new {{ class }} instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
{% if configurable %}
/**
* {@inheritdoc}
*/
protected function defineOptions(): array {
$options = parent::defineOptions();
$options['example'] = ['default' => ''];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
$form['example'] = [
'#type' => 'textfield',
'#title' => $this->t('Example'),
'#default_value' => $this->options['example'],
];
}
{% endif %}
/**
* {@inheritdoc}
*
* @todo Make sure the return type-hint matches the argument type.
*/
public function getArgument(): int {
// @DCG
// Here is the place where you should create a default argument for the
// contextual filter. The source of this argument depends on your needs.
// For example, the argument can be extracted from the URL or fetched from
// some fields of the currently viewed entity.
$argument = 123;
return $argument;
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge(): int {
return Cache::PERMANENT;
}
/**
* {@inheritdoc}
*/
public function getCacheContexts(): array {
// @todo Use 'url.path' or 'url.query_args:%key' contexts if the argument
// comes from URL.
return [];
}
}

View File

@@ -0,0 +1,108 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\views\field;
{% apply sort_namespaces %}
use Drupal\Component\Render\MarkupInterface;
use Drupal\views\Plugin\views\field\FieldPluginBase;
use Drupal\views\ResultRow;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
{% if services %}
{{ di.use(services) }}
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Provides {{ plugin_label }} field handler.
*
* @ViewsField("{{ plugin_id }}")
*
* @DCG
* The plugin needs to be assigned to a specific table column through
* hook_views_data() or hook_views_data_alter().
* Put the following code to {{ machine_name }}.views.inc file.
* @code
* function foo_views_data_alter(array &$data): void {
* $data['node']['foo_example']['field'] = [
* 'title' => t('Example'),
* 'help' => t('Custom example field.'),
* 'id' => 'foo_example',
* ];
* }
* @endcode
*/
final class {{ class }} extends FieldPluginBase {
{% if services %}
/**
* Constructs a new {{ class }} instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
{% if configurable %}
/**
* {@inheritdoc}
*/
protected function defineOptions(): array {
$options = parent::defineOptions();
$options['example'] = ['default' => ''];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
parent::buildOptionsForm($form, $form_state);
$form['example'] = [
'#type' => 'textfield',
'#title' => $this->t('Example'),
'#default_value' => $this->options['example'],
];
}
{% endif %}
/**
* {@inheritdoc}
*/
public function query(): void {
// For non-existent columns (i.e. computed fields) this method must be
// empty.
}
/**
* {@inheritdoc}
*/
public function render(ResultRow $values): string|MarkupInterface {
$value = parent::render($values);
// @todo Modify or replace the rendered value here.
return $value;
}
}

View File

@@ -0,0 +1,7 @@
views.field.{{ plugin_id }}:
type: views.field.field
label: '{{ plugin_label }}'
mapping:
example:
type: string
label: 'Example'

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
/**
* @file
* Primary module hooks for {{ name }} module.
*/
use Drupal\Core\Template\Attribute;
/**
* Prepares variables for views-style-{{ plugin_id|u2h }}.html.twig template.
*/
function template_preprocess_views_style_{{ plugin_id }}(array &$variables): void {
$view = $variables['view'];
$options = $view->style_plugin->options;
{% if configurable %}
// Fetch wrapper classes from handler options.
if ($options['wrapper_class']) {
$variables['attributes']['class'] = explode(' ', $options['wrapper_class']);
}
{% endif %}
$variables['default_row_class'] = $options['default_row_class'];
foreach ($variables['rows'] as $id => $row) {
$variables['rows'][$id] = [
'content' => $row,
'attributes' => new Attribute(),
];
if ($row_class = $view->style_plugin->getRowClass($id)) {
$variables['rows'][$id]['attributes']->addClass($row_class);
}
}
}

View File

@@ -0,0 +1,7 @@
views.style.{{ plugin_id }}:
type: views_style
label: '{{ plugin_label }}'
mapping:
wrapper_class:
type: string
label: 'Wrapper class'

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\views\style;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
use Drupal\views\Plugin\views\style\StylePluginBase;
/**
* {{ plugin_label }} style plugin.
*
* @ViewsStyle(
* id = "{{ plugin_id }}",
* title = @Translation("{{ plugin_label }}"),
* help = @Translation("@todo Add help text here."),
* theme = "views_style_{{ plugin_id }}",
* display_types = {"normal"},
* )
*/
final class {{ class }} extends StylePluginBase {
/**
* {@inheritdoc}
*/
protected $usesRowPlugin = TRUE;
/**
* {@inheritdoc}
*/
protected $usesRowClass = TRUE;
{% if configurable %}
/**
* {@inheritdoc}
*/
protected function defineOptions(): array {
$options = parent::defineOptions();
$options['wrapper_class'] = ['default' => 'item-list'];
return $options;
}
/**
* {@inheritdoc}
*/
public function buildOptionsForm(&$form, FormStateInterface $form_state): void {
parent::buildOptionsForm($form, $form_state);
$form['wrapper_class'] = [
'#type' => 'textfield',
'#title' => $this->t('Wrapper class'),
'#description' => $this->t('The class to provide on the wrapper, outside rows.'),
'#default_value' => $this->options['wrapper_class'],
];
}
{% endif %}
}

View File

@@ -0,0 +1,23 @@
{{ '{#' }}
/**
* @file
* Default theme implementation for a view template to display a list of rows.
*
* Available variables:
* - attributes: HTML attributes for the container.
* - rows: A list of rows.
* - attributes: The row's HTML attributes.
* - content: The row's contents.
* - title: The title of this group of rows. May be empty.
*
* @see template_preprocess_views_style_{{ plugin_id }}()
*/
{{ '#}' }}{% verbatim %}
<div{{ attributes }}>
{% set row_classes = [default_row_class ? 'views-row'] %}
{% for row in rows %}
<div{{ row.attributes.addClass(row_classes) }}>
{{ row.content }}
</div>
{% endfor %}
</div>{% endverbatim %}

View File

@@ -0,0 +1,123 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Action;
{% apply sort_namespaces %}
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Entity\ContentEntityInterface;
{% if configurable %}
use Drupal\Core\Action\ConfigurableActionBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Session\AccountInterface;
{% else %}
use Drupal\Core\Action\ActionBase;
use Drupal\Core\Session\AccountInterface;
{% endif %}
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Provides {{ plugin_label|article }} action.
*
* @Action(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* type = "{{ entity_type }}",
* category = @Translation("{{ category }}"),
* )
*
* @DCG
* For updating entity fields consider extending FieldUpdateActionBase.
* @see \Drupal\Core\Field\FieldUpdateActionBase
*
* @DCG
* In order to set up the action through admin interface the plugin has to be
* configurable.
* @see https://www.drupal.org/project/drupal/issues/2815301
* @see https://www.drupal.org/project/drupal/issues/2815297
*
* @DCG
* The whole action API is subject of change.
* @see https://www.drupal.org/project/drupal/issues/2011038
*/
final class {{ class }} extends {{ configurable ? 'ConfigurableActionBase' : 'ActionBase' }} {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* {@inheritdoc}
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
{% if configurable %}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return ['example' => ''];
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
$form['example'] = [
'#type' => 'textfield',
'#title' => $this->t('Example'),
'#default_value' => $this->configuration['example'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void {
$this->configuration['example'] = $form_state->getValue('example');
}
{% endif %}
/**
* {@inheritdoc}
*/
public function access($entity, AccountInterface $account = NULL, $return_as_object = FALSE): AccessResultInterface|bool {
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$access = $entity->access('update', $account, TRUE)
->andIf($entity->get('field_example')->access('edit', $account, TRUE));
return $return_as_object ? $access : $access->isAllowed();
}
/**
* {@inheritdoc}
*/
public function execute(ContentEntityInterface $entity = NULL): void {
$entity->set('field_example', 'New value')->save();
}
}

View File

@@ -0,0 +1,7 @@
action.configuration.{{ plugin_id }}:
type: mapping
label: 'Configuration for "{{ plugin_label }}" action'
mapping:
example:
type: string
label: Example

View File

@@ -0,0 +1,111 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Block;
{% apply sort_namespaces %}
{% if access %}
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Session\AccountInterface;
{% endif %}
use Drupal\Core\Block\BlockBase;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Provides {{ plugin_label|article|lower }} block.
*
* @Block(
* id = "{{ plugin_id }}",
* admin_label = @Translation("{{ plugin_label }}"),
* category = @Translation("{{ category }}"),
* )
*/
final class {{ class }} extends BlockBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs the plugin instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
{% if configurable %}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return [
'example' => $this->t('Hello world!'),
];
}
/**
* {@inheritdoc}
*/
public function blockForm($form, FormStateInterface $form_state): array {
$form['example'] = [
'#type' => 'textarea',
'#title' => $this->t('Example'),
'#default_value' => $this->configuration['example'],
];
return $form;
}
/**
* {@inheritdoc}
*/
public function blockSubmit($form, FormStateInterface $form_state): void {
$this->configuration['example'] = $form_state->getValue('example');
}
{% endif %}
/**
* {@inheritdoc}
*/
public function build(): array {
$build['content'] = [
'#markup' => $this->t('It works!'),
];
return $build;
}
{% if access %}
/**
* {@inheritdoc}
*/
protected function blockAccess(AccountInterface $account): AccessResult {
// @todo Evaluate the access condition here.
return AccessResult::allowedIf(TRUE);
}
{% endif %}
}

View File

@@ -0,0 +1,7 @@
block.settings.{{ plugin_id }}:
type: block_settings
label: '{{ plugin_label }} block'
mapping:
example:
type: string
label: Example

View File

@@ -0,0 +1,3 @@
.ckeditor5-toolbar-button-{{ fe_plugin_id }} {
background-image: url(../icons/{{ unprefixed_plugin_id|u2h }}.svg);
}

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,3 @@
<svg width="20" height="20" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg">
<path d="M1.25 3.75h17.5v3.75h-17.5zM1.25 8.75h17.5v3.75h-17.5zM1.25 13.75h17.5v3.75h-17.5z"/>
</svg>

After

Width:  |  Height:  |  Size: 188 B

View File

@@ -0,0 +1 @@
This is the default destination folder for CKEditor 5 plugin builds.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,35 @@
import { Plugin } from 'ckeditor5/src/core';
import { ButtonView } from 'ckeditor5/src/ui';
import icon from '../../../../icons/{{ unprefixed_plugin_id|u2h }}.svg';
export default class {{ class }} extends Plugin {
init() {
const editor = this.editor;
const buttonFactory = function () {
const button = new ButtonView();
button.set(
{
label: '{{ plugin_label }}',
icon: icon,
tooltip: true,
}
);
// Change the model using the model writer.
const write = writer => {
// Insert the text at the user's current position.
editor.model.insertContent(writer.createText('It works!'));
}
const executeHandler = () => {
editor.model.change(write);
}
button.on('execute', executeHandler);
return button;
};
editor.ui.componentFactory.add('{{ fe_plugin_id }}', buttonFactory);
}
}

View File

@@ -0,0 +1,10 @@
/**
* @file The build process always expects an index.js file. Anything exported
* here will be recognized by CKEditor 5 as an available plugin. Multiple
* plugins can be exported in this one file.
*
* I.e. this file's purpose is to make plugin(s) discoverable.
*/
import {{ class }} from './{{ class }}';
export default { {{ class }} };

View File

@@ -0,0 +1,18 @@
# @see core/modules/ckeditor5/ckeditor5.ckeditor5.yml
# @see https://www.drupal.org/project/ckeditor5_dev
{{ plugin_id }}:
# Configuration that will be sent to CKEditor 5 JavaScript plugins.
ckeditor5:
plugins:
- {{ fe_plugin_id }}.{{ class }}
# Configuration that will be used directly by Drupal.
drupal:
label: '{{ plugin_label }}'
library: {{ machine_name }}/{{ unprefixed_plugin_id }}
admin_library: {{ machine_name }}/admin.{{ unprefixed_plugin_id }}
toolbar_items:
{{ fe_plugin_id }}:
label: '{{ plugin_label }}'
# The plugin does not provide elements.
elements: false

View File

@@ -0,0 +1,12 @@
{{ unprefixed_plugin_id }}:
js:
js/build/{{ class|camelize(false) }}.js: { preprocess: false, minified: true }
dependencies:
- ckeditor5/ckeditor5
# Loaded in the text format configuration form to provide styling for the icon
# used in toolbar config.
admin.{{ unprefixed_plugin_id }}:
css:
theme:
css/{{ unprefixed_plugin_id|u2h }}.admin.css: { }

View File

@@ -0,0 +1,18 @@
{
"name": "{{ machine_name }}",
"version": "1.0.0",
"description": "Provides CKEditor plugin.",
"author": "",
"license": "GPL-2.0-or-later",
"scripts": {
"watch": "webpack --mode development --watch",
"build": "webpack"
},
"devDependencies": {
"ckeditor5": "~35.3.2",
"raw-loader": "^4.0.2",
"terser-webpack-plugin": "^5.3.3",
"webpack": "^5.51.1",
"webpack-cli": "^4.4.0"
}
}

View File

@@ -0,0 +1,59 @@
const path = require('path');
const fs = require('fs');
const TerserPlugin = require('terser-webpack-plugin');
const webpack = require('webpack');
const SRC_PATH = './js/ckeditor5_plugins';
// Loop through every subdirectory in src, each a different plugin, and build
// each one in ./build.
module.exports = fs
.readdirSync(SRC_PATH)
.filter(item => fs.statSync(path.join(SRC_PATH, item)).isDirectory())
.map(createConfig);
function createConfig(dir) {
return {
mode: 'production',
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
format: {
comments: false,
},
},
test: /\.js(\?.*)?$/i,
extractComments: false,
}),
],
moduleIds: 'named',
},
entry: {
path: path.resolve(
__dirname,
'js/ckeditor5_plugins',
dir,
'src/index.js',
),
},
output: {
path: path.resolve(__dirname, './js/build'),
filename: `${dir}.js`,
library: ['CKEditor5', dir],
libraryTarget: 'umd',
libraryExport: 'default',
},
plugins: [
new webpack.DllReferencePlugin({
manifest: require('./node_modules/ckeditor5/build/ckeditor5-dll.manifest.json'),
scope: 'ckeditor5/src',
name: 'CKEditor5.dll',
}),
],
module: {
rules: [{ test: /\.svg$/, use: 'raw-loader' }],
},
};
}

View File

@@ -0,0 +1,98 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Condition;
{% apply sort_namespaces %}
use Drupal\Core\Condition\ConditionPluginBase;
use Drupal\Core\Form\FormStateInterface;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Provides a '{{ plugin_label }}' condition.
*
* @Condition(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* )
*/
final class {{ class }} extends ConditionPluginBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs a new {{ class }} instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
return ['example' => ''] + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
$form['example'] = [
'#type' => 'textfield',
'#title' => $this->t('Example'),
'#default_value' => $this->configuration['example'],
];
return parent::buildConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void {
$this->configuration['example'] = $form_state->getValue('example');
parent::submitConfigurationForm($form, $form_state);
}
/**
* {@inheritdoc}
*/
public function summary(): string {
return (string) $this->t(
'Example: @example', ['@example' => $this->configuration['example']],
);
}
/**
* {@inheritdoc}
*/
public function evaluate(): bool {
// @todo Evaluate the condition here.
return TRUE;
}
}

View File

@@ -0,0 +1,7 @@
condition.plugin.{{ plugin_id }}:
type: condition.plugin
label: '{{ plugin_label }} condition'
mapping:
age:
type: integer
label: Age

View File

@@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Validation\Constraint;
use Symfony\Component\Validator\Constraint;
/**
* Provides {{ plugin_label|article }} constraint.
*
* @Constraint(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}", context = "Validation"),
* )
{% if input_type == 'entity' %}
*
* @see https://www.drupal.org/node/2015723.
{% elseif input_type == 'item_list' %}
*
* @DCG
* To apply this constraint on third party entity types implement either
* hook_entity_base_field_info_alter() or hook_entity_bundle_field_info_alter().
*
* @see https://www.drupal.org/node/2015723
{% elseif input_type == 'item' %}
*
* @DCG
* To apply this constraint on third party field types. Implement
* hook_field_info_alter() as follows.
* @code
* function {{ machine_name }}_field_info_alter(array &$info): void {
* $info['FIELD_TYPE']['constraints']['{{ plugin_id }}'] = [];
* }
* @endcode
*
* @see https://www.drupal.org/node/2015723
{% endif %}
*/
final class {{ class }} extends Constraint {
public string $message = '@todo Specify error message here.';
}

View File

@@ -0,0 +1,106 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Validation\Constraint;
{% apply sort_namespaces %}
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;
{% if input_type == 'item' %}
use Drupal\Core\Field\FieldItemInterface;
{% elseif input_type == 'item_list' %}
use Drupal\Core\Field\FieldItemListInterface;
{% elseif input_type == 'entity' %}
use Drupal\Core\Entity\EntityInterface;
{% endif %}
{% if services %}
{{ di.use(services) }}
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Validates the {{ plugin_label }} constraint.
*/
final class {{ class }}Validator extends ConstraintValidator {% if services %}implements ContainerInjectionInterface {% endif %}{
{% if services %}
/**
* Constructs the object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): self {
return new self(
{{ di.container(services) }}
);
}
{% endif %}
/**
* {@inheritdoc}
*/
{% if input_type == 'raw_value' %}
public function validate(mixed $value, Constraint $constraint): void {
// @todo Validate the value here.
if ($value === 'wrong') {
$this->context->addViolation($constraint->message);
}
}
{% elseif input_type == 'item' %}
public function validate(mixed $item, Constraint $constraint): void {
if (!$item instanceof FieldItemInterface) {
throw new \InvalidArgumentException(
sprintf('The validated value must be instance of \Drupal\Core\Field\FieldItemInterface, %s was given.', get_debug_type($item))
);
}
// @todo Validate the item value here.
if ($item->value === 'wrong') {
$this->context->addViolation($constraint->message);
}
}
{% elseif input_type == 'item_list' %}
public function validate(mixed $items, Constraint $constraint): void {
if (!$items instanceof FieldItemListInterface) {
throw new \InvalidArgumentException(
sprintf('The validated value must be instance of \Drupal\Core\Field\FieldItemListInterface, %s was given.', get_debug_type($items))
);
}
foreach ($items as $delta => $item) {
// @todo Validate the item list here.
if ($item->value === 'wrong') {
$this->context->buildViolation($constraint->message)
->atPath($delta)
->addViolation();
}
}
}
{% elseif input_type == 'entity' %}
public function validate(mixed $entity, Constraint $constraint): void {
if (!$entity instanceof EntityInterface) {
throw new \InvalidArgumentException(
sprintf('The validated value must be instance of \Drupal\Core\Entity\EntityInterface, %s was given.', get_debug_type($entity))
);
}
// @todo Validate the entity here.
if ($entity->label() === 'wrong') {
// @DCG Use the following code to bind the violation to a specific field.
// @code
// $this->context->buildViolation($constraint->message)
// ->atPath('field_example')
// ->addViolation();
// @endcode
$this->context->addViolation($constraint->message);
}
}
{% endif %}
}

View File

@@ -0,0 +1,63 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\EntityReferenceSelection;
{% apply sort_namespaces %}
use Drupal\Core\Entity\Query\QueryInterface;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
use {{ base_class_full }}{% if base_class == class %} as Base{{ base_class }}{% endif %};
{% endapply %}
/**
* @todo Add plugin description here.
*
* @EntityReferenceSelection(
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* group = "{{ plugin_id }}",
* entity_types = {"{{ entity_type }}"},
* )
*/
final class {{ class }} extends {{ base_class == class ? 'Base' ~ base_class : base_class }} {
{% if configurable %}
/**
* {@inheritdoc}
*/
public function defaultConfiguration(): array {
$default_configuration = [
'foo' => 'bar',
];
return $default_configuration + parent::defaultConfiguration();
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state): array {
$form = parent::buildConfigurationForm($form, $form_state);
$form['foo'] = [
'#type' => 'textfield',
'#title' => $this->t('Foo'),
'#default_value' => $this->configuration['foo'],
];
return $form;
}
{% endif %}
/**
* {@inheritdoc}
*/
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS'): QueryInterface {
$query = parent::buildEntityQuery($match, $match_operator);
// @todo Modify the query here.
return $query;
}
}

View File

@@ -0,0 +1,10 @@
entity_reference_selection.{{ plugin_id }}:
{# User selection plugin provides has some additional options. #}
type: entity_reference_selection.default{{ entity_type == 'user' ? ':user' }}
label: '{{ plugin_label }} handler settings'
{% if configurable %}
mapping:
foo:
type: string
label: Foo
{% endif %}

View File

@@ -0,0 +1,96 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Filter;
{% apply sort_namespaces %}
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* @todo Add filter description here.
*
* @Filter(
* id = "{{ plugin_id }}",
* title = @Translation("{{ plugin_label }}"),
* type = Drupal\filter\Plugin\FilterInterface::{{ filter_type }},
{% if configurable %}
* settings = {
* "example" = "foo",
* },
{% endif %}
* )
*/
final class {{ class }} extends FilterBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs a new {{ class }} instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
{% if configurable %}
/**
* {@inheritdoc}
*/
public function settingsForm(array $form, FormStateInterface $form_state): array {
$form['example'] = [
'#type' => 'textfield',
'#title' => $this->t('Example'),
'#default_value' => $this->settings['example'],
'#description' => $this->t('Description of the setting.'),
];
return $form;
}
{% endif %}
/**
* {@inheritdoc}
*/
public function process($text, $langcode): FilterProcessResult {
// @todo Process text here.
{% if SUT_TEST %}
$text = \str_replace('foo', 'bar', $text);
{% endif %}
return new FilterProcessResult($text);
}
/**
* {@inheritdoc}
*/
public function tips($long = FALSE): string {
return (string) $this->t('@todo Provide filter tips here.');
}
}

View File

@@ -0,0 +1,7 @@
filter_settings.{{ plugin_id }}:
type: filter
label: '{{ plugin_label }} filter'
mapping:
example:
type: string
label: Example

View File

@@ -0,0 +1,71 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Menu;
{% apply sort_namespaces %}
use Drupal\Core\Menu\MenuLinkDefault;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* @todo Provide description for this class.
*
* @DCG
* Typically a module-defined menu link relies on
* \Drupal\Core\Menu\MenuLinkDefault class that builds the link using plugin
* definitions located in YAML files (MODULE_NAME.links.menu.yml). The purpose
* of having custom menu link class is to make the link dynamic. Sometimes, the
* title and the URL of a link should vary based on some context, i.e. user
* being logged, current page URL, etc. Check out the parent classes for the
* methods you can override to make the link dynamic.
*
* @DCG It is important to supply the link with correct cache metadata.
* @see self::getCacheContexts()
* @see self::getCacheTags()
*
* @DCG
* You can apply the class to a link as follows.
* @code
* foo.example:
* title: Example
* route_name: foo.example
* menu_name: main
* class: \Drupal\foo\Plugin\Menu\FooMenuLink
* @endcode
*/
final class {{ class }} extends MenuLinkDefault {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs a new {{ class }} instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
}

View File

@@ -0,0 +1,61 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\QueueWorker;
{% apply sort_namespaces %}
use Drupal\Core\Queue\QueueWorkerBase;
{% if services %}
{{ di.use(services) }}
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
{% endif %}
{% endapply %}
/**
* Defines '{{ plugin_id }}' queue worker.
*
* @QueueWorker(
* id = "{{ plugin_id }}",
* title = @Translation("{{ plugin_label }}"),
* cron = {"time" = 60},
* )
*/
final class {{ class }} extends QueueWorkerBase {% if services %}implements ContainerFactoryPluginInterface {% endif %}{
{% if services %}
/**
* Constructs a new {{ class }} instance.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
{{ di.signature(services) }}
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
{{ di.container(services) }}
);
}
{% endif %}
/**
* {@inheritdoc}
*/
public function processItem($data): void {
// @todo Process data here.
}
}

View File

@@ -0,0 +1,156 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\rest\resource;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\rest\ModifiedResourceResponse;
use Drupal\rest\Plugin\ResourceBase;
use Drupal\rest\ResourceResponse;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use Symfony\Component\Routing\Route;
/**
* Represents {{ plugin_label }} records as resources.
*
* @RestResource (
* id = "{{ plugin_id }}",
* label = @Translation("{{ plugin_label }}"),
* uri_paths = {
* "canonical" = "/api/{{ plugin_id|u2h }}/{id}",
* "create" = "/api/{{ plugin_id|u2h }}"
* }
* )
*
* @DCG
* The plugin exposes key-value records as REST resources. In order to enable it
* import the resource configuration into active configuration storage. An
* example of such configuration can be located in the following file:
* core/modules/rest/config/optional/rest.resource.entity.node.yml.
* Alternatively, you can enable it through admin interface provider by REST UI
* module.
* @see https://www.drupal.org/project/restui
*
* @DCG
* Notice that this plugin does not provide any validation for the data.
* Consider creating custom normalizer to validate and normalize the incoming
* data. It can be enabled in the plugin definition as follows.
* @code
* serialization_class = "Drupal\foo\MyDataStructure",
* @endcode
*
* @DCG
* For entities, it is recommended to use REST resource plugin provided by
* Drupal core.
* @see \Drupal\rest\Plugin\rest\resource\EntityResource
*/
final class {{ class }} extends ResourceBase {
/**
* The key-value storage.
*/
private readonly KeyValueStoreInterface $storage;
/**
* {@inheritdoc}
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
array $serializer_formats,
LoggerInterface $logger,
KeyValueFactoryInterface $keyValueFactory,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
$this->storage = $keyValueFactory->get('{{ plugin_id }}');
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition): self {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$container->getParameter('serializer.formats'),
$container->get('logger.factory')->get('rest'),
$container->get('keyvalue')
);
}
/**
* Responds to POST requests and saves the new record.
*/
public function post(array $data): ModifiedResourceResponse {
$data['id'] = $this->getNextId();
$this->storage->set($data['id'], $data);
$this->logger->notice('Created new {{ plugin_label|lower }} record @id.', ['@id' => $data['id']]);
// Return the newly created record in the response body.
return new ModifiedResourceResponse($data, 201);
}
/**
* Responds to GET requests.
*/
public function get($id): ResourceResponse {
if (!$this->storage->has($id)) {
throw new NotFoundHttpException();
}
$resource = $this->storage->get($id);
return new ResourceResponse($resource);
}
/**
* Responds to PATCH requests.
*/
public function patch($id, array $data): ModifiedResourceResponse {
if (!$this->storage->has($id)) {
throw new NotFoundHttpException();
}
$stored_data = $this->storage->get($id);
$data += $stored_data;
$this->storage->set($id, $data);
$this->logger->notice('The {{ plugin_label|lower }} record @id has been updated.', ['@id' => $id]);
return new ModifiedResourceResponse($data, 200);
}
/**
* Responds to DELETE requests.
*/
public function delete($id): ModifiedResourceResponse {
if (!$this->storage->has($id)) {
throw new NotFoundHttpException();
}
$this->storage->delete($id);
$this->logger->notice('The {{ plugin_label|lower }} record @id has been deleted.', ['@id' => $id]);
// Deleted responses have an empty body.
return new ModifiedResourceResponse(NULL, 204);
}
/**
* {@inheritdoc}
*/
protected function getBaseRoute($canonical_path, $method): Route {
$route = parent::getBaseRoute($canonical_path, $method);
// Set ID validation pattern.
if ($method !== 'POST') {
$route->setRequirement('id', '\d+');
}
return $route;
}
/**
* Returns next available ID.
*/
private function getNextId(): int {
$ids = \array_keys($this->storage->getAll());
return count($ids) > 0 ? max($ids) + 1 : 1;
}
}

View File

@@ -0,0 +1,59 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Access;
{% apply sort_namespaces %}
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Routing\Access\AccessInterface;
use Symfony\Component\Routing\Route;
{% if services %}
{{ di.use(services) }}
{% endif %}
{% endapply %}
/**
* Checks if passed parameter matches the route configuration.
*
* Usage example:
* @code
* foo.example:
* path: '/example/{parameter}'
* defaults:
* _title: 'Example'
* _controller: '\Drupal\{{ machine_name }}\Controller\{{ machine_name|camelize }}Controller'
* requirements:
* {{ requirement }}: 'some value'
* @endcode
*/
final class {{ class }} implements AccessInterface {
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
{% endif %}
/**
* Access callback.
*
* @DCG
* Drupal does some magic when resolving arguments for this callback. Make
* sure the parameter name matches the name of the placeholder defined in the
* route, and it is of the same type.
* The following additional parameters are resolved automatically.
* - \Drupal\Core\Routing\RouteMatchInterface
* - \Drupal\Core\Session\AccountInterface
* - \Symfony\Component\HttpFoundation\Request
* - \Symfony\Component\Routing\Route
*/
public function access(Route $route, mixed $parameter): AccessResult {
return AccessResult::allowedIf($parameter === $route->getRequirement('{{ requirement }}'));
}
}

View File

@@ -0,0 +1,9 @@
{% import '@lib/di.twig' as di %}
services:
access_check.{{ machine_name }}.{{ requirement|trim('_') }}:
class: Drupal\{{ machine_name }}\Access\{{ class }}
{% if services %}
arguments: [{{ di.arguments(services) }}]
{% endif %}
tags:
- { name: access_check, applies_to: {{ requirement }} }

View File

@@ -0,0 +1,54 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
{% apply sort_namespaces %}
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Link;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
{% if services %}
{{ di.use(services) }}
{% endif %}
{% endapply %}
/**
* @todo Add description for this breadcrumb builder.
*/
final class {{ class }} implements BreadcrumbBuilderInterface {
use StringTranslationTrait;
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
{% endif %}
/**
* {@inheritdoc}
*/
public function applies(RouteMatchInterface $route_match): bool {
return $route_match->getRouteName() === 'example';
}
/**
* {@inheritdoc}
*/
public function build(RouteMatchInterface $route_match): Breadcrumb {
$breadcrumb = new Breadcrumb();
$links[] = Link::createFromRoute($this->t('Home'), '<front>');
$links[] = Link::createFromRoute($this->t('Example'), '<none>');
return $breadcrumb->setLinks($links);
}
}

View File

@@ -0,0 +1,11 @@
{% import '@lib/di.twig' as di %}
services:
{{ machine_name }}.breadcrumb:
class: Drupal\{{ machine_name }}\{{ class }}
{% if services %}
arguments: [{{ di.arguments(services) }}]
{% endif %}
tags:
# In order to override breadcrumbs built with PathBasedBreadcrumbBuilder
# set the priority higher than zero.
- { name: breadcrumb_builder, priority: 1000 }

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Cache\Context;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Cache\Context\{{ interface }};
{% if base_class %}
use Drupal\Core\Cache\Context\{{ base_class }};
{% endif %}
/**
* @todo Add a description for the cache context.
*
* Cache context ID: '{{ context_id }}'.
*
* @DCG
* Check out the core/lib/Drupal/Core/Cache/Context directory for examples of
* cache contexts provided by Drupal core.
*/
final class {{ class }} {% if base_class %}extends {{ base_class }} {% endif %}implements {{ interface }} {
/**
* {@inheritdoc}
*/
public static function getLabel(): string {
return (string) t('{{ context_label }}');
}
/**
* {@inheritdoc}
*/
public function getContext({% if calculated %}$parameter = NULL{% endif %}): string {
// @todo Calculate the cache context here.
$context = 'some_string_value';
return $context;
}
/**
* {@inheritdoc}
*/
public function getCacheableMetadata({% if calculated %}$parameter = NULL{% endif %}): CacheableMetadata {
return new CacheableMetadata();
}
}

View File

@@ -0,0 +1,10 @@
services:
cache_context.{{ context_id }}:
class: Drupal\{{ machine_name }}\Cache\Context\{{ class }}
{% if base_class == 'RequestStackCacheContextBase' %}
arguments: ['@request_stack']
{% elseif base_class == 'UserCacheContextBase' %}
arguments: ['@current_user']
{% endif %}
tags:
- { name: cache.context}

View File

@@ -0,0 +1,32 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
{% if services %}
{{ di.use(services) }}
{% endif %}
/**
* @todo Add class description.
*/
final class {{ class }}{{ interface ? ' implements ' ~ interface }} {
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
{% endif %}
/**
* {{ interface ? '{@inheritdoc}' : '@todo Add method description.'}}
*/
public function doSomething(): void {
// @todo Place your code here.
}
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
/**
* @todo Add interface description.
*/
interface {{ interface }} {
/**
* @todo Add method description.
*/
public function doSomething(): void;
}

View File

@@ -0,0 +1,7 @@
{% import '@lib/di.twig' as di %}
services:
{{ service_name }}:
class: Drupal\{{ machine_name }}\{{ class }}
{% if services %}
arguments: [{{ di.arguments(services) }}]
{% endif %}

View File

@@ -0,0 +1,62 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\EventSubscriber;
{% apply sort_namespaces %}
{% if services %}
{{ di.use(services) }}
{% endif %}
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\Event\RequestEvent;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
{% endapply %}
/**
* @todo Add description for this subscriber.
*/
final class {{ class }} implements EventSubscriberInterface {
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
{% endif %}
/**
* Kernel request event handler.
*/
public function onKernelRequest(RequestEvent $event): void {
// @todo Place your code here.
{% if SUT_TEST %}
$this->messenger->addStatus(__FUNCTION__);
{% endif %}
}
/**
* Kernel response event handler.
*/
public function onKernelResponse(ResponseEvent $event): void {
// @todo Place your code here.
{% if SUT_TEST %}
$this->messenger->addStatus(__FUNCTION__);
{% endif %}
}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
KernelEvents::REQUEST => ['onKernelRequest'],
KernelEvents::RESPONSE => ['onKernelResponse'],
];
}
}

View File

@@ -0,0 +1,9 @@
{% import '@lib/di.twig' as di %}
services:
{{ machine_name }}.event_subscriber:
class: Drupal\{{ machine_name }}\EventSubscriber\{{ class }}
{% if services %}
arguments: [{{ di.arguments(services) }}]
{% endif %}
tags:
- { name: event_subscriber }

View File

@@ -0,0 +1,48 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Logger;
{% apply sort_namespaces %}
use Drupal\Core\Logger\LogMessageParserInterface;
use Drupal\Core\Logger\RfcLoggerTrait;
use Psr\Log\LoggerInterface;
{% if services %}
{{ di.use(services) }}
{% endif %}
{% endapply %}
/**
* @todo Add a description for the logger.
*/
final class {{ class }} implements LoggerInterface {
use RfcLoggerTrait;
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
{% endif %}
/**
* {@inheritdoc}
*/
public function log($level, string|\Stringable $message, array $context = []): void {
// Convert PSR3-style messages to \Drupal\Component\Render\FormattableMarkup
// style, so they can be translated too.
$placeholders = $this->parser->parseMessagePlaceholders($message, $context);
// @see \Drupal\Core\Logger\LoggerChannel::log() for all available contexts.
$rendered_message = strtr($message, $placeholders);
// @todo Log the rendered message here.
{% if SUT_TEST %}
\file_put_contents('temporary://logger_test.log', $level . ' -> ' . $rendered_message);
{% endif %}
}
}

View File

@@ -0,0 +1,9 @@
{% import '@lib/di.twig' as di %}
services:
logger.{{ service_id }}:
class: Drupal\{{ machine_name }}\Logger\{{ class }}
{% if services %}
arguments: [{{ di.arguments(services) }}]
{% endif %}
tags:
- { name: logger }

View File

@@ -0,0 +1,44 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }};
{% apply sort_namespaces %}
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\HttpKernelInterface;
{% if services %}
{{ di.use(services) }}
{% endif %}
{% endapply %}
/**
* @todo Add a description for the middleware.
*/
final class {{ class }} implements HttpKernelInterface {
{% if services %}
/**
* Constructs {{ class|article }} object.
*/
public function __construct(
{{ di.signature(services) }}
) {}
{% endif %}
/**
* {@inheritdoc}
*/
public function handle(Request $request, $type = self::MAIN_REQUEST, $catch = TRUE): Response {
// @todo Modify the request here.
$response = $this->httpKernel->handle($request, $type, $catch);
// @todo Modify the response here.
{% if SUT_TEST %}
$response->headers->set('x-middleware-handle-test', NULL);
{% endif %}
return $response;
}
}

Some files were not shown because too many files have changed in this diff Show More