From 1f36eae90a74b238696832eddc09ac199828c664 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 9 Sep 2024 15:51:35 -0400 Subject: [PATCH 1/7] entity:save supports a state transition --- src/Commands/core/EntityCommands.php | 43 ++++++++++++++++++++++------ 1 file changed, 35 insertions(+), 8 deletions(-) diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index 488ea99f31..60165f6541 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -8,10 +8,13 @@ use Consolidation\AnnotatedCommand\Input\StdinAwareTrait; use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; use Drupal\Component\Plugin\Exception\PluginNotFoundException; +use Drupal\content_moderation\ModerationInformationInterface; +use Drupal\Core\Entity\ContentEntityInterface; 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 Drush\Attributes as CLI; use Drush\Commands\AutowireTrait; @@ -26,8 +29,10 @@ 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 ModerationInformationInterface $moderationInformation + ) { parent::__construct(); } @@ -103,15 +108,16 @@ public function doDelete(string $entity_type, array $ids): void #[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: 'unpublish', description: 'Unpublish entities as they are saved.')] + #[CLI\Option(name: 'state', description: 'Transition entities to the specified Content Moderation state.')] #[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.')); @@ -123,6 +129,9 @@ public function loadSave(string $entity_type, $ids = null, array $options = ['bu } elseif ($options['unpublish']) { $action = 'unpublish'; } + + $state = $options['state'] ?? null; + if ($ids === '-') { $ids = $this->stdin()->contents(); } @@ -136,7 +145,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 +153,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 +167,35 @@ 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 { $storage = $this->entityTypeManager->getStorage($entity_type); $entities = $storage->loadMultiple($ids); foreach ($entities as $entity) { - if (is_a($entity, EntityPublishedInterface::class)) { + 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(); } elseif ($action === 'unpublish') { $entity->setUnpublished(); } } + + if ($state) { + if (!$this->moderationInformation->isModeratedEntity($entity)) { + throw new \InvalidArgumentException(dt('!bundle !id does not support content moderation.', ['!bundle' => $entity->bundle(), '!id' => $entity->id()])); + } + assert($entity instanceof ContentEntityInterface); + $entity->set('moderation_state', $state); + } + if (is_a($entity, RevisionLogInterface::class)) { - $entity->setRevisionLogMessage(dt('Re-saved by Drush entity:save. Action is !action.', ['!action' => $action ?? 'none'])); + $entity->setRevisionLogMessage(dt('Re-saved by Drush entity:save. Action is !action, State is !state.', ['!action' => $action ?? 'none', '!state' => $state ?? 'none'])); + } + if (is_a($entity, RevisionableInterface::class)) { + $entity->isDefaultRevision(true); } $entity->save(); } From 6fd68f19db65349b9564eaf156f408a245aa66e7 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 9 Sep 2024 16:08:45 -0400 Subject: [PATCH 2/7] Nullable param --- src/Commands/core/EntityCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index 60165f6541..294328fb1b 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -31,7 +31,7 @@ final class EntityCommands extends DrushCommands implements StdinAwareInterface public function __construct( protected EntityTypeManagerInterface $entityTypeManager, - protected ModerationInformationInterface $moderationInformation + protected ?ModerationInformationInterface $moderationInformation ) { parent::__construct(); } From 46da03c8708a37bbd76d84b21221fc575542feac Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Mon, 9 Sep 2024 16:29:00 -0400 Subject: [PATCH 3/7] Remove dependency --- src/Commands/core/EntityCommands.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index 294328fb1b..ce3e5787e2 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -8,7 +8,6 @@ use Consolidation\AnnotatedCommand\Input\StdinAwareTrait; use Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException; use Drupal\Component\Plugin\Exception\PluginNotFoundException; -use Drupal\content_moderation\ModerationInformationInterface; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityPublishedInterface; use Drupal\Core\Entity\EntityStorageException; @@ -31,7 +30,6 @@ final class EntityCommands extends DrushCommands implements StdinAwareInterface public function __construct( protected EntityTypeManagerInterface $entityTypeManager, - protected ?ModerationInformationInterface $moderationInformation ) { parent::__construct(); } @@ -184,9 +182,12 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string } if ($state) { - if (!$this->moderationInformation->isModeratedEntity($entity)) { + // 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); } From 9be43b186a9af8c01777f3db851871f103162953 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 10 Sep 2024 08:20:39 -0400 Subject: [PATCH 4/7] State and publish options are mutually exclusive --- src/Commands/core/EntityCommands.php | 61 ++++++++++++++++++---------- 1 file changed, 39 insertions(+), 22 deletions(-) diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index ce3e5787e2..5ebf5677f3 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -6,6 +6,7 @@ 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; @@ -15,6 +16,7 @@ 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; @@ -30,6 +32,8 @@ final class EntityCommands extends DrushCommands implements StdinAwareInterface public function __construct( protected EntityTypeManagerInterface $entityTypeManager, + protected TimeInterface $time, + protected AccountInterface $currentUser ) { parent::__construct(); } @@ -104,9 +108,9 @@ 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.')] + #[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 --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.')] @@ -118,18 +122,24 @@ public function doDelete(string $entity_type, array $ids): 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'; } - $state = $options['state'] ?? null; - if ($ids === '-') { $ids = $this->stdin()->contents(); } @@ -167,20 +177,18 @@ public function loadSave(string $entity_type, $ids = null, array $options = ['bu */ 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 ($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(); - } elseif ($action === 'unpublish') { - $entity->setUnpublished(); - } + if (is_a($entity, RevisionableInterface::class)) { + /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + $entity = $storage->createRevision($entity, true); + $entity->setRevisionCreationTime($this->time->getRequestTime()); + $entity->setChangedTime($this->time->getRequestTime()); + $entity->setRevisionUserId($this->currentUser->id()); } - if ($state) { // AutowireTrait does not support optional params so can't use DI. $moderationInformation = \Drupal::service('content_moderation.moderation_information'); @@ -190,13 +198,22 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string // This line satisfies the bully that is phpstan. assert($entity instanceof ContentEntityInterface); $entity->set('moderation_state', $state); + $message = 'State transitioned to ' . $state; } - - if (is_a($entity, RevisionLogInterface::class)) { - $entity->setRevisionLogMessage(dt('Re-saved by Drush entity:save. Action is !action, State is !state.', ['!action' => $action ?? 'none', '!state' => $state ?? 'none'])); + 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, RevisionableInterface::class)) { - $entity->isDefaultRevision(true); + if (is_a($entity, RevisionLogInterface::class)) { + $entity->setRevisionLogMessage('Re-saved by Drush entity:save. ' . $message); } $entity->save(); } From 49991030495faa264c40f5fe82cab328a1cbc193 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 10 Sep 2024 08:23:05 -0400 Subject: [PATCH 5/7] phpstan --- src/Commands/core/EntityCommands.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index 5ebf5677f3..71d76c1c56 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -185,9 +185,7 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); $entity = $storage->createRevision($entity, true); - $entity->setRevisionCreationTime($this->time->getRequestTime()); - $entity->setChangedTime($this->time->getRequestTime()); - $entity->setRevisionUserId($this->currentUser->id()); + // $entity->setChangedTime($this->time->getRequestTime()); } if ($state) { // AutowireTrait does not support optional params so can't use DI. @@ -214,6 +212,8 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string } if (is_a($entity, RevisionLogInterface::class)) { $entity->setRevisionLogMessage('Re-saved by Drush entity:save. ' . $message); + $entity->setRevisionCreationTime($this->time->getRequestTime()); + $entity->setRevisionUserId($this->currentUser->id()); } $entity->save(); } From 050480e7bb7f08ae6038d5847f5c993035b6037c Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 10 Sep 2024 08:29:39 -0400 Subject: [PATCH 6/7] Fix autoloader property in cache:crebuild --- src/Commands/core/CacheRebuildCommands.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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(); } From 394cb60888efc4e8addb163938c4c10e70c03579 Mon Sep 17 00:00:00 2001 From: Moshe Weitzman Date: Tue, 10 Sep 2024 08:47:05 -0400 Subject: [PATCH 7/7] Set changed time --- src/Commands/core/EntityCommands.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Commands/core/EntityCommands.php b/src/Commands/core/EntityCommands.php index 71d76c1c56..70a7d7b8b9 100644 --- a/src/Commands/core/EntityCommands.php +++ b/src/Commands/core/EntityCommands.php @@ -10,6 +10,7 @@ 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; @@ -185,7 +186,6 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */ $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); $entity = $storage->createRevision($entity, true); - // $entity->setChangedTime($this->time->getRequestTime()); } if ($state) { // AutowireTrait does not support optional params so can't use DI. @@ -193,6 +193,7 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string 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); @@ -215,6 +216,9 @@ public function doSave(string $entity_type, array $ids, ?string $action, ?string $entity->setRevisionCreationTime($this->time->getRequestTime()); $entity->setRevisionUserId($this->currentUser->id()); } + if (is_a($entity, EntityChangedInterface::class)) { + $entity->setChangedTime($this->time->getRequestTime()); + } $entity->save(); } }