diff --git a/islandora.services.yml b/islandora.services.yml index 4b3a9d16..e48818b9 100644 --- a/islandora.services.yml +++ b/islandora.services.yml @@ -59,3 +59,9 @@ services: arguments: ['@jwt.authentication.jwt'] tags: - { name: event_subscriber } + islandora.upload_children.batch_processor: + class: Drupal\islandora\Form\AddChildrenWizard\BatchProcessor + arguments: + - '@entity_type.manager' + - '@database' + - '@current_user' diff --git a/src/Form/AddChildrenWizard/BatchProcessor.php b/src/Form/AddChildrenWizard/BatchProcessor.php new file mode 100644 index 00000000..2e4a84bf --- /dev/null +++ b/src/Form/AddChildrenWizard/BatchProcessor.php @@ -0,0 +1,190 @@ +<?php + +namespace Drupal\islandora\Form\AddChildrenWizard; + +use Drupal\Core\Database\Connection; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Core\Url; +use Drupal\islandora\IslandoraUtils; +use Drupal\node\NodeInterface; +use Symfony\Component\HttpKernel\Exception\HttpException; +use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; + +/** + * Children addition batch processor. + */ +class BatchProcessor { + + use FieldTrait; + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null + */ + protected ?EntityTypeManagerInterface $entityTypeManager; + + /** + * The database connection serivce. + * + * @var \Drupal\Core\Database\Connection|null + */ + protected ?Connection $database; + + /** + * The current user. + * + * @var \Drupal\Core\Session\AccountProxyInterface|null + */ + protected ?AccountProxyInterface $currentUser; + + /** + * Constructor. + */ + public function __construct( + EntityTypeManagerInterface $entity_type_manager, + Connection $database, + AccountProxyInterface $current_user + ) { + $this->entityTypeManager = $entity_type_manager; + $this->database = $database; + $this->currentUser = $current_user; + } + + /** + * Implements callback_batch_operation() for our child addition batch. + */ + public function batchOperation($delta, $info, array $values, &$context) { + $transaction = $this->database->startTransaction(); + + try { + $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); + + /** @var \Drupal\file\FileInterface $file */ + $file = $this->entityTypeManager->getStorage('file')->load($info['target_id']); + $file->setPermanent(); + if ($file->save() !== SAVED_UPDATED) { + throw new \Exception("Failed to update file '{$file->id()}' to be permanent."); + } + + $node_storage = $this->entityTypeManager->getStorage('node'); + $parent = $node_storage->load($values['node']); + + // Create a node (with the filename?) (and also belonging to the target + // node). + /** @var \Drupal\node\NodeInterface $node */ + $node = $node_storage->create([ + 'type' => $values['bundle'], + 'title' => $file->getFilename(), + IslandoraUtils::MEMBER_OF_FIELD => $parent, + 'uid' => $this->currentUser->id(), + 'status' => NodeInterface::PUBLISHED, + IslandoraUtils::MODEL_FIELD => ($values['model'] ? + $taxonomy_term_storage->load($values['model']) : + NULL), + ]); + + if ($node->save() !== SAVED_NEW) { + throw new \Exception("Failed to create node for file '{$file->id()}'."); + } + + // Create a media with the file attached and also pointing at the node. + $field = $this->getField($values); + + $media_values = array_merge( + [ + 'bundle' => $values['media_type'], + 'name' => $file->getFilename(), + IslandoraUtils::MEDIA_OF_FIELD => $node, + IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ? + $taxonomy_term_storage->loadMultiple($values['use']) : + NULL), + 'uid' => $this->currentUser->id(), + // XXX: Published... no constant? + 'status' => 1, + ], + [ + $field->getName() => [ + $info, + ], + ] + ); + $media = $this->entityTypeManager->getStorage('media')->create($media_values); + if ($media->save() !== SAVED_NEW) { + throw new \Exception("Failed to create media for file '{$file->id()}."); + } + + $context['results'] = array_merge_recursive($context['results'], [ + 'validation_violations' => $this->validationClassification([ + $file, + $media, + $node, + ]), + ]); + $context['results']['count'] += 1; + } + catch (HttpExceptionInterface $e) { + $transaction->rollBack(); + throw $e; + } + catch (\Exception $e) { + $transaction->rollBack(); + throw new HttpException(500, $e->getMessage(), $e); + } + } + + /** + * Helper to bulk process validatable entities. + * + * @param array $entities + * An array of entities to scan for validation violations. + * + * @return array + * An associative array mapping entity type IDs to entity IDs to a count + * of validation violations found on then given entity. + */ + protected function validationClassification(array $entities) { + $violations = []; + + foreach ($entities as $entity) { + $entity_violations = $entity->validate(); + if ($entity_violations->count() > 0) { + $violations[$entity->getEntityTypeId()][$entity->id()] = $entity_violations->count(); + } + } + + return $violations; + } + + /** + * Implements callback_batch_finished() for our child addition batch. + */ + public function batchProcessFinished($success, $results, $operations): void { + if ($success) { + $this->messenger()->addMessage($this->formatPlural( + $results['count'], + 'Added 1 child node.', + 'Added @count child nodes.' + )); + foreach ($results['validation_violations'] ?? [] as $entity_type => $info) { + foreach ($info as $id => $count) { + $this->messenger()->addWarning($this->formatPlural( + $count, + '1 validation error present in <a href=":uri">bulk created entity of type %type, with ID %id</a>.', + '@count validation errors present in <a href=":uri">bulk created entity of type %type, with ID %id</a>.', + [ + '%type' => $entity_type, + ':uri' => Url::fromRoute("entity.{$entity_type}.canonical", [$entity_type => $id])->toString(), + '%id' => $id, + ] + )); + } + } + } + else { + $this->messenger()->addError($this->t('Encountered an error when adding children.')); + } + } + +} diff --git a/src/Form/AddChildrenWizard/FieldTrait.php b/src/Form/AddChildrenWizard/FieldTrait.php new file mode 100644 index 00000000..156000e3 --- /dev/null +++ b/src/Form/AddChildrenWizard/FieldTrait.php @@ -0,0 +1,66 @@ +<?php + +namespace Drupal\islandora\Form\AddChildrenWizard; + +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; + +/** + * Field lookup helper trait. + */ +trait FieldTrait { + + use MediaTypeTrait; + + /** + * The entity field manager service. + * + * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null + */ + protected ?EntityFieldManagerInterface $entityFieldManager; + + /** + * Helper; get field instance, given our required values. + * + * @param array $values + * See ::getMediaType() for which values are required. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface + * The target field. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getField(array $values): FieldDefinitionInterface { + $media_type = $this->getMediaType($values); + $media_source = $media_type->getSource(); + $source_field = $media_source->getSourceFieldDefinition($media_type); + + $fields = $this->entityFieldManager()->getFieldDefinitions('media', $media_type->id()); + + return $fields[$source_field->getFieldStorageDefinition()->getName()] ?? + $media_source->createSourceField(); + } + + /** + * Lazy-initialization of the entity field manager service. + * + * @return \Drupal\Core\Entity\EntityFieldManagerInterface + * The entity field manager service. + */ + protected function entityFieldManager() : EntityFieldManagerInterface { + if ($this->entityFieldManager === NULL) { + $this->setEntityFieldManager(\Drupal::service('entity_field.manager')); + } + return $this->entityFieldManager; + } + + /** + * Setter for entity field manager. + */ + public function setEntityFieldManager(EntityFieldManagerInterface $entity_field_manager) : self { + $this->entityFieldManager = $entity_field_manager; + return $this; + } + +} diff --git a/src/Form/AddChildrenWizard/FileSelectionForm.php b/src/Form/AddChildrenWizard/FileSelectionForm.php index 8d29bab3..084eb5e5 100644 --- a/src/Form/AddChildrenWizard/FileSelectionForm.php +++ b/src/Form/AddChildrenWizard/FileSelectionForm.php @@ -2,11 +2,8 @@ namespace Drupal\islandora\Form\AddChildrenWizard; -use Drupal\Component\Plugin\PluginManagerInterface; use Drupal\Core\Batch\BatchBuilder; use Drupal\Core\Database\Connection; -use Drupal\Core\Entity\EntityFieldManagerInterface; -use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldItemList; use Drupal\Core\Field\FieldStorageDefinitionInterface; @@ -15,38 +12,19 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\Url; -use Drupal\islandora\IslandoraUtils; use Drupal\media\MediaTypeInterface; -use Drupal\node\NodeInterface; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpKernel\Exception\HttpException; -use Symfony\Component\HttpKernel\Exception\HttpExceptionInterface; /** * Children addition wizard's second step. */ class FileSelectionForm extends FormBase { - /** - * The entity type manager service. - * - * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null - */ - protected ?EntityTypeManagerInterface $entityTypeManager; - - /** - * The widget plugin manager service. - * - * @var \Drupal\Core\Field\WidgetPluginManager|null - */ - protected ?PluginManagerInterface $widgetPluginManager; - - /** - * The entity field manager service. - * - * @var \Drupal\Core\Entity\EntityFieldManagerInterface|null - */ - protected ?EntityFieldManagerInterface $entityFieldManager; + use WizardTrait { + WizardTrait::getField as doGetField; + WizardTrait::getMediaType as doGetMediaType; + WizardTrait::getWidget as doGetWidget; + } /** * The database connection serivce. @@ -62,6 +40,13 @@ class FileSelectionForm extends FormBase { */ protected ?AccountProxyInterface $currentUser; + /** + * The batch processor service. + * + * @var \Drupal\islandora\Form\AddChildrenWizard\BatchProcessor|null + */ + protected ?BatchProcessor $batchProcessor; + /** * {@inheritdoc} */ @@ -74,6 +59,8 @@ class FileSelectionForm extends FormBase { $instance->database = $container->get('database'); $instance->currentUser = $container->get('current_user'); + $instance->batchProcessor = $container->get('islandora.upload_children.batch_processor'); + return $instance; } @@ -100,24 +87,6 @@ class FileSelectionForm extends FormBase { return $this->doGetMediaType($form_state->getTemporaryValue('wizard')); } - /** - * Helper; get media type, given our required values. - * - * @param array $values - * An associative array which must contain at least: - * - media_type: The machine name of the media type to load. - * - * @return \Drupal\media\MediaTypeInterface - * The loaded media type. - * - * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException - * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException - */ - protected function doGetMediaType(array $values): MediaTypeInterface { - /** @var \Drupal\media\MediaTypeInterface $media_type */ - return $this->entityTypeManager->getStorage('media_type')->load($values['media_type']); - } - /** * Helper; get field instance, based off discovering from form state. * @@ -136,26 +105,6 @@ class FileSelectionForm extends FormBase { return $field; } - /** - * Helper; get field instance, given our required values. - * - * @param array $values - * See ::doGetMediaType() for which values are required. - * - * @return \Drupal\Core\Field\FieldDefinitionInterface - * The target field. - */ - protected function doGetField(array $values): FieldDefinitionInterface { - $media_type = $this->doGetMediaType($values); - $media_source = $media_type->getSource(); - $source_field = $media_source->getSourceFieldDefinition($media_type); - - $fields = $this->entityFieldManager->getFieldDefinitions('media', $media_type->id()); - - return $fields[$source_field->getFieldStorageDefinition()->getName()] ?? - $media_source->createSourceField(); - } - /** * Helper; get widget for the field, based on discovering from form state. * @@ -169,23 +118,6 @@ class FileSelectionForm extends FormBase { return $this->doGetWidget($this->getField($form_state)); } - /** - * Helper; get the base widget for the given field. - * - * @param \Drupal\Core\Field\FieldDefinitionInterface $field - * The field for which get obtain the widget. - * - * @return \Drupal\Core\Field\WidgetInterface - * The widget. - */ - protected function doGetWidget(FieldDefinitionInterface $field): WidgetInterface { - return $this->widgetPluginManager->getInstance([ - 'field_definition' => $field, - 'form_mode' => 'default', - 'prepare' => TRUE, - ]); - } - /** * {@inheritdoc} */ @@ -218,12 +150,12 @@ class FileSelectionForm extends FormBase { $builder = (new BatchBuilder()) ->setTitle($this->t('Creating children...')) ->setInitMessage($this->t('Initializing...')) - ->setFinishCallback([$this, 'batchProcessFinished']); + ->setFinishCallback([$this->batchProcessor, 'batchProcessFinished']); $values = $form_state->getValue($this->doGetField($cached_values)->getName()); $massaged_values = $widget->massageFormValues($values, $form, $form_state); foreach ($massaged_values as $delta => $file) { $builder->addOperation( - [$this, 'batchProcess'], + [$this->batchProcessor, 'batchOperation'], [$delta, $file, $cached_values] ); } @@ -231,134 +163,4 @@ class FileSelectionForm extends FormBase { $form_state->setRedirectUrl(Url::fromUri("internal:/node/{$cached_values['node']}/members")); } - /** - * Implements callback_batch_operation() for our child addition batch. - */ - public function batchProcess($delta, $info, array $values, &$context) { - $transaction = $this->database->startTransaction(); - - try { - $taxonomy_term_storage = $this->entityTypeManager->getStorage('taxonomy_term'); - - /** @var \Drupal\file\FileInterface $file */ - $file = $this->entityTypeManager->getStorage('file')->load($info['target_id']); - $file->setPermanent(); - if ($file->save() !== SAVED_UPDATED) { - throw new \Exception("Failed to update file '{$file->id()}' to be permanent."); - } - - $node_storage = $this->entityTypeManager->getStorage('node'); - $parent = $node_storage->load($values['node']); - - // Create a node (with the filename?) (and also belonging to the target - // node). - /** @var \Drupal\node\NodeInterface $node */ - $node = $node_storage->create([ - 'type' => $values['bundle'], - 'title' => $file->getFilename(), - IslandoraUtils::MEMBER_OF_FIELD => $parent, - 'uid' => $this->currentUser->id(), - 'status' => NodeInterface::PUBLISHED, - IslandoraUtils::MODEL_FIELD => ($values['model'] ? - $taxonomy_term_storage->load($values['model']) : - NULL), - ]); - - if ($node->save() !== SAVED_NEW) { - throw new \Exception("Failed to create node for file '{$file->id()}'."); - } - - // Create a media with the file attached and also pointing at the node. - $field = $this->doGetField($values); - - $media_values = array_merge( - [ - 'bundle' => $values['media_type'], - 'name' => $file->getFilename(), - IslandoraUtils::MEDIA_OF_FIELD => $node, - IslandoraUtils::MEDIA_USAGE_FIELD => ($values['use'] ? - $taxonomy_term_storage->loadMultiple($values['use']) : - NULL), - 'uid' => $this->currentUser->id(), - // XXX: Published... no constant? - 'status' => 1, - ], - [ - $field->getName() => [ - $info, - ], - ] - ); - $media = $this->entityTypeManager->getStorage('media')->create($media_values); - if ($media->save() !== SAVED_NEW) { - throw new \Exception("Failed to create media for file '{$file->id()}."); - } - - $context['results'] = array_merge_recursive($context['results'], [ - 'validation_violations' => $this->validationClassification([ - $file, - $media, - $node, - ]), - ]); - $context['results']['count'] += 1; - } - catch (HttpExceptionInterface $e) { - $transaction->rollBack(); - throw $e; - } - catch (\Exception $e) { - $transaction->rollBack(); - throw new HttpException(500, $e->getMessage(), $e); - } - } - - /** - * @param array $entities - * - * @return array - */ - protected function validationClassification(array $entities) { - $violations = []; - - foreach ($entities as $entity) { - $entity_violations = $entity->validate(); - if ($entity_violations->count() > 0) { - $violations[$entity->getEntityTypeId()][$entity->id()] = $entity_violations->count(); - } - } - - return $violations; - } - - /** - * Implements callback_batch_finished() for our child addition batch. - */ - public function batchProcessFinished($success, $results, $operations): void { - if ($success) { - $this->messenger()->addMessage($this->formatPlural( - $results['count'], - 'Added 1 child node.', - 'Added @count child nodes.' - )); - foreach ($results['validation_violations'] ?? [] as $entity_type => $info) { - foreach ($info as $id => $count) { - $this->messenger()->addWarning($this->formatPlural( - $count, - '1 validation error present in <a href=":uri">bulk created entity of type %type, with ID %id</a>.', - '@count validation errors present in <a href=":uri">bulk created entity of type %type, with ID %id</a>.', - [ - '%type' => $entity_type, - ':uri' => Url::fromRoute("entity.{$entity_type}.canonical", [$entity_type => $id])->toString(), - '%id' => $id, - ] - )); - } - } - } - else { - $this->messenger()->addError($this->t('Encountered an error when adding children.')); - } - } - } diff --git a/src/Form/AddChildrenWizard/Form.php b/src/Form/AddChildrenWizard/Form.php index 68489f18..97ac8b23 100644 --- a/src/Form/AddChildrenWizard/Form.php +++ b/src/Form/AddChildrenWizard/Form.php @@ -2,10 +2,8 @@ namespace Drupal\islandora\Form\AddChildrenWizard; -use Drupal\Core\Access\AccessResult; use Drupal\Core\DependencyInjection\ClassResolverInterface; use Drupal\Core\Form\FormBuilderInterface; -use Drupal\Core\Routing\RouteMatch; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountProxyInterface; use Drupal\Core\TempStore\SharedTempStoreFactory; @@ -56,8 +54,6 @@ class Form extends FormWizardBase { EventDispatcherInterface $event_dispatcher, RouteMatchInterface $route_match, $tempstore_id, - IslandoraUtils $utils, - RouteMatchInterface $current_route_match, AccountProxyInterface $current_user, $machine_name = NULL, $step = NULL @@ -65,9 +61,7 @@ class Form extends FormWizardBase { parent::__construct($tempstore, $builder, $class_resolver, $event_dispatcher, $route_match, $tempstore_id, $machine_name, $step); - $this->utils = $utils; - $this->currentRoute = $current_route_match; - $this->nodeId = $this->currentRoute->getParameter('node'); + $this->nodeId = $this->routeMatch->getParameter('node'); $this->currentUser = $current_user; } @@ -78,10 +72,9 @@ class Form extends FormWizardBase { return array_merge( parent::getParameters(), [ - 'utils' => \Drupal::service('islandora.utils'), 'tempstore_id' => 'islandora.upload_children', - 'current_route_match' => \Drupal::service('current_route_match'), 'current_user' => \Drupal::service('current_user'), + 'batch_processor' => \Drupal::service('islandora.upload_children.batch_processor'), ] ); } diff --git a/src/Form/AddChildrenWizard/MediaTypeTrait.php b/src/Form/AddChildrenWizard/MediaTypeTrait.php new file mode 100644 index 00000000..e2caf2c2 --- /dev/null +++ b/src/Form/AddChildrenWizard/MediaTypeTrait.php @@ -0,0 +1,59 @@ +<?php + +namespace Drupal\islandora\Form\AddChildrenWizard; + +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\media\MediaTypeInterface; + +/** + * Media type lookup helper trait. + */ +trait MediaTypeTrait { + + /** + * The entity type manager service. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|null + */ + protected ?EntityTypeManagerInterface $entityTypeManager; + + /** + * Helper; get media type, given our required values. + * + * @param array $values + * An associative array which must contain at least: + * - media_type: The machine name of the media type to load. + * + * @return \Drupal\media\MediaTypeInterface + * The loaded media type. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + protected function getMediaType(array $values): MediaTypeInterface { + /** @var \Drupal\media\MediaTypeInterface $media_type */ + return $this->entityTypeManager()->getStorage('media_type')->load($values['media_type']); + } + + /** + * Lazy-initialization of the entity type manager service. + * + * @return \Drupal\Core\Entity\EntityTypeManagerInterface + * The entity type manager service. + */ + protected function entityTypeManager() : EntityTypeManagerInterface { + if ($this->entityTypeManager === NULL) { + $this->setEntityTypeManager(\Drupal::service('entity_type.manager')); + } + return $this->entityTypeManager; + } + + /** + * Setter for the entity type manager service. + */ + public function setEntityTypeManager(EntityTypeManagerInterface $entity_type_manager) : self { + $this->entityTypeManager = $entity_type_manager; + return $this; + } + +} diff --git a/src/Form/AddChildrenWizard/WizardTrait.php b/src/Form/AddChildrenWizard/WizardTrait.php new file mode 100644 index 00000000..dd56450f --- /dev/null +++ b/src/Form/AddChildrenWizard/WizardTrait.php @@ -0,0 +1,40 @@ +<?php + +namespace Drupal\islandora\Form\AddChildrenWizard; + +use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\WidgetInterface; + +/** + * Wizard/widget lookup helper trait. + */ +trait WizardTrait { + + use FieldTrait; + + /** + * The widget plugin manager service. + * + * @var \Drupal\Core\Field\WidgetPluginManager + */ + protected PluginManagerInterface $widgetPluginManager; + + /** + * Helper; get the base widget for the given field. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field + * The field for which get obtain the widget. + * + * @return \Drupal\Core\Field\WidgetInterface + * The widget. + */ + protected function getWidget(FieldDefinitionInterface $field): WidgetInterface { + return $this->widgetPluginManager->getInstance([ + 'field_definition' => $field, + 'form_mode' => 'default', + 'prepare' => TRUE, + ]); + } + +}