Skip to content

Commit

Permalink
entity:save supports a state transition (#6107)
Browse files Browse the repository at this point in the history
* entity:save supports a state transition

* Nullable param

* Remove dependency

* State and publish options are mutually exclusive

* phpstan

* Fix autoloader property in cache:crebuild

* Set changed time
  • Loading branch information
weitzman committed Sep 10, 2024
1 parent a92303e commit 6a3f0cf
Show file tree
Hide file tree
Showing 2 changed files with 62 additions and 13 deletions.
2 changes: 1 addition & 1 deletion src/Commands/core/CacheRebuildCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
73 changes: 61 additions & 12 deletions src/Commands/core/EntityCommands.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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();
}

Expand Down Expand Up @@ -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();
}
Expand All @@ -136,14 +154,17 @@ 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();
$this->logger()->success(dt("Saved !type entity ids: !ids", ['!type' => $entity_type, '!ids' => implode(', ', array_values($result))]));
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]));
}
}
}

Expand All @@ -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();
}
Expand Down

0 comments on commit 6a3f0cf

Please sign in to comment.