diff --git a/core/modules/media/src/Element/MediaManagedFile.php b/core/modules/media/src/Element/MediaManagedFile.php index 07b5148..ec095e3 100644 --- a/core/modules/media/src/Element/MediaManagedFile.php +++ b/core/modules/media/src/Element/MediaManagedFile.php @@ -14,6 +14,21 @@ class MediaManagedFile extends ManagedFile { /** * {@inheritdoc} */ + public function getInfo() { + $element = parent::getInfo(); + + $class = get_class($this); + $element['#element_validate'] = [ + [$class, 'validateManagedFile'], + [$class, 'validateEntityForm'], + ]; + + return $element; + } + + /** + * {@inheritdoc} + */ public static function processManagedFile(&$element, FormStateInterface $form_state, &$complete_form) { if (!empty($element['#value']['target_id'])) { // We're editing an entity with a previously-saved Media entity @@ -46,4 +61,19 @@ public static function processManagedFile(&$element, FormStateInterface $form_st return $element; } + /** + * Validates the media form using our custom form handler. + * + * @param array $entity_form + * The entity form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public static function validateEntityForm(&$entity_form, FormStateInterface $form_state) { + $form_mode = 'add_inline'; + $inline_form_handler = \Drupal::entityTypeManager() + ->getHandler('media', $form_mode); + $inline_form_handler->entityFormValidate($entity_form, $form_state); + } + } diff --git a/core/modules/media/src/Form/MediaInlineForm.php b/core/modules/media/src/Form/MediaInlineForm.php index 8ac4d8c..c575f4b 100644 --- a/core/modules/media/src/Form/MediaInlineForm.php +++ b/core/modules/media/src/Form/MediaInlineForm.php @@ -9,6 +9,7 @@ use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\Element; use Drupal\media\MediaInlineFormInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -81,28 +82,26 @@ public static function createInstance(ContainerInterface $container, EntityTypeI */ public function entityForm(array $entity_form, FormStateInterface $form_state) { // Build the media entity form. - // @TODO Why shouldn't we assume this is a MediaInterface instead? - /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + /** @var \Drupal\media\MediaInterface $entity */ $entity = $entity_form['#entity']; - $form_display = EntityFormDisplay::collectRenderDisplay($entity, $entity_form['#form_mode']); + $form_display = $this->getFormDisplay($entity, $entity_form['#form_mode']); $form_display->buildForm($entity, $entity_form, $form_state); $entity_form['#weight'] = 100; // In the service of keeping this form as user-friendly as possible in the // context of a parent entity form, only show required fields. - // @TODO Inject the service. - /** @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager */ - $entity_field_manager = \Drupal::service('entity_field.manager'); - $field_definitions = $entity_field_manager->getFieldDefinitions('media', 'file'); + $field_definitions = $this->entityFieldManager->getFieldDefinitions('media', $entity->bundle()); /** @var \Drupal\Core\Field\BaseFieldDefinition $field_definition */ foreach ($field_definitions as $field_definition) { $field_name = $field_definition->getName(); if (!$field_definition->isRequired()) { $entity_form[$field_name]['#access'] = FALSE; } - else { - // @TODO: Why is this here? - $entity_form[$field_name]['#element_validate'] = []; + elseif (!empty($entity_form[$field_name])) { + // Elements with "#required" key set will always be validated, even if + // 'limit_validation_errors' is set. Disable their validation here, we + // will enforce validation happens inside the real submit handler. + $entity_form[$field_name] = $this->disableElementChildrenValidation($entity_form[$field_name]); } } @@ -133,4 +132,89 @@ public function entityForm(array $entity_form, FormStateInterface $form_state) { return $entity_form; } + /** + * {@inheritdoc} + */ + public function entityFormValidate(array &$entity_form, FormStateInterface $form_state) { + // Perform entity validation only if the inline form was submitted, + // skipping other requests such as file uploads. + $triggering_element = $form_state->getTriggeringElement(); + // @TODO Finish this when the media form is inside a specific container. +// if (strpos($triggering_element['#name'], 'upload_button') === FALSE && +// !empty($entity_form['#entity']) && is_a($entity_form['#entity'], '\Drupal\Core\Entity\ContentEntityInterface')) { +// /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ +// $entity = $entity_form['#entity']; +// $this->buildEntity($entity_form, $entity, $form_state); +// $form_display = $this->getFormDisplay($entity, $entity_form['#form_mode']); +// $form_display->validateFormValues($entity, $entity_form, $form_state); +// $entity->setValidationRequired(FALSE); +// +// foreach($form_state->getErrors() as $name => $message) { +// // $name may be unknown in $form_state and +// // $form_state->setErrorByName($name, $message) may suppress the error message. +// $form_state->setError($triggering_element, $message); +// } +// } + } + + /** + * Bypass validation in all children of a given element. + * + * @param array $element + * The element array. + * + * @return array + * The same element array, after recursively checking all its children and + * setting "#validated" => TRUE in all required elements. + */ + private function disableElementChildrenValidation(array $element) { + foreach (Element::children($element) as $key) { + if (isset($element[$key]) && $element[$key]) { + $element[$key] = $this->disableElementChildrenValidation($element[$key]); + } + } + + if (!empty($element['#needs_validation']) || !empty($element['#required'])) { + $element['#validated'] = TRUE; + } + + return $element; + } + + /** + * Builds an updated entity object based upon the submitted form values. + * + * @param array $entity_form + * The entity form. + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + protected function buildEntity(array $entity_form, ContentEntityInterface $entity, FormStateInterface $form_state) { + $form_display = $this->getFormDisplay($entity, $entity_form['#form_mode']); + $form_display->extractFormValues($entity, $entity_form, $form_state); + // Invoke all specified builders for copying form values to entity fields. + if (isset($entity_form['#entity_builders'])) { + foreach ($entity_form['#entity_builders'] as $function) { + call_user_func_array($function, [$entity->getEntityTypeId(), $entity, &$entity_form, &$form_state]); + } + } + } + + /** + * Gets the form display for the given entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The entity. + * @param string $form_mode + * The form mode. + * + * @return \Drupal\Core\Entity\Display\EntityFormDisplayInterface + * The form display. + */ + protected function getFormDisplay(ContentEntityInterface $entity, $form_mode) { + return EntityFormDisplay::collectRenderDisplay($entity, $form_mode); + } + } diff --git a/core/modules/media/src/MediaInlineFormInterface.php b/core/modules/media/src/MediaInlineFormInterface.php index d624e66..b65e6d4 100644 --- a/core/modules/media/src/MediaInlineFormInterface.php +++ b/core/modules/media/src/MediaInlineFormInterface.php @@ -25,4 +25,14 @@ */ public function entityForm(array $entity_form, FormStateInterface $form_state); + /** + * Validates the entity form. + * + * @param array $entity_form + * The entity form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The form state of the parent form. + */ + public function entityFormValidate(array &$entity_form, FormStateInterface $form_state); + } diff --git a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php index e103467..13827f8 100644 --- a/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php +++ b/core/modules/media/src/Plugin/Field/FieldWidget/MediaFileWidget.php @@ -2,9 +2,12 @@ namespace Drupal\media\Plugin\Field\FieldWidget; +use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Render\RendererInterface; use Drupal\file\Entity\File; use Drupal\file\Plugin\Field\FieldWidget; use Drupal\file\Plugin\Field\FieldWidget\FileWidget; @@ -14,6 +17,7 @@ use Drupal\media\Entity\MediaType; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Link; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Plugin implementation of the 'media_file' widget. @@ -29,6 +33,36 @@ class MediaFileWidget extends FileWidget { /** + * The renderer. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** + * {@inheritdoc} + */ + public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, array $third_party_settings, ElementInfoManagerInterface $element_info, RendererInterface $renderer) { + parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $third_party_settings, $element_info); + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['third_party_settings'], + $container->get('element_info'), + $container->get('renderer') + ); + } + + /** * {@inheritdoc} * * Uses nearly the same code that FileWidget does here, except that we have to @@ -101,8 +135,7 @@ public function formElement(FieldItemListInterface $items, $delta, array $elemen '#upload_validators' => $element['#upload_validators'], '#cardinality' => $cardinality, ]; - // @TODO: Inject the service. - $element['#description'] = \Drupal::service('renderer')->renderPlain($file_upload_help); + $element['#description'] = $this->renderer->renderPlain($file_upload_help); $element['#multiple'] = $cardinality != 1 ? TRUE : FALSE; if ($cardinality != 1 && $cardinality != -1) { $element['#element_validate'][] = [get_class($this), 'validateMultipleCount']; @@ -120,6 +153,7 @@ public function massageFormValues(array $values, array $form, FormStateInterface // entities instead of files. $new_values = []; foreach ($values as &$value) { + $value['mids'] = !empty($value['mids']) ? $value['mids'] : []; foreach ($value['mids'] as $mid) { $new_value = $value; $new_value['target_id'] = $mid; @@ -137,20 +171,28 @@ public function massageFormValues(array $values, array $form, FormStateInterface public static function value($element, $input, FormStateInterface $form_state) { $return = parent::value($element, $input, $form_state); + $media_type = FALSE; + $host_entity = $form_state->getFormObject()->getEntity(); + if ($host_entity && $host_entity instanceof ContentEntityInterface) { + $handler_settings = $host_entity->getFieldDefinitions()[$element['#field_name']]->getSetting('handler_settings'); + // If this field happens to point to multiple media types (bundles), just + // use the first one that implements a file source. + $media_type = !empty($handler_settings['target_bundles']) ? self::getFileMediaTypeFromSet($handler_settings['target_bundles']) : FALSE; + } + // When the form has just been submitted with file(s) being uploaded, // $input['mids'] should be empty. FileWidget::value() calls // ManagedFile::valueCallback(), which populates $return['fids']. If there's // info in there AND we're not doing the subsequent form rebuild, it's time // to make the media. - if (empty($input['mids']) && !empty($return['fids']) && !$form_state->isRebuilding()) { + if (empty($input['mids']) && !empty($return['fids']) && !$form_state->isRebuilding() && $media_type) { foreach ($return['fids'] as $fid) { $file = File::load($fid); /** @var \Drupal\media\MediaInterface $media_entity */ $media_entity = Media::create([ // @TODO: Shouldn't media entities automatically deal with empty names? 'name' => $file->getFilename(), - 'bundle' => 'file', // TODO: generalize (note that we have to apply this to MediaInlineFileWidgetTest::setUp() too, in createdMediaType() call) - // @TODO: Inject the service. + 'bundle' => $media_type, 'uid' => \Drupal::currentUser()->id(), // TODO: figure out langcode in this context. // 'langcode' => !empty($element['#langcode']) ? $element['#langcode'] : LanguageInterface::LANGCODE_DEFAULT, @@ -246,7 +288,6 @@ public static function process($element, FormStateInterface $form_state, $form) '#form_mode' => $form_mode, '#parents' => $form_element_parents, ]; - // @TODO: Inject the service. $inline_form_handler = \Drupal::entityTypeManager() ->getHandler('media', $form_mode); $media_form = $inline_form_handler->entityForm($media_form, $form_state); @@ -321,7 +362,56 @@ public static function submit($form, FormStateInterface $form_state) { * {@inheritdoc} */ public static function isApplicable(FieldDefinitionInterface $field_definition) { - return parent::isApplicable($field_definition); - // @TODO Restrict this to only fields targeting a single media type. + // This widget is restricted to entity_reference fields pointing to media + // entities. + if ($field_definition->getSetting('target_type') !== 'media') { + return FALSE; + } + + // This widget can be chosen only if the field points to a single target + // Media Type, and this type has a source of type "File". + $handler_settings = $field_definition->getSetting('handler_settings'); + if (empty($handler_settings['target_bundles']) || count($handler_settings['target_bundles']) !== 1) { + return FALSE; + } + $type_name = reset($handler_settings['target_bundles']); + $media_type = MediaType::load($type_name); + if ($media_type) { + $source = $media_type->getSource(); + if (is_a($source, '\Drupal\media\Plugin\media\Source\File')) { + return TRUE; + } + } + + return FALSE; } + + /** + * Given a set of media types, fetch the first one with a File source. + * + * @param array $types + * An array where the values are media type IDs. + * + * @return bool|string + * The ID of the first type found (from the set passed in) with a source + * plugin that implements + * \Drupal\media\Plugin\media\Source\File, or FALSE if none found. + */ + public static function getFileMediaTypeFromSet(array $types) { + $media_types = MediaType::loadMultiple($types); + if (!$media_types) { + return FALSE; + } + + foreach ($media_types as $media_type) { + /** @var \Drupal\media\MediaTypeInterface $media_type */ + $source = $media_type->getSource(); + if (is_a($source, '\Drupal\media\Plugin\media\Source\File')) { + return $media_type->id(); + } + } + + return FALSE; + } + } diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php index 1e448e2..63bbb79 100644 --- a/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaInlineFileWidgetTest.php @@ -217,6 +217,7 @@ public function testInlineFileWidget() { $required_text2 = $this->randomMachineName(); $page->fillField('field_media_required', $required_text2); $page->pressButton('Save'); + // Check everything was saved correctly. $assert_session->pageTextContains("{$this->nodeTypeId} $node_title has been updated"); $assert_session->pageTextContains($filename1);