diff --git a/src/Commands/core/CacheRebuildCommands.php b/src/Commands/core/CacheRebuildCommands.php index 7fefe420ae..378e78ca0c 100644 --- a/src/Commands/core/CacheRebuildCommands.php +++ b/src/Commands/core/CacheRebuildCommands.php @@ -22,7 +22,7 @@ final class CacheRebuildCommands extends DrushCommands public function __construct( private readonly BootstrapManager $bootstrapManager, - private readonly ClassLoader $autoloader + private ClassLoader $autoloader ) { parent::__construct(); } diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index 488ea99f31..70a7d7b8b9 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -6,13 +6,18 @@ use Consolidation\AnnotatedCommand\Input\StdinAwareInterface; use Consolidation\AnnotatedCommand\Input\StdinAwareTrait; +use Drupal\Component\Datetime\TimeInterface; use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityChangedInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityStorageException; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\Query\QueryInterface; +use Drupal\Core\Entity\RevisionableInterface; use Drupal\Core\Entity\RevisionLogInterface; +use Drupal\Core\Session\AccountInterface; use Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; use Drush\Commands\DrushCommands; @@ -26,8 +31,11 @@ final class EntityCommands extends DrushCommands implements StdinAwareInterface const DELETE = 'entity:delete'; const SAVE = 'entity:save'; - public function __construct(protected EntityTypeManagerInterface $entityTypeManager) - { + public function __construct( + protected EntityTypeManagerInterface $entityTypeManager, + protected TimeInterface $time, + protected AccountInterface $currentUser + ) { parent::__construct(); } @@ -101,28 +109,38 @@ public function doDelete(string $entity_type, array $ids): void #[CLI\Option(name: 'bundle', description: 'Restrict to the specified bundle. Ignored when ids is specified.')] #[CLI\Option(name: 'exclude', description: 'Exclude certain entities. Ignored when ids is specified.')] #[CLI\Option(name: 'chunks', description: 'Define how many entities will be loaded in the same step.')] - #[CLI\Option(name: 'publish', description: 'Publish entities as they are saved.')] + #[CLI\Option(name: 'publish', description: 'Publish entities as they are saved. ')] #[CLI\Option(name: 'unpublish', description: 'Unpublish entities as they are saved.')] + #[CLI\Option(name: 'state', description: 'Transition entities to the specified Content Moderation state. Do not pass --publish or --unpublish since the transition state determines handles publishing.')] #[CLI\Usage(name: 'drush entity:save node --bundle=article', description: 'Re-save all article entities.')] - #[CLI\Usage(name: 'drush entity:save shortcut --unpublish', description: 'Re-save all shortcut entities, and unpublish them all.')] + #[CLI\Usage(name: 'drush entity:save shortcut --unpublish --state=draft', description: 'Unpublish and transition all shortcut entities.')] #[CLI\Usage(name: 'drush entity:save node 22,24', description: 'Re-save nodes 22 and 24.')] #[CLI\Usage(name: 'cat /path/to/ids.csv | drush entity:save node -', description: 'Re-save the nodes whose Ids are listed in ids.csv.')] #[CLI\Usage(name: 'drush entity:save node --exclude=9,14,81', description: 'Re-save all nodes except node 9, 14 and 81.')] #[CLI\Usage(name: 'drush entity:save user', description: 'Re-save all users.')] #[CLI\Usage(name: 'drush entity:save node --chunks=5', description: 'Re-save all node entities in steps of 5.')] #[CLI\Version(version: '11.0')] - public function loadSave(string $entity_type, $ids = null, array $options = ['bundle' => self::REQ, 'exclude' => self::REQ, 'chunks' => 50, 'publish' => false, 'unpublish' => false]): void + public function loadSave(string $entity_type, $ids = null, array $options = ['bundle' => self::REQ, 'exclude' => self::REQ, 'chunks' => 50, 'publish' => false, 'unpublish' => false, 'state' => self::REQ]): void { if ($options['publish'] && $options['unpublish']) { - throw new \InvalidArgumentException(dt('You cannot specify both --publish and --unpublish.')); + throw new \InvalidArgumentException(dt('You may not specify both --publish and --unpublish.')); + } + if ($options['state'] && $options['publish']) { + throw new \InvalidArgumentException(dt('You may not specify both --state and --publish.')); + } + if ($options['state'] && $options['unpublish']) { + throw new \InvalidArgumentException(dt('You may not specify both --state and --unpublish.')); } - $action = null; - if ($options['publish']) { + $action = $state = null; + if ($options['state']) { + $state = $options['state']; + } elseif ($options['publish']) { $action = 'publish'; } elseif ($options['unpublish']) { $action = 'unpublish'; } + if ($ids === '-') { $ids = $this->stdin()->contents(); } @@ -136,7 +154,7 @@ public function loadSave(string $entity_type, $ids = null, array $options = ['bu $progress = $this->io()->progress('Saving entities', count($chunks)); $progress->start(); foreach ($chunks as $chunk) { - drush_op([$this, 'doSave'], $entity_type, $chunk, $action); + drush_op([$this, 'doSave'], $entity_type, $chunk, $action, $state); $progress->advance(); } $progress->finish(); @@ -144,6 +162,9 @@ public function loadSave(string $entity_type, $ids = null, array $options = ['bu if ($action) { $this->logger()->success(dt("Entities have been !actioned.", ['!action' => $action])); } + if ($state) { + $this->logger()->success(dt("Entities have been transitioned to !state.", ['!state' => $state])); + } } } @@ -155,20 +176,48 @@ public function loadSave(string $entity_type, $ids = null, array $options = ['bu * @throws PluginNotFoundException * @throws EntityStorageException */ - public function doSave(string $entity_type, array $ids, ?string $action): void + public function doSave(string $entity_type, array $ids, ?string $action, ?string $state): void { + $message = []; $storage = $this->entityTypeManager->getStorage($entity_type); $entities = $storage->loadMultiple($ids); foreach ($entities as $entity) { - if (is_a($entity, EntityPublishedInterface::class)) { + if (is_a($entity, RevisionableInterface::class)) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + $entity = $storage->createRevision($entity, true); + } + if ($state) { + // AutowireTrait does not support optional params so can't use DI. + $moderationInformation = \Drupal::service('content_moderation.moderation_information'); + if (!$moderationInformation->isModeratedEntity($entity)) { + throw new \InvalidArgumentException(dt('!bundle !id does not support content moderation.', ['!bundle' => $entity->bundle(), '!id' => $entity->id()])); + } + + // This line satisfies the bully that is phpstan. + assert($entity instanceof ContentEntityInterface); + $entity->set('moderation_state', $state); + $message = 'State transitioned to ' . $state; + } + if ($action) { + if (!is_a($entity, EntityPublishedInterface::class)) { + throw new \InvalidArgumentException(dt('!bundle !id does not support publish/unpublish.', ['!bundle' => $entity->bundle(), '!id' => $entity->id()])); + } if ($action === 'publish') { $entity->setPublished(); + $message = 'Published.'; } elseif ($action === 'unpublish') { $entity->setUnpublished(); + $message = 'Unpublished.'; } } if (is_a($entity, RevisionLogInterface::class)) { - $entity->setRevisionLogMessage(dt('Re-saved by Drush entity:save. Action is !action.', ['!action' => $action ?? 'none'])); + $entity->setRevisionLogMessage('Re-saved by Drush entity:save. ' . $message); + $entity->setRevisionCreationTime($this->time->getRequestTime()); + $entity->setRevisionUserId($this->currentUser->id()); + } + if (is_a($entity, EntityChangedInterface::class)) { + $entity->setChangedTime($this->time->getRequestTime()); } $entity->save(); }