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 3c8aa09..769092d 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..454f27d --- /dev/null +++ b/src/Annotation/EntityBrowserWidgetValidation.php @@ -0,0 +1,50 @@ +selectionStorage->setWithExpire($this->getUuid(), $entities, Settings::get('entity_browser_expire', 21600)); - } + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '', array $entities = []) { + // Store our validators and currently selected entities so that after being + // rendered they can be accessed. + $this->setStorage([ + 'entities' => $entities, + 'validators' => $validators + ]); + } + + /** + * {@inheritdoc} + */ + public function setStorage($value) { + $key = $this->getUuid(); + $this->selectionStorage->setWithExpire($key, $value, Settings::get('entity_browser_expire', 21600)); + + return $key; + } + + /** + * {@inheritdoc} + */ + public function getStorage($key) { + return $this->selectionStorage->get($key, [ + 'entities' => [], + 'validators' => '' + ]); } } diff --git a/src/DisplayInterface.php b/src/DisplayInterface.php index 7513852..85b251c 100644 --- a/src/DisplayInterface.php +++ b/src/DisplayInterface.php @@ -36,13 +36,16 @@ 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(). * @param \Drupal\Core\Entity\EntityInterface[] $entities * (optional) Existing selection that should be passed to the entity browser. * * @return array * An array suitable for drupal_render(). */ - public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []); + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '', array $entities = []); /** * Indicates completed selection. @@ -71,4 +74,23 @@ interface DisplayInterface extends PluginInspectionInterface, ConfigurablePlugin */ public function setUuid($uuid); + /** + * Sets the selection storage. + * + * @param mixed $value + * The storage value to set. Typically this is an associative array + * containing the keys 'validators' and 'entities'. + * + * @return string + * The key generated when setting the storage. + */ + public function setStorage($value); + + /** + * Gets the selection storage. + * + * @param string $key + * The key (usually UUID) to use when loading the selection storage. + */ + public function getStorage($key); } diff --git a/src/Form/EntityBrowserForm.php b/src/Form/EntityBrowserForm.php index ae3a12a..304773b 100644 --- a/src/Form/EntityBrowserForm.php +++ b/src/Form/EntityBrowserForm.php @@ -91,8 +91,9 @@ class EntityBrowserForm extends FormBase implements EntityBrowserFormInterface { $form_state->set(['entity_browser', 'selection_completed'], FALSE); // Initialize select from the query parameter. - if ($selection = $this->selectionStorage->get($form_state->get(['entity_browser', 'instance_uuid']))) { - $form_state->set(['entity_browser', 'selected_entities'], $selection); + if ($storage = $this->selectionStorage->get($form_state->get(['entity_browser', 'instance_uuid']))) { + $form_state->set(['entity_browser', 'selected_entities'], $storage['entities']); + $form_state->set(['entity_browser', 'validators'], $storage['validators']); } } diff --git a/src/Plugin/EntityBrowser/Display/IFrame.php b/src/Plugin/EntityBrowser/Display/IFrame.php index c3edcba..19a7571 100644 --- a/src/Plugin/EntityBrowser/Display/IFrame.php +++ b/src/Plugin/EntityBrowser/Display/IFrame.php @@ -115,8 +115,8 @@ class IFrame extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { - parent::displayEntityBrowser($form_state, $entities); + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '', array $entities = []) { + parent::displayEntityBrowser($form_state, $validators, $entities); /** @var \Drupal\entity_browser\Events\RegisterJSCallbacks $event */ $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $this->getUuid()); $js_event_object->registerCallback('Drupal.entityBrowser.selectionCompleted'); diff --git a/src/Plugin/EntityBrowser/Display/Modal.php b/src/Plugin/EntityBrowser/Display/Modal.php index aa9c8b2..9de158c 100644 --- a/src/Plugin/EntityBrowser/Display/Modal.php +++ b/src/Plugin/EntityBrowser/Display/Modal.php @@ -72,7 +72,7 @@ class Modal extends DisplayBase implements DisplayRouterInterface { * Event dispatcher service. * @param \Drupal\Component\Uuid\UuidInterface * UUID generator interface. - * @parem \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage + * @param \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage * The selection storage. * @param \Drupal\Core\Routing\RouteMatchInterface $current_route_match * The currently active route match object. @@ -119,8 +119,8 @@ class Modal extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { - parent::displayEntityBrowser($form_state, $entities); + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '', array $entities = []) { + parent::displayEntityBrowser($form_state, $validators, $entities); $js_event_object = new RegisterJSCallbacks($this->configuration['entity_browser_id'], $this->getUuid()); $js_event_object->registerCallback('Drupal.entityBrowser.selectionCompleted'); $js_event = $this->eventDispatcher->dispatch(Events::REGISTER_JS_CALLBACKS, $js_event_object); diff --git a/src/Plugin/EntityBrowser/Display/Standalone.php b/src/Plugin/EntityBrowser/Display/Standalone.php index e7c8eb9..7708acb 100644 --- a/src/Plugin/EntityBrowser/Display/Standalone.php +++ b/src/Plugin/EntityBrowser/Display/Standalone.php @@ -45,8 +45,8 @@ class Standalone extends DisplayBase implements DisplayRouterInterface { /** * {@inheritdoc} */ - public function displayEntityBrowser(FormStateInterface $form_state, array $entities = []) { - parent::displayEntityBrowser($form_state, $entities); + public function displayEntityBrowser(FormStateInterface $form_state, $validators = '', array $entities = []) { + parent::displayEntityBrowser($form_state, $validators, $entities); // @TODO Implement it. } diff --git a/src/Plugin/EntityBrowser/Widget/Upload.php b/src/Plugin/EntityBrowser/Widget/Upload.php index f34353e..85850e5 100644 --- a/src/Plugin/EntityBrowser/Widget/Upload.php +++ b/src/Plugin/EntityBrowser/Widget/Upload.php @@ -8,6 +8,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; 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; @@ -37,7 +38,7 @@ class Upload extends WidgetBase { protected $token; /** - * Constructs upload plugin. + * Upload constructor. * * @param array $configuration * A configuration array containing information about the plugin instance. @@ -49,13 +50,15 @@ class Upload extends WidgetBase { * Event dispatcher service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. + * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager + * The Widget Validation Manager service. * @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, EntityTypeManagerInterface $entity_type_manager, ModuleHandlerInterface $module_handler, Token $token) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, WidgetValidationManager $validation_manager, ModuleHandlerInterface $module_handler, Token $token) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager, $validation_manager); $this->moduleHandler = $module_handler; $this->token = $token; } @@ -70,6 +73,7 @@ class Upload extends WidgetBase { $plugin_definition, $container->get('event_dispatcher'), $container->get('entity_type.manager'), + $container->get('plugin.manager.entity_browser.widget_validation'), $container->get('module_handler'), $container->get('token') ); @@ -87,7 +91,7 @@ class Upload extends WidgetBase { /** * {@inheritdoc} */ - public function getForm(array &$original_form, FormStateInterface $form_state, array $aditional_widget_parameters) { + public function getForm(array &$original_form, FormStateInterface $form_state, array $additional_widget_parameters) { $form = []; $form['upload'] = [ '#type' => 'managed_file', @@ -103,13 +107,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->entityTypeManager->getStorage('file')->load($fid); } + return $files; } /** diff --git a/src/Plugin/EntityBrowser/Widget/View.php b/src/Plugin/EntityBrowser/Widget/View.php index 5e1e730..07a5389 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\EntityTypeManagerInterface; +use Symfony\Component\HttpFoundation\Request; /** * Uses a view to provide entity listing in a browser's widget. @@ -53,6 +56,7 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { $plugin_definition, $container->get('event_dispatcher'), $container->get('entity_type.manager'), + $container->get('plugin.manager.entity_browser.widget_validation'), $container->get('current_user') ); } @@ -70,18 +74,20 @@ class View extends WidgetBase implements ContainerFactoryPluginInterface { * Event dispatcher service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager. + * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager + * The Widget Validation Manager service. * @param \Drupal\Core\Session\AccountInterface $current_user * The current user. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, AccountInterface $current_user) { - parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager); + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, WidgetValidationManager $validation_manager, AccountInterface $current_user) { + parent::__construct($configuration, $plugin_id, $plugin_definition, $event_dispatcher, $entity_type_manager, $validation_manager); $this->currentUser = $current_user; } /** * {@inheritdoc} */ - public function getForm(array &$original_form, FormStateInterface $form_state, array $aditional_widget_parameters) { + public function getForm(array &$original_form, FormStateInterface $form_state, array $additional_widget_parameters) { $form = []; // TODO - do we need better error handling for view and view_display (in case // either of those is nonexistent or display not of correct type)? 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 @@ +getDisplay(); $entity_browser_display->setUuid($entity_browser_uuid); - $element['entity_browser'] = $entity_browser_display->displayEntityBrowser($form_state, []); + // Gather and set validators. + $validators = [ + 'entity_type' => ['type' => $entity_type], + 'cardinality' => ['cardinality' => $cardinality], + ]; + + $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'], ], ]; @@ -541,5 +546,4 @@ class EntityReference extends WidgetBase implements ContainerFactoryPluginInterf ), ]; } - } diff --git a/src/WidgetBase.php b/src/WidgetBase.php index 254dce4..e90575a 100644 --- a/src/WidgetBase.php +++ b/src/WidgetBase.php @@ -10,6 +10,7 @@ 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\Validator\ConstraintViolationList; /** * Base class for widget plugins. @@ -60,7 +61,14 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain protected $entityTypeManager; /** - * Constructs widget plugin. + * The Widget Validation Manager service. + * + * @var \Drupal\entity_browser\WidgetValidationManager + */ + private $validationManager; + + /** + * WidgetBase constructor. * * @param array $configuration * A configuration array containing information about the plugin instance. @@ -72,11 +80,16 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain * Event dispatcher service. * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager * The entity type manager service. + * @param \Drupal\entity_browser\WidgetValidationManager $validation_manager + * The Widget Validation Manager service. + * @parem \Drupal\Core\KeyValueStore\KeyValueStoreExpirableInterface $selection_storage + * The selection storage. */ - public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager) { + public function __construct(array $configuration, $plugin_id, $plugin_definition, EventDispatcherInterface $event_dispatcher, EntityTypeManagerInterface $entity_type_manager, WidgetValidationManager $validation_manager) { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->eventDispatcher = $event_dispatcher; $this->entityTypeManager = $entity_type_manager; + $this->validationManager = $validation_manager; $this->setConfiguration($configuration); } @@ -89,7 +102,8 @@ abstract class WidgetBase extends PluginBase implements WidgetInterface, Contain $plugin_id, $plugin_definition, $container->get('event_dispatcher'), - $container->get('entity_type.manager') + $container->get('entity_type.manager'), + $container->get('plugin.manager.entity_browser.widget_validation') ); } @@ -189,7 +203,43 @@ 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); + $validators = &$form_state->get(['entity_browser', 'validators']); + $violations = $this->runWidgetValidators($entities, $validators); + if (!empty($violations)) { + foreach ($violations as $violation) { + $form_state->setError($form['widget'], $violation->getMessage()); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function runWidgetValidators(array $entities, $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..e0aa763 100644 --- a/src/WidgetInterface.php +++ b/src/WidgetInterface.php @@ -79,13 +79,27 @@ interface WidgetInterface extends PluginInspectionInterface, ConfigurablePluginI * unit. * @param \Drupal\Core\Form\FormStateInterface $form_state * Form state object. - * @param array $aditional_widget_parameters + * @param array $additional_widget_parameters * Additional parameters that we want to pass to the widget. * * @return array * Form structure. */ - public function getForm(array &$original_form, FormStateInterface $form_state, array $aditional_widget_parameters); + public function getForm(array &$original_form, FormStateInterface $form_state, array $additional_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. @@ -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'); + } + +}