Issue #2920682 by phenaproxima, alexpott, Sam152, borisson_, Wim Leers, larowlan: Add config validation for plugin IDs
parent
9cc7cfaa1c
commit
529550a4b2
|
@ -0,0 +1,93 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\Core\Plugin\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\Exception\MissingOptionsException;
|
||||
|
||||
/**
|
||||
* Checks if a plugin exists and optionally implements a particular interface.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "PluginExists",
|
||||
* label = @Translation("Plugin exists", context = "Validation"),
|
||||
* )
|
||||
*/
|
||||
class PluginExistsConstraint extends Constraint implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The error message if a plugin does not exist.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $unknownPluginMessage = "The '@plugin_id' plugin does not exist.";
|
||||
|
||||
/**
|
||||
* The error message if a plugin does not implement the expected interface.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public string $invalidInterfaceMessage = "The '@plugin_id' plugin must implement or extend @interface.";
|
||||
|
||||
/**
|
||||
* The ID of the plugin manager service.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected string $manager;
|
||||
|
||||
/**
|
||||
* Optional name of the interface that the plugin must implement.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public ?string $interface = NULL;
|
||||
|
||||
/**
|
||||
* Constructs a PluginExistsConstraint.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface $pluginManager
|
||||
* The plugin manager associated with the constraint.
|
||||
* @param mixed|null $options
|
||||
* The options (as associative array) or the value for the default option
|
||||
* (any other type).
|
||||
* @param array|null $groups
|
||||
* An array of validation groups.
|
||||
* @param mixed|null $payload
|
||||
* Domain-specific data attached to a constraint.
|
||||
*/
|
||||
public function __construct(public readonly PluginManagerInterface $pluginManager, mixed $options = NULL, array $groups = NULL, mixed $payload = NULL) {
|
||||
parent::__construct($options, $groups, $payload);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
$plugin_manager_id = $configuration['manager'] ?? $configuration['value'] ?? NULL;
|
||||
if ($plugin_manager_id === NULL) {
|
||||
throw new MissingOptionsException(sprintf('The option "manager" must be set for constraint "%s".', static::class), ['manager']);
|
||||
}
|
||||
return new static($container->get($plugin_manager_id), $configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDefaultOption(): ?string {
|
||||
return 'manager';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getRequiredOptions(): array {
|
||||
return ['manager'];
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace Drupal\Core\Plugin\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Component\Plugin\Factory\DefaultFactory;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Validates the PluginExists constraint.
|
||||
*/
|
||||
class PluginExistsConstraintValidator extends ConstraintValidator {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate(mixed $plugin_id, Constraint $constraint) {
|
||||
assert($constraint instanceof PluginExistsConstraint);
|
||||
|
||||
$definition = $constraint->pluginManager->getDefinition($plugin_id, FALSE);
|
||||
if (empty($definition)) {
|
||||
$this->context->addViolation($constraint->unknownPluginMessage, [
|
||||
'@plugin_id' => $plugin_id,
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// If we don't need to validate the plugin class's interface, we're done.
|
||||
if (empty($constraint->interface)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!is_a(DefaultFactory::getPluginClass($plugin_id, $definition), $constraint->interface, TRUE)) {
|
||||
$this->context->addViolation($constraint->invalidInterfaceMessage, [
|
||||
'@plugin_id' => $plugin_id,
|
||||
'@interface' => $constraint->interface,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -22,6 +22,10 @@ block.block.*:
|
|||
plugin:
|
||||
type: string
|
||||
label: 'Plugin'
|
||||
constraints:
|
||||
PluginExists:
|
||||
manager: plugin.manager.block
|
||||
interface: Drupal\Core\Block\BlockPluginInterface
|
||||
settings:
|
||||
type: block.settings.[%parent.plugin]
|
||||
visibility:
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\block\Kernel;
|
||||
|
||||
use Drupal\block\Entity\Block;
|
||||
use Drupal\KernelTests\Core\Config\ConfigEntityValidationTestBase;
|
||||
|
||||
/**
|
||||
* Tests validation of block entities.
|
||||
*
|
||||
* @group block
|
||||
*/
|
||||
class BlockValidationTest extends ConfigEntityValidationTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['block'];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->entity = Block::create([
|
||||
'id' => 'test_block',
|
||||
'theme' => 'stark',
|
||||
'plugin' => 'system_powered_by_block',
|
||||
]);
|
||||
$this->entity->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests validating a block with an unknown plugin ID.
|
||||
*/
|
||||
public function testInvalidPluginId(): void {
|
||||
$this->entity->set('plugin', 'non_existent');
|
||||
$this->assertValidationErrors(["The 'non_existent' plugin does not exist."]);
|
||||
}
|
||||
|
||||
}
|
|
@ -10,6 +10,10 @@ editor.editor.*:
|
|||
editor:
|
||||
type: string
|
||||
label: 'Text editor'
|
||||
constraints:
|
||||
PluginExists:
|
||||
manager: plugin.manager.editor
|
||||
interface: Drupal\editor\Plugin\EditorPluginInterface
|
||||
settings:
|
||||
type: editor.settings.[%parent.editor]
|
||||
image_upload:
|
||||
|
|
|
@ -62,4 +62,12 @@ class EditorValidationTest extends ConfigEntityValidationTestBase {
|
|||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests validating an editor with an unknown plugin ID.
|
||||
*/
|
||||
public function testInvalidPluginId(): void {
|
||||
$this->entity->setEditor('non_existent');
|
||||
$this->assertValidationErrors(["The 'non_existent' plugin does not exist."]);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\KernelTests\Core\Plugin;
|
||||
|
||||
use Drupal\Core\Action\ActionInterface;
|
||||
use Drupal\Core\TypedData\DataDefinition;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\system\MenuInterface;
|
||||
|
||||
/**
|
||||
* @group Plugin
|
||||
* @group Validation
|
||||
*
|
||||
* @covers \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint
|
||||
* @covers \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraintValidator
|
||||
*/
|
||||
class PluginExistsConstraintValidatorTest extends KernelTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = ['action_test', 'system'];
|
||||
|
||||
/**
|
||||
* Tests validation of plugin existence.
|
||||
*/
|
||||
public function testValidation(): void {
|
||||
$definition = DataDefinition::create('string')
|
||||
->addConstraint('PluginExists', 'plugin.manager.action');
|
||||
|
||||
// An existing action plugin should pass validation.
|
||||
$data = $this->container->get('typed_data_manager')->create($definition);
|
||||
$data->setValue('action_test_save_entity');
|
||||
$this->assertCount(0, $data->validate());
|
||||
|
||||
// It should also pass validation if we check for an interface it actually
|
||||
// implements.
|
||||
$definition->setConstraints([
|
||||
'PluginExists' => [
|
||||
'manager' => 'plugin.manager.action',
|
||||
'interface' => ActionInterface::class,
|
||||
],
|
||||
]);
|
||||
$this->assertCount(0, $data->validate());
|
||||
|
||||
// A non-existent plugin should be invalid, regardless of interface.
|
||||
$data->setValue('non_existent_plugin');
|
||||
$violations = $data->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame("The 'non_existent_plugin' plugin does not exist.", (string) $violations->get(0)->getMessage());
|
||||
|
||||
// An existing plugin that doesn't implement the specified interface should
|
||||
// raise an error.
|
||||
$definition->setConstraints([
|
||||
'PluginExists' => [
|
||||
'manager' => 'plugin.manager.action',
|
||||
'interface' => MenuInterface::class,
|
||||
],
|
||||
]);
|
||||
$data->setValue('action_test_save_entity');
|
||||
$violations = $data->validate();
|
||||
$this->assertCount(1, $violations);
|
||||
$this->assertSame("The 'action_test_save_entity' plugin must implement or extend " . MenuInterface::class . '.', (string) $violations->get(0)->getMessage());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\Core\Plugin;
|
||||
|
||||
use Drupal\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint;
|
||||
use Drupal\Tests\UnitTestCase;
|
||||
use Symfony\Component\Validator\Exception\MissingOptionsException;
|
||||
|
||||
/**
|
||||
* @group Plugin
|
||||
* @group Validation
|
||||
*
|
||||
* @coversDefaultClass \Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint
|
||||
*/
|
||||
class PluginExistsConstraintTest extends UnitTestCase {
|
||||
|
||||
/**
|
||||
* Tests missing option.
|
||||
*
|
||||
* @covers ::create
|
||||
*/
|
||||
public function testMissingOption(): void {
|
||||
$this->expectException(MissingOptionsException::class);
|
||||
$this->expectExceptionMessage('The option "manager" must be set for constraint "Drupal\Core\Plugin\Plugin\Validation\Constraint\PluginExistsConstraint".');
|
||||
$container = $this->createMock(ContainerInterface::class);
|
||||
PluginExistsConstraint::create($container, [], 'test_plugin_id', []);
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests with different option keys.
|
||||
*
|
||||
* @testWith ["value"]
|
||||
* ["manager"]
|
||||
*
|
||||
* @covers ::create
|
||||
* @covers ::__construct
|
||||
*/
|
||||
public function testOption(string $option_key): void {
|
||||
$container = $this->createMock(ContainerInterface::class);
|
||||
$manager = $this->createMock(PluginManagerInterface::class);
|
||||
$container->expects($this->any())
|
||||
->method('get')
|
||||
->with('plugin.manager.mock')
|
||||
->willReturn($manager);
|
||||
$constraint = PluginExistsConstraint::create($container, [$option_key => 'plugin.manager.mock'], 'test_plugin_id', []);
|
||||
$this->assertSame($manager, $constraint->pluginManager);
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in New Issue