Initial Drupal 11 with DDEV setup

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

View File

@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Field\FieldFormatter;
{% apply sort_namespaces %}
use Drupal\Core\Field\Attribute\FieldFormatter;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\FormatterBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
{% endapply %}
/**
* Plugin implementation of the '{{ plugin_label }}' formatter.
*/
#[FieldFormatter(
id: '{{ plugin_id }}',
label: new TranslatableMarkup('{{ plugin_label }}'),
field_types: ['string'],
)]
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,151 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Field\FieldType;
{% apply sort_namespaces %}
use Drupal\Component\Utility\Random;
use Drupal\Core\Field\Attribute\FieldType;
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\StringTranslation\TranslatableMarkup;
use Drupal\Core\TypedData\DataDefinition;
{% endapply %}
/**
* Defines the '{{ plugin_id }}' field type.
*/
#[FieldType(
id: '{{ plugin_id }}',
label: new TranslatableMarkup('{{ plugin_label }}'),
description: new TranslatableMarkup('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,99 @@
{% 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\Attribute\FieldWidget;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\Field\WidgetBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
{% 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: new TranslatableMarkup('{{ 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\Attribute\MigrateDestination;
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('{{ 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\Attribute\MigrateProcess;
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
*/
#[MigrateProcess('{{ 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,113 @@
{% 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;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsArgumentDefault;
{% 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: new TranslatableMarkup('{{ 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\Attribute\ViewsField;
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.
*
* @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
*/
#[ViewsField('{{ plugin_id }}')]
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,62 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\views\style;
{% apply sort_namespaces %}
{% if configurable %}
use Drupal\Core\Form\FormStateInterface;
{% endif %}
use Drupal\views\Plugin\views\style\StylePluginBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\views\Attribute\ViewsStyle;
{% endapply %}
/**
* {{ plugin_label }} style plugin.
*/
#[ViewsStyle(
id: '{{ plugin_id }}',
title: new TranslatableMarkup('{{ plugin_label }}'),
help: new TranslatableMarkup('@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,124 @@
{% 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\Action\Attribute\Action;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
{% 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.
*
* @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
*/
#[Action(
id: '{{ plugin_id }}',
label: new TranslatableMarkup('{{ plugin_label }}'),
category: new TranslatableMarkup('{{ category }}'),
type: '{{ entity_type }}',
)]
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,112 @@
{% 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\Attribute\Block;
use Drupal\Core\Block\BlockBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
{% 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: new TranslatableMarkup('{{ plugin_label }}'),
category: new TranslatableMarkup('{{ 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,99 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Condition;
{% apply sort_namespaces %}
use Drupal\Core\Condition\Attribute\Condition;
use Drupal\Core\StringTranslation\TranslatableMarkup;
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: new TranslatableMarkup('{{ 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,45 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\Validation\Constraint;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;
/**
* Provides {{ plugin_label|article }} constraint.
{% 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 %}
*/
#[Constraint(
id: '{{ plugin_id }}',
label: new TranslatableMarkup('{{ plugin_label }}', options: ['context' => 'Validation'])
)]
final class {{ class }} extends SymfonyConstraint {
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,65 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\EntityReferenceSelection;
{% apply sort_namespaces %}
use Drupal\Core\Entity\Attribute\EntityReferenceSelection;
use Drupal\Core\Entity\Query\QueryInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
{% 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: new TranslatableMarkup('{{ plugin_label }}'),
group: '{{ plugin_id }}',
weight: 1,
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\Core\StringTranslation\TranslatableMarkup;
use Drupal\filter\Attribute\Filter;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Plugin\FilterBase;
use Drupal\filter\Plugin\FilterInterface;
{% 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: new TranslatableMarkup('{{ plugin_label }}'),
type: 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,62 @@
{% import '@lib/di.twig' as di %}
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\QueueWorker;
{% apply sort_namespaces %}
use Drupal\Core\Queue\Attribute\QueueWorker;
use Drupal\Core\Queue\QueueWorkerBase;
use Drupal\Core\StringTranslation\TranslatableMarkup;
{% 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: new TranslatableMarkup('{{ 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,157 @@
<?php
declare(strict_types=1);
namespace Drupal\{{ machine_name }}\Plugin\rest\resource;
use Drupal\Core\KeyValueStore\KeyValueFactoryInterface;
use Drupal\Core\KeyValueStore\KeyValueStoreInterface;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\rest\Attribute\RestResource;
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.
*
* @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
*/
#[RestResource(
id: '{{ plugin_id }}',
label: new TranslatableMarkup('{{ plugin_label }}'),
uri_paths: [
'canonical' => '/api/{{ plugin_id|u2h }}/{id}',
'create' => '/api/{{ plugin_id|u2h }}',
],
)]
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;
}
}