diff --git a/entity_browser.api.php b/entity_browser.api.php index 8231aea..230eefc 100644 --- a/entity_browser.api.php +++ b/entity_browser.api.php @@ -62,5 +62,16 @@ function hook_entity_browser_field_widget_display_info_alter(&$field_displays) { } /** + * Alter the information provided in \Drupal\entity_browser\Annotation\EntityBrowserWidgetValidation. + * + * @param $validation_plugins + * The array of widget validation plugins, keyed on the machine-readable + * name. + */ +function hook_entity_browser_widget_validation_info_alter(&$validation_plugins) { + $field_displays['not_null']['label'] = t('Not null fabulous validator'); +} + +/** * @} End of "addtogroup hooks". */ diff --git a/entity_browser.services.yml b/entity_browser.services.yml index 88fedfd..4c27024 100644 --- a/entity_browser.services.yml +++ b/entity_browser.services.yml @@ -14,6 +14,9 @@ services: plugin.manager.entity_browser.field_widget_display: class: Drupal\entity_browser\FieldWidgetDisplayManager parent: default_plugin_manager + plugin.manager.entity_browser.widget_validation: + class: Drupal\entity_browser\WidgetValidationManager + parent: default_plugin_manager entity_browser.route_subscriber: class: Drupal\entity_browser\RouteSubscriber arguments: ['@entity.manager', '@plugin.manager.entity_browser.display', '@entity.query'] diff --git a/src/Annotation/EntityBrowserWidgetValidation.php b/src/Annotation/EntityBrowserWidgetValidation.php new file mode 100644 index 0000000..adf1588 --- /dev/null +++ b/src/Annotation/EntityBrowserWidgetValidation.php @@ -0,0 +1,50 @@ +configuration += $this->defaultConfiguration(); $this->eventDispatcher = $event_dispatcher; $this->uuidGenerator = $uuid_generator; + $this->keyValue = $key_value; } /** @@ -80,7 +91,8 @@ abstract class DisplayBase extends PluginBase implements DisplayInterface, Conta $plugin_id, $plugin_definition, $container->get('event_dispatcher'), - $container->get('uuid') + $container->get('uuid'), + $container->get('keyvalue.expirable')->get('entity_browser') ); } @@ -139,4 +151,24 @@ abstract class DisplayBase extends PluginBase implements DisplayInterface, Conta $this->uuid = $uuid; } + /** + * {@inheritdoc} + */ + public function setValidators(array $validators) { + // Generate the hash that we use as key for the key/value. + $hash = md5(serialize($validators)); + + if (!$this->keyValue->has($hash)) { + $this->keyValue->set($hash, $validators); + } + return $hash; + } + + /** + * {@inheritdoc} + */ + public function getValidators($hash) { + return $this->keyValue->get($hash, []); + } + } diff --git a/src/DisplayInterface.php b/src/DisplayInterface.php index a73f443..e0238d2 100644 --- a/src/DisplayInterface.php +++ b/src/DisplayInterface.php @@ -36,11 +36,14 @@ interface DisplayInterface extends PluginInspectionInterface, ConfigurablePlugin * * @param \Drupal\Core\Form\FormStateInterface $form_state * The form state object. + * @param string $validators + * (optional) Validators hash identifier as returned by + * EntityReference::prepareValidators(). * * @return array * An array suitable for drupal_render(). */ - public function displayEntityBrowser(FormStateInterface $form_state); + public function displayEntityBrowser(FormStateInterface $form_state, $validators = ''); /** * Indicates completed selection. @@ -69,4 +72,29 @@ interface DisplayInterface extends PluginInspectionInterface, ConfigurablePlugin */ public function setUuid($uuid); + /** + * Set validators. + * + * Saves Entity Browser Widget validators in key/value storage if an identical + * set of constraints is not already stored there. + * + * @param array $validators + * An array where keys are validator ids and values configurations for them. + * + * @return string + * The hash generated from hashing the validators array. + */ + public function setValidators(array $validators); + + /** + * Get validators. + * + * @param $hash + * The hash generated from hashing the validators array. + * + * @return mixed + * An array where keys are validator ids and values configurations for them + * or empty array if no validators are stored. + */ + public function getValidators($hash); } diff --git a/src/Plugin/EntityBrowser/Display/IFrame.php b/src/Plugin/EntityBrowser/Display/IFrame.php index 9eed34f..070659d 100644 --- a/src/Plugin/EntityBrowser/Display/IFrame.php +++ b/src/Plugin/EntityBrowser/Display/IFrame.php @@ -5,6 +5,7 @@ namespace Drupal\entity_browser\Plugin\EntityBrowser\Display; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\entity_browser\DisplayBase; @@ -72,9 +73,11 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { * Current request. * @param \Drupal\Core\Path\CurrentPathStack $current_path * The current path. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value + * The key value store. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, RouteMatchInterface $current_route_match, Request $request, CurrentPathStack $current_path) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, RouteMatchInterface $current_route_match, Request $request, CurrentPathStack $current_path, KeyValueStoreExpirableInterface $key_value) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid, $key_value); $this->currentRouteMatch = $current_route_match; $this->request = $request; $this->currentPath = $current_path; @@ -92,7 +95,8 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { $container->get('uuid'), $container->get('current_route_match'), $container->get('request_stack')->getCurrentRequest(), - $container->get('path.current') + $container->get('path.current'), + $container->get('keyvalue.expirable')->get('entity_browser') ); } @@ -111,7 +115,7 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state) { + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '') { $uuid = $this->getUuid(); /** @var \Drupal\entity_browser\Events\RegisterJSCallbacks $event */ $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $uuid); @@ -123,6 +127,7 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { 'query' => [ 'uuid' => $uuid, 'original_path' => $original_path, + 'validators' => $validators, ], ], 'attributes' => [ diff --git a/src/Plugin/EntityBrowser/Display/Modal.php b/src/Plugin/EntityBrowser/Display/Modal.php index 433cf27..6941d94 100644 --- a/src/Plugin/EntityBrowser/Display/Modal.php +++ b/src/Plugin/EntityBrowser/Display/Modal.php @@ -7,6 +7,7 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Component\Uuid\UuidInterface; use Drupal\Core\Ajax\OpenDialogCommand; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Url; use Drupal\entity_browser\DisplayBase; @@ -73,13 +74,15 @@ class Modal extends DisplayBase implements DisplayRouterInterface { * UUID generator interface. * @param \Drupal\Core\Routing\RouteMatchInterface * The currently active route match object. - * @param \Symfony\Component\HttpFoundation\Request $request - * Current request. * @param \Drupal\Core\Path\CurrentPathStack $current_path * The current path. + * @param \Symfony\Component\HttpFoundation\Request $request + * Current request. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value + * The key value store. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, RouteMatchInterface $current_route_match, CurrentPathStack $current_path, Request $request) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, UuidInterface $uuid, RouteMatchInterface $current_route_match, CurrentPathStack $current_path, Request $request, KeyValueStoreExpirableInterface $key_value) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $uuid, $key_value); $this->currentRouteMatch = $current_route_match; $this->currentPath = $current_path; $this->request = $request; @@ -97,7 +100,8 @@ class Modal extends DisplayBase implements DisplayRouterInterface { $container->get('uuid'), $container->get('current_route_match'), $container->get('path.current'), - $container->get('request_stack')->getCurrentRequest() + $container->get('request_stack')->getCurrentRequest(), + $container->get('keyvalue.expirable')->get('entity_browser') ); } @@ -115,7 +119,7 @@ class Modal extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state) { + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '') { $uuid = $this->getUuid(); $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $uuid); $js_event_object->registerCallback('Drupal.entityBrowser.selectionCompleted'); @@ -126,6 +130,7 @@ class Modal extends DisplayBase implements DisplayRouterInterface { 'query' => [ 'uuid' => $uuid, 'original_path' => $original_path, + 'validators' => $validators, ], ], 'attributes' => [ diff --git a/src/Plugin/EntityBrowser/Display/Standalone.php b/src/Plugin/EntityBrowser/Display/Standalone.php index 9b8fbc6..949fc5e 100644 --- a/src/Plugin/EntityBrowser/Display/Standalone.php +++ b/src/Plugin/EntityBrowser/Display/Standalone.php @@ -45,7 +45,7 @@ class Standalone extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state) { + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '') { // @TODO Implement it. } diff --git a/src/Plugin/EntityBrowser/Widget/Upload.php b/src/Plugin/EntityBrowser/Widget/Upload.php index eff51b2..59c7441 100644 --- a/src/Plugin/EntityBrowser/Widget/Upload.php +++ b/src/Plugin/EntityBrowser/Widget/Upload.php @@ -6,10 +6,13 @@ use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Utility\Token; use Drupal\entity_browser\WidgetBase; +use Drupal\entity_browser\WidgetValidationManager; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; /** * Uses a view to provide entity listing in a browser's widget. @@ -37,7 +40,7 @@ class Upload extends WidgetBase { protected $token; /** - * Constructs upload plugin. + * Upload constructor. * * @param array $configuration * A configuration array containing information about the plugin instance. @@ -48,14 +51,20 @@ class Upload extends WidgetBase { * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * Event dispatcher service. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager - * The entity manager. + * Entity manager service. + * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager + * The Widget Validation Manager service. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value + * The Expirable key value store. + * @param \Symfony\Component\HttpFoundation\Request $request + * The Request object. * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler. * @param \Drupal\Core\Utility\Token $token * The token service. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager, ModuleHandlerInterface $module_handler, Token $token) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_manager); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager, WidgetValidationManager $validation_manager, KeyValueStoreExpirableInterface $key_value, Request $request, ModuleHandlerInterface $module_handler, Token $token) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_manager, $validation_manager, $key_value, $request); $this->moduleHandler = $module_handler; $this->token = $token; } @@ -70,6 +79,9 @@ class Upload extends WidgetBase { $plugin_definition, $container->get('event_dispatcher'), $container->get('entity.manager'), + $container->get('plugin.manager.entity_browser.widget_validation'), + $container->get('keyvalue.expirable')->get('entity_browser'), + $container->get('request_stack')->getCurrentRequest(), $container->get('module_handler'), $container->get('token') ); @@ -103,13 +115,12 @@ class Upload extends WidgetBase { /** * {@inheritdoc} */ - public function validate(array &$form, FormStateInterface $form_state) { - $uploaded_files = $form_state->getValue(['upload'], []); - $trigger = $form_state->getTriggeringElement(); - // Only validate if we are uploading a file. - if (empty($uploaded_files) && $trigger['#value'] == 'Upload') { - $form_state->setError($form['widget']['upload'], t('At least one file should be uploaded.')); + public function prepareEntities(FormStateInterface $form_state) { + $files = []; + foreach ($form_state->getValue(['upload'], []) as $fid) { + $files[] = $this->entityManager->getStorage('file')->load($fid); } + return $files; } /** diff --git a/src/Plugin/EntityBrowser/Widget/View.php b/src/Plugin/EntityBrowser/Widget/View.php index 6e86429..b9fb8a6 100644 --- a/src/Plugin/EntityBrowser/Widget/View.php +++ b/src/Plugin/EntityBrowser/Widget/View.php @@ -4,15 +4,18 @@ namespace Drupal\entity_browser\Plugin\EntityBrowser\Widget; use Drupal\Component\Plugin\Exception\PluginNotFoundException; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Render\Element; use Drupal\entity_browser\WidgetBase; use Drupal\Core\Url; +use Drupal\entity_browser\WidgetValidationManager; use Drupal\views\Views; use Symfony\Component\DependencyInjection\ContainerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Drupal\Core\Entity\EntityManagerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Uses a view to provide entity listing in a browser's widget. @@ -53,12 +56,15 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { $plugin_definition, $container->get('event_dispatcher'), $container->get('entity.manager'), + $container->get('plugin.manager.entity_browser.widget_validation'), + $container->get('keyvalue.expirable')->get('entity_browser'), + $container->get('request_stack')->getCurrentRequest(), $container->get('current_user') ); } /** - * Constructs a new View object. + * View constructor. * * @param array $configuration * A configuration array containing information about the plugin instance. @@ -68,12 +74,20 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { * The plugin implementation definition. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * Event dispatcher service. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * Entity manager service. + * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager + * The Widget Validation Manager service. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value + * The Expirable key value store. + * @param \Symfony\Component\HttpFoundation\Request $request + * The Request object. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager, AccountInterface $current_user) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_manager); - $this->currentUser = $current_user; + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager, WidgetValidationManager $validation_manager, KeyValueStoreExpirableInterface $key_value, Request $request, AccountInterface $current_user) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_manager, $validation_manager, $key_value, $request); + $this->currentUser = $current_user; } /** diff --git a/src/Plugin/EntityBrowser/WidgetValidation/Cardinality.php b/src/Plugin/EntityBrowser/WidgetValidation/Cardinality.php new file mode 100644 index 0000000..92d5dd7 --- /dev/null +++ b/src/Plugin/EntityBrowser/WidgetValidation/Cardinality.php @@ -0,0 +1,45 @@ + $max) { + $message = 'You can not select more than %max Entities.'; + $parameters = ['%max' => $max]; + $violation = new ConstraintViolation($this->t($message, $parameters), $message, $parameters, $count, '', $count); + $violations->add($violation); + } + + return $violations; + } + +} diff --git a/src/Plugin/EntityBrowser/WidgetValidation/EntityType.php b/src/Plugin/EntityBrowser/WidgetValidation/EntityType.php new file mode 100644 index 0000000..06d6171 --- /dev/null +++ b/src/Plugin/EntityBrowser/WidgetValidation/EntityType.php @@ -0,0 +1,22 @@ +entityManager = $entity_manager; $this->fieldDisplayManager = $field_display_manager; + $this->keyValue = $key_value; } /** @@ -96,7 +105,8 @@ class EntityReference extends WidgetBase implements ContainerFactoryPluginInterf $configuration['third_party_settings'], $container->get('entity.manager'), $container->get('event_dispatcher'), - $container->get('plugin.manager.entity_browser.field_widget_display') + $container->get('plugin.manager.entity_browser.field_widget_display'), + $container->get('keyvalue.expirable')->get('entity_browser') ); } @@ -375,16 +385,24 @@ class EntityReference extends WidgetBase implements ContainerFactoryPluginInterf ], ]; + // Gather and set validators. + // @todo Is there a better place to do that? $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); + + $validators = $this->prepareValidators([ + 'entity_type' => ['type' => $entity_type], + 'cardinality' => ['cardinality' => $cardinality], + ]); + if ($cardinality == FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED || count($ids) < $cardinality) { $entity_browser_uuid = sha1(implode('-', array_merge($form['#parents'], [$this->fieldDefinition->getName(), $delta]))); $entity_browser_display = $entity_browser->getDisplay(); $entity_browser_display->setUuid($entity_browser_uuid); - $element['entity_browser'] = $entity_browser_display->displayEntityBrowser($form_state); + $element['entity_browser'] = $entity_browser_display->displayEntityBrowser($form_state, $validators); $element['#attached']['library'][] = 'entity_browser/entity_reference'; $element['#attached']['drupalSettings']['entity_browser'] = [ $entity_browser->getDisplay()->getUuid() => [ - 'cardinality' => $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(), + 'cardinality' => $cardinality, 'selector' => '#' . $element['target_id']['#attributes']['id'], ], ]; @@ -540,4 +558,26 @@ class EntityReference extends WidgetBase implements ContainerFactoryPluginInterf ]; } + /** + * Prepare validators. + * + * Saves Entity Browser Widget validators in key/value storage if an identical + * set of constraints is not already stored there. + * + * @param array $validators + * An array where keys are validator ids and values configurations for them. + * + * @return string + * The hash generated from hashing the validators array. + */ + protected function prepareValidators(array $validators) { + // Generate the hash that we use as key for the key/value. + $hash = md5(serialize($validators)); + + if (!$this->keyValue->has($hash)) { + $this->keyValue->set($hash, $validators); + } + + return $hash; + } } diff --git a/src/WidgetBase.php b/src/WidgetBase.php index e1d702d..f97c53f 100644 --- a/src/WidgetBase.php +++ b/src/WidgetBase.php @@ -2,6 +2,7 @@ namespace Drupal\entity_browser; +use Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface; use Drupal\Core\Plugin\PluginBase; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormStateInterface; @@ -10,6 +11,8 @@ use Drupal\entity_browser\Events\EntitySelectionEvent; use Drupal\entity_browser\Events\Events; use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; +use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\Validator\ConstraintViolationList; /** * Base class for widget plugins. @@ -60,7 +63,28 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain protected $entityManager; /** - * Constructs widget plugin. + * The Widget Validation Manager service. + * + * @var \Drupal\entity_browser\WidgetValidationManager + */ + private $validationManager; + + /** + * The Expirable key value store. + * + * @var \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface + */ + private $keyValue; + + /** + * The Request object. + * + * @var \Symfony\Component\HttpFoundation\Request + */ + private $request; + + /** + * WidgetBase constructor. * * @param array $configuration * A configuration array containing information about the plugin instance. @@ -70,12 +94,23 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain * The plugin implementation definition. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * Event dispatcher service. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * Entity manager service. + * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager + * The Widget Validation Manager service. + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $key_value + * The Expirable key value store. + * @param \Symfony\Component\HttpFoundation\Request $request + * The Request object. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityManagerInterface $entity_manager, WidgetValidationManager $validation_manager, KeyValueStoreExpirableInterface $key_value, Request $request) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->eventDispatcher = $event_dispatcher; $this->entityManager = $entity_manager; + $this->validationManager = $validation_manager; $this->setConfiguration($configuration); + $this->keyValue = $key_value; + $this->request = $request; } /** @@ -87,7 +122,10 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain $plugin_id, $plugin_definition, $container->get('event_dispatcher'), - $container->get('entity.manager') + $container->get('entity.manager'), + $container->get('plugin.manager.entity_browser.widget_validation'), + $container->get('keyvalue.expirable')->get('entity_browser'), + $container->get('request_stack')->getCurrentRequest() ); } @@ -187,7 +225,51 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain /** * {@inheritdoc} */ - public function validate(array &$form, FormStateInterface $form_state) {} + public function prepareEntities(FormStateInterface $form_state) { + return []; + } + + /** + * {@inheritdoc} + */ + public function validate(array &$form, FormStateInterface $form_state) { + $trigger = $form_state->getTriggeringElement(); + + if (in_array('submit', $trigger['#array_parents'])) { + $entities = $this->prepareEntities($form_state); + $violations = $this->runWidgetValidators($entities); + if (!empty($violations)) { + foreach ($violations as $violation) { + $form_state->setError($form['widget'], $violation->getMessage()); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function runWidgetValidators(array $entities, $validators = []) { + // @todo Implement a centralized way to get arguments from path for all widgets? + if ($validators_hash = $this->request->get('validators')) { + $passed_validators = $this->keyValue->get($validators_hash); + + if (!empty($passed_validators)) { + $validators += $passed_validators; + } + } + + $violations = new ConstraintViolationList(); + foreach ($validators as $validator_id => $options) { + /** @var \Drupal\entity_browser\WidgetValidationInterface $widget_validator */ + $widget_validator = $this->validationManager->createInstance($validator_id, []); + if ($widget_validator) { + $violations->addAll($widget_validator->validate($entities, $options)); + } + } + + return $violations; + } /** * {@inheritdoc} diff --git a/src/WidgetInterface.php b/src/WidgetInterface.php index ddd3763..0146e7f 100644 --- a/src/WidgetInterface.php +++ b/src/WidgetInterface.php @@ -88,6 +88,20 @@ interface WidgetInterface extends PluginInspectionInterface, ConfigurablePluginI public function getForm(array &$original_form, FormStateInterface $form_state, array $aditional_widget_parameters); /** + * Prepares the entities without saving them. + * + * We need this method when we want to validation or perform other operations + * before submit. + * + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state object. + * + * @return \Drupal\Core\Entity\ContentEntityInterface[] + * Array of entities. + */ + public function prepareEntities(FormStateInterface $form_state); + + /** * Validates form. * * @param array $form @@ -98,6 +112,20 @@ interface WidgetInterface extends PluginInspectionInterface, ConfigurablePluginI public function validate(array &$form, FormStateInterface $form_state); /** + * Run widget validators. + * + * @param array $entities + * Array of entity ids to validate. + * @param array $validators + * Array of additional widget validator ids. + * + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + * A list of constraint violations. If the list is empty, validation + * succeeded. + */ + public function runWidgetValidators(array $entities, $validators = []); + + /** * Submits form. * * @param array $element diff --git a/src/WidgetValidationBase.php b/src/WidgetValidationBase.php new file mode 100644 index 0000000..f98741e --- /dev/null +++ b/src/WidgetValidationBase.php @@ -0,0 +1,141 @@ + null, + ]; + parent::__construct($configuration, $plugin_id, $plugin_definition); + $this->typedDataManager = $typed_data_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('typed_data_manager') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return []; + } + + /** + * {@inheritdoc} + */ + public function label() { + $this->label; + } + + /** + * {@inheritdoc} + */ + public function validate(array $entities, $options = []) { + $plugin_definition = $this->getPluginDefinition(); + $data_definition = $this->getDataDefinition($plugin_definition['data_type'], $plugin_definition['constraint'], $options); + return $this->validateDataDefinition($data_definition, $entities); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = $configuration; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * Gets a data definition and optionally adds a constraint. + * + * @param string $data_type + * The data type plugin ID, for which a constraint should be added. + * @param string $constraint_name + * The name of the constraint to add, i.e. its plugin id. + * @param $options + * Array of options needed by the constraint validator. + * + * @return \Drupal\Core\TypedData\DataDefinitionInterface + * A data definition object for the given data type. + */ + protected function getDataDefinition($data_type, $constraint_name = NULL, $options = []) { + $data_definition = $this->typedDataManager->createDataDefinition($data_type); + if ($constraint_name) { + $data_definition->addConstraint($constraint_name, $options); + } + return $data_definition; + } + + /** + * Creates and validates instances of typed data for each Entity. + * + * @param \Drupal\Core\TypedData\DataDefinitionInterface $data_definition + * The data definition generated from ::getDataDefinition(). + * @param array $entities + * An array of Entities to validate the definition against + * + * @return \Symfony\Component\Validator\ConstraintViolationListInterface + * A list of violations. + */ + protected function validateDataDefinition(DataDefinitionInterface $data_definition, array $entities) { + $violations = new ConstraintViolationList(); + foreach ($entities as $entity) { + $validation_result = $this->typedDataManager->create($data_definition, $entity)->validate(); + $violations->addAll($validation_result); + } + + return $violations; + } +} diff --git a/src/WidgetValidationInterface.php b/src/WidgetValidationInterface.php new file mode 100644 index 0000000..3ae0653 --- /dev/null +++ b/src/WidgetValidationInterface.php @@ -0,0 +1,40 @@ +alterInfo('entity_browser_widget_validation_info'); + $this->setCacheBackend($cache_backend, 'entity_browser_widget_validation_plugins'); + } + +}