diff --git a/config/schema/media_entity.schema.yml b/config/schema/media_entity.schema.yml index 1c1f720..2d88660 100644 --- a/config/schema/media_entity.schema.yml +++ b/config/schema/media_entity.schema.yml @@ -67,3 +67,13 @@ field.formatter.settings.media_thumbnail: image_style: type: string label: 'Image style' + +media_entity.bundle.field_aware_type: + type: mapping + mapping: + source_field: + type: string + label: 'Source field' + +media_entity.bundle.type.generic: + type: media_entity.bundle.field_aware_type diff --git a/src/Annotation/MediaType.php b/src/Annotation/MediaType.php index 65a3d70..0d43633 100644 --- a/src/Annotation/MediaType.php +++ b/src/Annotation/MediaType.php @@ -40,4 +40,11 @@ class MediaType extends Plugin { */ public $description = ''; + /** + * The field types that can be used as a source field for this type. + * + * @var string[] + */ + public $allowed_field_types = []; + } diff --git a/src/Entity/MediaBundle.php b/src/Entity/MediaBundle.php index 8833f82..2101771 100644 --- a/src/Entity/MediaBundle.php +++ b/src/Entity/MediaBundle.php @@ -2,12 +2,15 @@ namespace Drupal\media_entity\Entity; -use Drupal\Core\Entity\EntityDescriptionInterface; use Drupal\Core\Config\Entity\ConfigEntityBundleBase; +use Drupal\Core\Entity\EntityDescriptionInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityWithPluginCollectionInterface; use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection; +use Drupal\field\FieldStorageConfigInterface; use Drupal\media_entity\MediaBundleInterface; use Drupal\media_entity\MediaInterface; +use Drupal\media_entity\SourceFieldInterface; /** * Defines the Media bundle configuration entity. @@ -121,7 +124,7 @@ class MediaBundle extends ConfigEntityBundleBase implements MediaBundleInterface /** * Default status of this media bundle. * - * @var array + * @var bool */ public $status = TRUE; @@ -241,4 +244,57 @@ class MediaBundle extends ConfigEntityBundleBase implements MediaBundleInterface $this->new_revision = $new_revision; } + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + $type_plugin = $this->getType(); + if ($type_plugin instanceof SourceFieldInterface) { + $storage = $type_plugin->getSourceField($this)->getFieldStorageDefinition(); + if ($storage instanceof FieldStorageConfigInterface && $storage->isNew()) { + $storage->save(); + } + $configuration = $type_plugin->getConfiguration(); + $configuration['source_field'] = $storage->getName(); + $this->setTypeConfiguration($configuration); + } + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + $type_plugin = $this->getType(); + if ($type_plugin instanceof SourceFieldInterface) { + $field = $type_plugin->getSourceField($this); + + if ($field->isNew()) { + $entity_type = $field->getTargetEntityTypeId(); + $bundle = $field->getTargetBundle(); + + if ($field->isDisplayConfigurable('form')) { + $component = \Drupal::service('plugin.manager.field.widget') + ->prepareConfiguration($field->getType(), []); + + entity_get_form_display($entity_type, $bundle, 'default') + ->setComponent($field->getName(), $component) + ->save(); + } + if ($field->isDisplayConfigurable('view')) { + $component = \Drupal::service('plugin.manager.field.formatter') + ->prepareConfiguration($field->getType(), []); + + entity_get_display($entity_type, $bundle, 'default') + ->setComponent($field->getName(), $component) + ->save(); + } + $field->save(); + } + } + } + } diff --git a/src/MediaBundleForm.php b/src/MediaBundleForm.php index 014a538..5d310f0 100644 --- a/src/MediaBundleForm.php +++ b/src/MediaBundleForm.php @@ -373,7 +373,7 @@ class MediaBundleForm extends EntityForm { // Override the "status" base field default value, for this bundle. $fields = $this->entityFieldManager->getFieldDefinitions('media', $bundle->id()); - $media = $this->entityTypeManager->getStorage('media')->create(array('bundle' => $bundle->id())); + $media = $this->entityTypeManager->getStorage('media')->create(['bundle' => $bundle->id()]); $value = (bool) $form_state->getValue(['options', 'status']); if ($media->status->value != $value) { $fields['status']->getConfig($bundle->id())->setDefaultValue($value)->save(); diff --git a/src/MediaTypeBase.php b/src/MediaTypeBase.php index a472e7d..0a04e35 100644 --- a/src/MediaTypeBase.php +++ b/src/MediaTypeBase.php @@ -3,19 +3,19 @@ namespace Drupal\media_entity; use Drupal\Component\Plugin\PluginBase; +use Drupal\Component\Utility\NestedArray; use Drupal\Core\Config\Config; use Drupal\Core\Entity\EntityFieldManagerInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; -use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Plugin\ContainerFactoryPluginInterface; -use Drupal\Component\Utility\NestedArray; +use Drupal\Core\StringTranslation\StringTranslationTrait; use Symfony\Component\DependencyInjection\ContainerInterface; -use Drupal\Core\Form\FormStateInterface; /** * Base implementation of media type plugin. */ -abstract class MediaTypeBase extends PluginBase implements MediaTypeInterface, ContainerFactoryPluginInterface { +abstract class MediaTypeBase extends PluginBase implements MediaTypeInterface, SourceFieldInterface, ContainerFactoryPluginInterface { use StringTranslationTrait; /** @@ -105,7 +105,9 @@ abstract class MediaTypeBase extends PluginBase implements MediaTypeInterface, C * {@inheritdoc} */ public function defaultConfiguration() { - return []; + return [ + 'source_field' => NULL, + ]; } /** @@ -138,7 +140,27 @@ abstract class MediaTypeBase extends PluginBase implements MediaTypeInterface, C * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { - return []; + $options = []; + + foreach ($this->entityFieldManager->getFieldStorageDefinitions('media') as $field_name => $field) { + $allowed_type = in_array($field->getType(), $this->pluginDefinition['allowed_field_types']); + if ($allowed_type && !$field->isBaseField()) { + $options[$field_name] = $field->getLabel(); + } + } + + // If there are existing fields to choose from, allow the user to reuse one. + if ($options) { + $form['source_field'] = [ + '#type' => 'select', + '#title' => $this->t('Field with source information.'), + '#default_value' => $this->configuration['source_field'], + '#empty_option' => $this->t('- Create -'), + '#empty_value' => NULL, + '#options' => $options, + ]; + } + return $form; } /** @@ -158,4 +180,85 @@ abstract class MediaTypeBase extends PluginBase implements MediaTypeInterface, C return 'media:' . $media->bundle() . ':' . $media->uuid(); } + /** + * {@inheritdoc} + */ + public function getSourceField(MediaBundleInterface $bundle) { + if (empty($this->configuration['source_field'])) { + return $this->createSourceField($bundle); + } + $id = 'media.' . $bundle->id() . '.' . $this->configuration['source_field']; + + return $this->entityTypeManager + ->getStorage('field_config') + ->load($id) + ?: + $this->createSourceField($bundle); + } + + /** + * Returns the source field storage definition. + * + * @return \Drupal\field\FieldStorageConfigInterface + * The field storage definition. Will be unsaved if new. + */ + protected function getSourceFieldStorage() { + if ($this->configuration['source_field']) { + $id = 'media.' . $this->configuration['source_field']; + + return $this->entityTypeManager + ->getStorage('field_storage_config') + ->load($id); + } + else { + return $this->createSourceFieldStorage(); + } + } + + /** + * Creates the source field storage definition. + * + * @return \Drupal\field\FieldStorageConfigInterface + * The unsaved field storage definition. + */ + abstract protected function createSourceFieldStorage(); + + /** + * Creates the source field definition for a bundle. + * + * @param \Drupal\media_entity\MediaBundleInterface $bundle + * The bundle. + * + * @return \Drupal\field\FieldConfigInterface + * The unsaved field definition. The field storage definition, if new, + * should also be unsaved. + */ + abstract protected function createSourceField(MediaBundleInterface $bundle); + + /** + * Determine a free field name to use as the default field. + * + * @return string + * An appropriate field name that was determined to be available. + */ + protected function getSourceFieldName() { + $base_id = 'field_media_' . $this->getPluginId(); + $tries = 0; + $storage = $this->entityTypeManager->getStorage('field_storage_config'); + + // Iterate at least once, until no field with the generated ID is found. + do { + $id = $base_id; + // If we've tried before, increment and append the suffix. + if ($tries) { + $id .= '_' . $tries; + } + $field = $storage->load('media.' . $id); + $tries++; + } + while ($field); + + return $id; + } + } diff --git a/src/MediaTypeInterface.php b/src/MediaTypeInterface.php index 2a171c2..767217f 100644 --- a/src/MediaTypeInterface.php +++ b/src/MediaTypeInterface.php @@ -2,8 +2,8 @@ namespace Drupal\media_entity; -use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Component\Plugin\ConfigurablePluginInterface; +use Drupal\Component\Plugin\PluginInspectionInterface; use Drupal\Core\Plugin\PluginFormInterface; /** diff --git a/src/Plugin/Action/DeleteMedia.php b/src/Plugin/Action/DeleteMedia.php index b2a98d2..4a5a48e 100644 --- a/src/Plugin/Action/DeleteMedia.php +++ b/src/Plugin/Action/DeleteMedia.php @@ -84,7 +84,7 @@ class DeleteMedia extends ActionBase implements ContainerFactoryPluginInterface * {@inheritdoc} */ public function execute($object = NULL) { - $this->executeMultiple(array($object)); + $this->executeMultiple([$object]); } /** diff --git a/src/Plugin/MediaEntity/Type/Generic.php b/src/Plugin/MediaEntity/Type/Generic.php index b83ba9d..7fe313e 100644 --- a/src/Plugin/MediaEntity/Type/Generic.php +++ b/src/Plugin/MediaEntity/Type/Generic.php @@ -3,6 +3,7 @@ namespace Drupal\media_entity\Plugin\MediaEntity\Type; use Drupal\Core\Form\FormStateInterface; +use Drupal\media_entity\MediaBundleInterface; use Drupal\media_entity\MediaInterface; use Drupal\media_entity\MediaTypeBase; @@ -12,7 +13,8 @@ use Drupal\media_entity\MediaTypeBase; * @MediaType( * id = "generic", * label = @Translation("Generic media"), - * description = @Translation("Generic media type.") + * description = @Translation("Generic media type."), + * allowed_field_types = {"string"} * ) */ class Generic extends MediaTypeBase { @@ -42,6 +44,8 @@ class Generic extends MediaTypeBase { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + $form['text'] = [ '#type' => 'markup', '#markup' => $this->t("This type provider doesn't need configuration."), @@ -50,4 +54,32 @@ class Generic extends MediaTypeBase { return $form; } + /** + * {@inheritdoc} + */ + protected function createSourceFieldStorage() { + return $this->entityTypeManager + ->getStorage('field_storage_config') + ->create([ + 'entity_type' => 'media', + 'field_name' => $this->getSourceFieldName(), + // Strings are harmless, inoffensive puppies: a good choice for a + // generic media type. + 'type' => 'string', + ]); + } + + /** + * {@inheritdoc} + */ + protected function createSourceField(MediaBundleInterface $bundle) { + /** @var \Drupal\field\FieldConfigInterface $field */ + return $this->entityTypeManager + ->getStorage('field_config') + ->create([ + 'field_storage' => $this->getSourceFieldStorage(), + 'bundle' => $bundle->id(), + ]); + } + } diff --git a/src/SourceFieldInterface.php b/src/SourceFieldInterface.php new file mode 100644 index 0000000..9c6ad65 --- /dev/null +++ b/src/SourceFieldInterface.php @@ -0,0 +1,20 @@ + 'This is default value.', ]; } @@ -39,12 +40,16 @@ class TestType extends Generic { * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $form_state); + $form['test_config_value'] = [ '#type' => 'textfield', '#title' => $this->t('Test config value'), '#default_value' => empty($this->configuration['test_config_value']) ? NULL : $this->configuration['test_config_value'], ]; + $form['source_field']['#description'] = $this->t('Field on media entity that stores the source information.'); + return $form; } diff --git a/tests/src/FunctionalJavascript/BundleCreationTest.php b/tests/src/FunctionalJavascript/BundleCreationTest.php new file mode 100644 index 0000000..f0a3099 --- /dev/null +++ b/tests/src/FunctionalJavascript/BundleCreationTest.php @@ -0,0 +1,106 @@ +drupalGet('admin/structure/media/add'); + $page = $this->getSession()->getPage(); + + // Fill in a label to the bundle. + $page->fillField('label', $label); + // Wait for machine name generation. Default: waitUntilVisible(), does not + // work properly. + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$bundleMachineName}'"); + + // Select our test bundle type. + $this->assertSession()->fieldExists('Type provider'); + $this->assertSession()->optionExists('Type provider', 'test_type'); + $page->selectFieldOption('Type provider', 'test_type'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $page->pressButton('Save media bundle'); + + // Check whether the source field was correctly created. + $this->drupalGet("admin/structure/media/manage/{$bundleMachineName}/fields"); + + // Check 2nd column of first data row, to be machine name for field name. + $this->assertSession() + ->elementContains('xpath', '(//table[@id="field-overview"]//tr)[2]//td[2]', 'field_media_test_type'); + // Check 3rd column of first data row, to be correct field type. + $this->assertSession() + ->elementTextContains('xpath', '(//table[@id="field-overview"]//tr)[2]//td[3]', 'Text (plain)'); + + // Check that the source field is correctly assigned to media bundle. + $this->drupalGet("admin/structure/media/manage/{$bundleMachineName}"); + + $this->assertSession() + ->fieldValueEquals('type_configuration[test_type][source_field]', 'field_media_test_type'); + } + + /** + * Test creation of media bundle, reusing an existing source field. + */ + public function testBundleCreationReuseSourceField() { + // Create a new bundle, which should create a new field we can reuse. + MediaBundle::create([ + 'id' => 'pastafazoul', + 'label' => 'Pastafazoul', + 'type' => 'generic', + ])->save(); + + $label = 'Bundle reusing Default Field'; + $bundleMachineName = str_replace(' ', '_', strtolower($label)); + + $this->drupalGet('admin/structure/media/add'); + $page = $this->getSession()->getPage(); + + // Fill in a label to the bundle. + $page->fillField('label', $label); + + // Wait for machine name generation. Default: waitUntilVisible(), does not + // work properly. + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$bundleMachineName}'"); + + // Select our test bundle type. + $this->assertSession()->fieldExists('Type provider'); + $this->assertSession()->optionExists('Type provider', 'test_type'); + $page->selectFieldOption('Type provider', 'test_type'); + $this->assertSession()->assertWaitOnAjaxRequest(); + // Select the existing field for re-use. + $page->selectFieldOption('type_configuration[test_type][source_field]', 'field_media_generic'); + $page->pressButton('Save media bundle'); + + // Check that there are not fields created. + $this->drupalGet("admin/structure/media/manage/{$bundleMachineName}/fields"); + // The reused field should be present... + $this->assertSession()->pageTextContains('field_media_generic'); + // ...not a new, unique one. + $this->assertSession()->pageTextNotContains('field_media_generic_1'); + } + +} diff --git a/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php b/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php index c8219fe..4cd1c7e 100644 --- a/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php +++ b/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php @@ -140,6 +140,11 @@ class MediaUiJavascriptTest extends MediaEntityJavascriptTestBase { $this->assertFalse($loaded_bundle->getStatus()); $this->assertEquals($loaded_bundle->field_map, ['field_1' => 'name']); + // We need to clear the statically cached field definitions to account for + // fields that have been created by API calls in this test, since they exist + // in a separate memory space from the web server. + $this->container->get('entity_field.manager')->clearCachedFieldDefinitions(); + // Test that a media being created with default status to "FALSE" will be // created unpublished. /** @var \Drupal\media_entity\MediaInterface $unpublished_media */ diff --git a/tests/src/Kernel/BasicCreationTest.php b/tests/src/Kernel/BasicCreationTest.php index 306b553..38f74e8 100644 --- a/tests/src/Kernel/BasicCreationTest.php +++ b/tests/src/Kernel/BasicCreationTest.php @@ -79,7 +79,7 @@ class BasicCreationTest extends KernelTestBase { $this->assertEquals($test_bundle->get('label'), 'Test bundle', 'Could not assure the correct bundle label.'); $this->assertEquals($test_bundle->get('description'), 'Test bundle.', 'Could not assure the correct bundle description.'); $this->assertEquals($test_bundle->get('type'), 'generic', 'Could not assure the correct bundle plugin type.'); - $this->assertEquals($test_bundle->get('type_configuration'), [], 'Could not assure the correct plugin configuration.'); + $this->assertEquals($test_bundle->get('type_configuration'), ['source_field' => 'field_media_generic_1'], 'Could not assure the correct plugin configuration.'); $this->assertEquals($test_bundle->get('field_map'), [], 'Could not assure the correct field map.'); }