workflow-8.x-1.x-dev/src/Entity/WorkflowTransition.php
src/Entity/WorkflowTransition.php
<?php
namespace Drupal\workflow\Entity;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Language\LanguageInterface;
use Drupal\Core\Logger\LoggerChannelTrait;
use Drupal\Core\Messenger\MessengerTrait;
use Drupal\Core\Session\AccountInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\field\Entity\FieldConfig;
use Drupal\user\EntityOwnerTrait;
use Drupal\user\UserInterface;
use Drupal\workflow\Event\WorkflowEvents;
use Drupal\workflow\Event\WorkflowTransitionEvent;
use Drupal\workflow\Hook\WorkflowEntityHooks;
use Drupal\workflow\WorkflowTypeAttributeTrait;
/**
* Implements an actual, executed, Transition.
*
* If a transition is executed, the new state is saved in the Field.
* If a transition is saved, it is saved in table {workflow_transition_history}.
*
* @ContentEntityType(
* id = "workflow_transition",
* label = @Translation("Workflow transition"),
* label_singular = @Translation("Workflow transition"),
* label_plural = @Translation("Workflow transitions"),
* label_count = @PluralTranslation(
* singular = "@count Workflow transition",
* plural = "@count Workflow transitions",
* ),
* bundle_label = @Translation("Workflow type"),
* module = "workflow",
* translatable = FALSE,
* handlers = {
* "access" = "Drupal\workflow\WorkflowAccessControlHandler",
* "list_builder" = "Drupal\workflow\WorkflowTransitionListBuilder",
* "form" = {
* "add" = "Drupal\workflow\Form\WorkflowTransitionForm",
* "delete" = "Drupal\Core\Entity\EntityDeleteForm",
* "edit" = "Drupal\workflow\Form\WorkflowTransitionForm",
* "revert" = "Drupal\workflow\Form\WorkflowTransitionRevertForm",
* },
* "views_data" = "Drupal\workflow\WorkflowTransitionViewsData",
* },
* base_table = "workflow_transition_history",
* entity_keys = {
* "id" = "hid",
* "bundle" = "wid",
* "langcode" = "langcode",
* "owner" = "uid",
* },
* permission_granularity = "bundle",
* bundle_entity_type = "workflow_type",
* field_ui_base_route = "entity.workflow_type.edit_form",
* links = {
* "canonical" = "/workflow_transition/{workflow_transition}",
* "delete-form" = "/workflow_transition/{workflow_transition}/delete",
* "edit-form" = "/workflow_transition/{workflow_transition}/edit",
* "revert-form" = "/workflow_transition/{workflow_transition}/revert",
* },
* )
*/
class WorkflowTransition extends ContentEntityBase implements WorkflowTransitionInterface {
use EntityOwnerTrait;
use LoggerChannelTrait;
use MessengerTrait;
use StringTranslationTrait;
use WorkflowTypeAttributeTrait;
/**
* The event dispatcher.
*
* @var \Symfony\Component\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* Entity class functions.
*/
/**
* Creates a new entity.
*
* No arguments passed, when loading from DB.
* All arguments must be passed, when creating an object programmatically.
* One argument $entity may be passed, only to then directly call delete().
*
* {@inheritdoc}
*
* @see entity_create()
*/
public function __construct(array $values = [], $entity_type_id = 'workflow_transition', $bundle = FALSE, array $translations = []) {
parent::__construct($values, $entity_type_id, $bundle, $translations);
$this->eventDispatcher = \Drupal::service('event_dispatcher');
// This transition is not scheduled.
$this->schedule(FALSE);
// This transition is not executed, if it has no hid, yet, upon load.
$this->setExecuted((bool) $this->id());
}
/**
* {@inheritdoc}
*/
public static function create(array $values = []): ?WorkflowTransitionInterface {
$transition = NULL;
$entity = $values['entity'] ?? NULL;
$field_name = $values['field_name'] ?? '';
// First parameter must be State object or State ID.
if (isset($values[0])) {
$values['from_sid'] = $values[0];
unset($values[0]);
}
$state = $values['from_sid'] ?? NULL;
if (is_string($state)) {
$state = WorkflowState::load($state);
}
$wid = $values['wid'] ?? NULL;
if ($state instanceof WorkflowState) {
/** @var \Drupal\workflow\Entity\WorkflowState $state */
$wid ??= $state->getWorkflowId();
$values['from_sid'] ??= $state->id();
}
// Beware for recursive call on first entity instantiation.
if (empty($wid)) {
$items = $entity?->{$field_name};
// Fieldname may exist on CommentWithWorkflow, but not on entity.
// E.g, when adding comment with workflow, on entity w/o workflow field.
// Field may empty on new CommentWithWorkflow or entity w/o workflow field.
$wid ??= $items?->getWorkflowId();
}
if (empty($wid)) {
// @todo Raise error.
// This may return NULL.
// $transition = parent::create($values);
}
else {
$values['wid'] = $wid;
if ($entity) {
unset($values['entity']);
// @todo Use baseFieldDefinition::allowed_values_function,
// but problem with entity creation, hence added explicitly here.
$values['from_sid'] ??= workflow_node_current_state($entity, $field_name);
// Overwrite 'entity_id' with Object. Strange, but identical to 'uid'.
// An entity reference,
// which allows to access entity with $transition->entity_id->entity
// and to access the entity ID with $transition->entity_id->target_id.
$values['entity_id'] = $entity;
$values['entity_type'] = $entity->getEntityTypeId();
}
// Additional default values are defined in baseFieldDefinitions().
/** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $transition */
$transition = parent::create($values);
}
return $transition;
}
/**
* {@inheritdoc}
*/
public function createDuplicate($new_class_name = WorkflowTransition::class): WorkflowTransitionInterface {
$field_name = $this->getFieldName();
$from_sid = $this->getFromSid();
$duplicate = $new_class_name::create([$from_sid, 'field_name' => $field_name]);
$duplicate->setTargetEntity($this->getTargetEntity());
$duplicate->setValues($this->getToSid(), $this->getOwnerId(), $this->getTimestamp(), $this->getComment());
$duplicate->force($this->isForced());
$attached_field_definitions = $this->getAttachedFieldDefinitions();
foreach ($attached_field_definitions as $field_name => $field) {
// @todo Support Attached fields on WorkflowScheduledTransition.
if ($duplicate->hasField($field_name)) {
$values = $this->{$field_name}->value;
$duplicate->set($field_name, $values);
}
}
return $duplicate;
}
/**
* {@inheritdoc}
*/
public function setValues($to_sid, $uid = NULL, $timestamp = NULL, $comment = NULL, $force_create = FALSE): WorkflowTransitionInterface {
// Normally, the values are passed in an array
// and set in parent::__construct, but we do it ourselves.
$from_sid = $this->getFromSid();
$this->set('to_sid', $to_sid);
if ($uid !== NULL) {
$this->setOwnerId($uid);
}
if ($timestamp !== NULL) {
$this->setTimestamp($timestamp);
}
if ($comment !== NULL) {
$this->setComment($comment);
}
// If constructor is called with new() and arguments.
if (!$from_sid && !$to_sid && !$this->getTargetEntity()) {
// If constructor is called without arguments, e.g., loading from db.
}
elseif ($from_sid && $this->getTargetEntity()) {
// Caveat: upon entity_delete, $to_sid is '0'.
// If constructor is called with new() and arguments.
}
elseif ($from_sid === NULL) {
// Not all parameters are passed programmatically.
if (!$force_create) {
$this->messenger()->addError(
$this->t('Wrong call to constructor Workflow*Transition(%from_sid to %to_sid)',
['%from_sid' => $from_sid, '%to_sid' => $to_sid]));
}
}
return $this;
}
/**
* CRUD functions.
*/
/**
* {@inheritdoc}
*
* Parameter 'force' is deprecated. Use $transition->force(TRUE)->execute();
*/
public function execute(): string {
$to_sid = $this->getToSid();
// Set the timestamp to the current moment of execution.
// Timestamp also determines $transition::is_scheduled();
$this->setTimestamp($this->getDefaultRequestTime());
if (!$this->isScheduled()) {
$this->setExecuted(TRUE);
}
$this->alterComment();
// Save the transition in {workflow_transition_history} or
// Save the transition in {workflow_transition_scheduled}.
$this->save();
return $to_sid;
}
/**
* {@inheritdoc}
*/
public function executeAndUpdateEntity(?bool $force = FALSE): string {
$to_sid = $this->getToSid();
$from_sid = $this->getFromSid();
// Check new State. Generate error and stop if transition has no new State.
// @todo Add to isAllowed() ?
// @todo Add checks to WorkflowTransitionElement ?
if ($this->isToSidOkay() === FALSE) {
return $from_sid;
}
if ($this->isScheduled()) {
// Save the (scheduled) transition. $sid is always $from_sid.
// Do not update the entity itself.
return $sid = $this->save() ? $from_sid : $from_sid;
}
if ($this->isExecuted()) {
// Updating (comments of) existing transition (on Workflow History page).
// Do not update the entity itself.
return $sid = $this->save() ? $from_sid : $from_sid;
}
if ($this->isEmpty()) {
// No need to be saved. Note: save() will do the same.
return $sid = $from_sid;
}
// Execute the new transition.
$this
// Set the timestamp to the current moment of execution.
// Timestamp also determines $transition::isScheduled();
->setTimestamp($this->getDefaultRequestTime())
// Update targetEntity's WorkflowField and ChangedTime.
->setEntityWorkflowField()
// @todo Add setEntityChangedTime() on node (not on comment).
->setEntityChangedTime();
return $sid = $this
// Save the TargetEntity. It will save this transition, too.
->getTargetEntity()->save() ? $to_sid : $from_sid;
}
/**
* {@inheritdoc}
*/
public function isExecutedAlready(): bool {
if ($this->isEmpty()) {
return FALSE;
}
static $static_info = [];
// Create a single cache key instead of deep array nesting.
$entity = $this->getTargetEntity();
// Get type_id since in 1 call, both 'node' and 'comment' can be saved.
$type_id = $entity->getEntityTypeId();
$id = $entity->id() ?? 0;
// For non-default revisions, there is no way of executing the same
// transition twice in one call. Set a random identifier
// since we won't be needing to access this variable later.
$vid = 0;
if ($entity instanceof RevisionableInterface) {
/** @var \Drupal\Core\Entity\RevisionableInterface $entity */
if (!$entity->isDefaultRevision()) {
$vid = $entity->getRevisionId();
}
}
$field_name = $this->getFieldName();
$from_sid = $this->getFromSid();
$to_sid = $this->getToSid();
$cache_key = "{$type_id}:{$id}:{$vid}:{$field_name}:{$from_sid}:{$to_sid}";
if (!isset($static_info[$cache_key])) {
// OK. Prepare for next round.
$static_info[$cache_key] = TRUE;
return FALSE;
}
// Error: this Transition is already executed.
// On the development machine, execute() is called twice, when
// on an Edit Page, the entity has a scheduled transition, and
// user changes it to 'immediately'.
// Why does this happen?? ( BTW. This happens with every submit.)
// Remedies:
// - search root cause of second call.
// - try adapting code of transition->save() to avoid second record.
// - avoid executing twice.
$message = 'Transition is executed twice in a call. The second call for
@entity_type %entity_id is not executed.';
$this->logError($message);
// Return the result of the last call.
return $static_info[$cache_key];
}
/**
* {@inheritdoc}
*/
public function fail(): static {
$from_sid = $this->getFromSid();
$to_state = $this->getToState();
$comment = $this->getComment();
// Overwrite, make this a same-state transition.
$this->setValues($from_sid);
$this->setComment("{$comment} (Transition failed. State not set to $to_state).");
// Set transition, so it can be fetched in executeTransitionsOfEntity().
$this->setEntityWorkflowField();
return $this;
}
/**
* {@inheritdoc}
*
* Prerequisite: make sure that the latest version of $entity is referenced.
*
* @todo Also update entity with additional fields.
*/
public function setEntityWorkflowField(?bool &$is_updated = FALSE): static {
$entity = $this->getTargetEntity();
$field_name = $this->getFieldName();
$to_sid = $this->getToSid();
try {
// Set the Transition to the field. This also sets value to the State ID.
$entity->{$field_name}->setValue($this);
$is_updated = !$this->isScheduled() && $this->hasStateChange();
}
catch (\Error $e) {
// Exception: Error: Call to a member function setValue() on null.
// Happens when adding CommentWithWorkflow to mismatched Node.
$message = $this->t('A comment with Workflow field is added to a Content type. Both %entity_type_id and Comment must share the same field name %field_name, or else the comment value cannot be added to the %entity_type_id.',
[
'%entity_type_id' => $entity->getEntityTypeId(),
'%field_name' => $field_name,
]);
$this->messenger()->addError($message);
}
return $this;
}
/**
* {@inheritdoc}
*/
public function setEntityChangedTime(?bool &$is_updated = FALSE): static {
if (!$this->getWorkflow()->getSetting('always_update_entity')) {
return $this;
}
if ($this->isScheduled()) {
return $this;
}
if ($this->isEmpty()) {
return $this;
}
if (WorkflowManager::isTargetCommentEntity($this)) {
// Do not change the CommentWithWorkflow. Change the node, instead.
return $this;
}
$entity = $this->getTargetEntity();
// Copied from EntityFormDisplay::updateChangedTime(EntityInterface $entity)
if ($entity instanceof EntityChangedInterface) {
$entity->setChangedTime($this->getTimestamp());
$is_updated = TRUE;
}
return $this;
}
/**
* {@inheritdoc}
*
* Using WT::preSave() is too late. Use E::preSaveTransitionsOfEntity().
*/
public function preSave(EntityStorageInterface $storage) {
parent::preSave($storage);
if (!$this->isScheduled()) {
$this->setExecuted(TRUE);
}
}
/**
* Saves the entity.
*
* Mostly, you'd better use WorkflowTransitionInterface::execute().
*
* {@inheritdoc}
*/
public function save() {
if ($this->isEmpty()) {
// Empty transition.
$result = SAVED_UPDATED;
return $result;
}
if ($this->isScheduled()) {
if ($this->getEntityTypeId() == 'workflow_transition') {
// Convert/cast/wrap Transition to ScheduledTransition or v.v.
$transition = $this->createDuplicate(WorkflowScheduledTransition::class);
$transition->setEntityWorkflowField();
$result = $transition->save();
return $result;
}
}
// @todo $entity->revision_id is NOT SET when coming from node/XX/edit !!
$field_name = $this->getFieldName();
$entity = $this->getTargetEntity();
$entity->getRevisionId();
// Set Target Entity, to be used by Rules.
/** @var \Drupal\workflow\Entity\WorkflowTransitionInterface $reference */
if ($reference = $this->get('entity_id')->first()) {
$reference->set('entity', $entity);
}
$this->dispatchEvent(WorkflowEvents::PRE_TRANSITION);
switch (TRUE) {
case $this->isEmpty():
// Empty transition.
$result = SAVED_UPDATED;
break;
case $this->getEntityTypeId() == 'workflow_scheduled_transition':
// Update a scheduled workflow_scheduled_transition.
// Avoid custom actions for subclass WorkflowScheduledTransition.
if ($this->isNew()) {
WorkflowEntityHooks::deleteTransitionsOfEntity($entity, 'workflow_scheduled_transition', $field_name);
}
$result = parent::save();
break;
case $this->isScheduled():
// Create, update a scheduled workflow_transition.
// Avoid custom actions for subclass WorkflowScheduledTransition.
$result = parent::save();
break;
case $this->id() && $this->isExecuted():
// Update the transition (on history tab page). It already exists.
// Do not delete an existing scheduled transition.
$result = parent::save();
break;
case $this->id():
// Update the transition. It already exists.
WorkflowEntityHooks::deleteTransitionsOfEntity($entity, 'workflow_scheduled_transition', $field_name);
$result = parent::save();
break;
default:
// Insert the executed transition, unless it has already been inserted.
// Note: this might be outdated due to code improvements.
// @todo Allow a scheduled transition per revision.
// @todo Allow a state per language version (langcode).
WorkflowEntityHooks::deleteTransitionsOfEntity($entity, 'workflow_scheduled_transition', $field_name);
// @todo Compare with WT::isExecutedAlready().
// $twice = $this->isExecutedAlready();
$same_transition = self::loadByProperties($entity->getEntityTypeId(), $entity->id(), [], $field_name);
if ($same_transition &&
$same_transition->getTimestamp() == $this->getDefaultRequestTime() &&
$same_transition->getToSid() == $this->getToSid()) {
$result = SAVED_UPDATED;
}
else {
$result = parent::save();
}
break;
}
$this->dispatchEvent(WorkflowEvents::POST_TRANSITION);
\Drupal::moduleHandler()->invokeAll('workflow', ['transition post', $this, $this->getOwner()]);
$this->addPostSaveMessage();
return $result;
}
/**
* {@inheritdoc}
*/
public function dispatchEvent($event_name) {
$transition_event = new WorkflowTransitionEvent($this);
$this->eventDispatcher->dispatch($transition_event, $event_name);
return $this;
}
/**
* Generates a message after the Transition has been saved.
*/
protected function addPostSaveMessage() {
if (!empty($this->getWorkflow()->getSetting('watchdog_log'))) {
return $this;
}
if ($this->isExecuted() && $this->hasStateChange()) {
// Log the state change.
$message = match ($this->getEntityTypeId()) {
'workflow_scheduled_transition'
=> 'Scheduled state change of @entity_type_label %entity_label to %sid2 executed',
default
=> 'State of @entity_type_label %entity_label set to %sid2',
};
$this->logError($message, 'notice');
}
return $this;
}
/**
* {@inheritdoc}
*
* When a TargetEntity is updated, also its transitions must be invalidated.
* The use case for this is 'Workflow Entity history' view, where the 'revert'
* operation must be recalculated when new Transition is added.
*/
public function getCacheTagsToInvalidate() {
$tags = parent::getCacheTagsToInvalidate();
// Add 'node:NID' as CacheTag, next to 'workflow_transition:HID'.
$entity = $this->getTargetEntity();
if ($entity !== NULL) {
// Only for WorkflowTransitions, when target already set.
$tags = Cache::mergeTags($tags, $entity->getCacheTags());
}
return $tags;
}
/**
* {@inheritdoc}
*
* This function only serves debugging and php var typing.
*/
public static function load($id): ?WorkflowTransitionInterface {
$transition = parent::load($id);
return $transition;
}
/**
* {@inheritdoc}
*/
public static function loadByProperties($entity_type_id, $entity_id, array $revision_ids = [], $field_name = '', $langcode = '', $sort = 'ASC', $transition_type = 'workflow_transition'): ?WorkflowTransitionInterface {
$limit = 1;
$transitions = self::loadMultipleByProperties($entity_type_id, [$entity_id], $revision_ids, $field_name, $langcode, $limit, $sort, $transition_type);
if ($transitions) {
$transition = reset($transitions);
return $transition;
}
return NULL;
}
/**
* {@inheritdoc}
*/
public static function loadMultipleByProperties($entity_type_id, array $entity_ids, array $revision_ids = [], $field_name = '', $langcode = '', $limit = NULL, $sort = 'ASC', $transition_type = 'workflow_transition'): array {
/** @var \Drupal\Core\Entity\Query\QueryInterface $query */
$query = \Drupal::entityQuery($transition_type)
->condition('entity_type', $entity_type_id)
->accessCheck(FALSE)
->sort('timestamp', $sort)
->addTag($transition_type);
if (!empty($entity_ids)) {
$query->condition('entity_id', $entity_ids, 'IN');
}
if (!empty($revision_ids)) {
$query->condition('revision_id', $revision_ids, 'IN');
}
if ($field_name != '') {
$query->condition('field_name', $field_name, '=');
}
if ($langcode != '') {
$query->condition('langcode', $langcode, '=');
}
if ($limit) {
$query->range(0, $limit);
}
if ($transition_type == 'workflow_transition') {
$query->sort('hid', 'DESC');
}
$ids = $query->execute();
$transitions = $ids ? self::loadMultiple($ids) : [];
return $transitions;
}
/**
* Implementing interface WorkflowTransitionInterface - properties.
*/
/**
* {@inheritdoc}
*/
public static function loadBetween($start = 0, $end = 0, $from_sid = '', $to_sid = '', $type = 'workflow_transition'): array {
/** @var \Drupal\Core\Entity\Query\QueryInterface $query */
$query = \Drupal::entityQuery($type)
->sort('timestamp', 'ASC')
->accessCheck(FALSE)
->addTag($type);
if ($start) {
$query->condition('timestamp', $start, '>');
}
if ($end) {
$query->condition('timestamp', $end, '<');
}
if ($from_sid) {
$query->condition('from_sid', $from_sid, '=');
}
if ($to_sid) {
$query->condition('to_sid', $to_sid, '=');
}
$ids = $query->execute();
$transitions = $ids ? self::loadMultiple($ids) : [];
return $transitions;
}
/**
* {@inheritdoc}
*/
public function alterComment(): static {
if ($this->isScheduled()) {
return $this;
}
// The transition is allowed and must be executed now.
// Let other modules modify the comment.
$comment = $this->getComment();
// The transition (in $context) contains all relevant data.
$context = ['transition' => $this];
\Drupal::moduleHandler()->alter('workflow_comment', $comment, $context);
$this->setComment($comment);
return $this;
}
/**
* Generate error and stop if transition has no new State.
*
* @return bool
* TRUE if the test is OK, else FALSE.
*/
public function isToSidOkay(): bool {
$status = TRUE;
$to_sid = $this->getToSid();
if (!$to_sid) {
$entity = $this->getTargetEntity();
$t_args = [
'%sid2' => $this->getToState()->label(),
'%entity_label' => $entity->label(),
];
$message = "Transition is not executed for %entity_label, since 'To' state %sid2 is invalid.";
$this->logError($message);
$this->messenger()->addError($this->t($message, $t_args));
return FALSE;
}
return $status;
}
/**
* {@inheritdoc}
*
* @todo Add to isAllowed() ?
* @todo Add checks to WorkflowTransitionElement ?
*/
public function isValid(): bool {
$valid = TRUE;
// Load the entity, if not already loaded.
// This also sets the (empty) $revision_id in Scheduled Transitions.
$entity = $this->getTargetEntity();
$user = $this->getOwner();
$force = $this->isForced();
if (!$entity) {
// @todo There is a logger error, but no UI-error. Is this OK?
$message = 'User tried to execute a Transition without an entity.';
$this->logError($message);
return FALSE;
}
if (!$this->getFieldName()) {
// @todo The page is not correctly refreshed after this error.
$message = $this->t('The entity is not relevant for setting
a Workflow State. Please contact your system administrator.');
$this->messenger()->addError($message);
$message = 'Setting a non-relevant Entity from state %sid1 to %sid2';
$this->logError($message);
return FALSE;
}
// @todo Move below code to $this->isAllowed().
// If the state has changed, check the permissions.
// No need to check if Comments or attached fields are filled.
if ($this->hasStateChange()) {
if (!$this->isAllowed($user, $force)) {
$message = 'User %user not allowed to go from state %sid1 to %sid2';
$this->logError($message);
return FALSE; // <-- exit !!!
}
}
if ($this->hasStateChange()) {
// Make sure this transition is valid and allowed for the current user.
// Invoke a callback indicating a transition is about to occur.
// Modules may veto the transition by returning FALSE.
// (Even if $force is TRUE, but they shouldn't do that.)
// P.S. The D7 hook_workflow 'transition permitted' is removed,
// in favour of below hook_workflow 'transition pre'.
$permitted = \Drupal::moduleHandler()->invokeAll('workflow', ['transition pre', $this, $user]);
// Stop if a module says so.
if (in_array(FALSE, $permitted, TRUE)) {
// @todo There is a logger error, but no UI-error. Is this OK?
$message = 'Transition vetoed by module.';
$this->logError($message, 'notice');
return FALSE; // <-- exit !!!
}
}
return $valid;
}
/**
* {@inheritdoc}
*/
public function isEmpty(): bool {
if ($this->hasStateChange()) {
return FALSE;
}
if ($this->getComment()) {
return FALSE;
}
$attached_field_definitions = $this->getAttachedFieldDefinitions();
foreach ($attached_field_definitions as $field_name => $field) {
if (isset($this->{$field_name}) && !$this->{$field_name}->isEmpty()) {
return FALSE;
}
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function isAllowed(UserInterface $user, $force = FALSE): bool {
$result = FALSE;
$user = workflow_current_user($user);
// Do some performant checks before checking each possible transition.
// N.B. Keep aligned between WorkflowState, ~Transition, ~HistoryAccess.
if ($force) {
return TRUE;
}
if (!$this->hasStateChange()) {
// Anyone may save an entity without changing state.
return TRUE;
}
if ($user->isSuperUser($this)) {
// Get permission from admin/people/permissions page.
// Superuser is special (might be cron).
// And $force allows Rules to cause transition.
return TRUE;
}
$workflow = $this->getWorkflow();
$from_sid = $this->getFromSid();
$to_sid = $this->getToSid();
// Determine if user is owner of the target entity.
// If so, add role, to check the config_transition.
if ($user->isOwner($this)) {
$user->addOwnerRole($this);
}
// Determine if user has Access to each transition.
$config_transitions = $workflow->getTransitionsByStateId($from_sid, $to_sid);
foreach ($config_transitions as $config_transition) {
$result = $result || $config_transition->isAllowed($user, $force);
}
if ($result == FALSE) {
// @todo There is a logger error, but no UI-error. Is this OK?
$message = "Attempt to go to nonexistent transition (from $from_sid to $to_sid)";
$this->logError($message);
}
return $result;
}
/**
* {@inheritdoc}
*/
public function hasStateChange(): bool {
return $this->getFromSid() !== $this->getToSid();
}
/**
* {@inheritdoc}
*/
public function setTargetEntity(EntityInterface $entity): static {
$this->entity_type = '';
$this->entity_id = NULL;
$this->revision_id = '';
$this->langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED;
if ($entity) {
$this->set('entity_id', $entity);
/** @var \Drupal\Core\Entity\RevisionableContentEntityBase $entity */
$this->entity_type = $entity->getEntityTypeId();
$this->entity_id = $entity;
$this->revision_id = $entity->getRevisionId();
$this->langcode = $entity->language()->getId();
}
return $this;
}
/**
* {@inheritdoc}
*/
public function getTargetEntity(): ?EntityInterface {
$entity = $this->entity_id->entity;
if ($entity) {
return $entity;
}
$entity_id = $this->entity_id->target_id;
if ($entity_id ??= $this->getTargetEntityId()) {
$entity_type_id = $this->getTargetEntityTypeId();
$entity = \Drupal::entityTypeManager()->getStorage($entity_type_id)->load($entity_id);
$this->entity_id = $entity;
}
return $entity;
}
/**
* {@inheritdoc}
*/
public function getTargetEntityId() {
return $this->get('entity_id')->target_id;
}
/**
* {@inheritdoc}
*/
public function getTargetEntityTypeId(): string {
return $this->get('entity_type')->value ?? '';
}
/**
* {@inheritdoc}
*/
public function getFieldName(): string {
// Can be empty when adding new (file upload) field on
// admin/config/workflow/workflow/TYPE/add-field/workflow_transition.
return $this->get('field_name')->value ?? '';
}
/**
* Returns the label for the transition's field.
*
* @return string
* The label of the field, or empty if not set.
*/
public function getFieldLabel(): string {
$entity = $this->getTargetEntity();
$field_name = $this->getFieldName();
$label = $entity?->{$field_name}?->getFieldLabel();
return $label;
}
/**
* {@inheritdoc}
*/
public function getLangcode(): string {
return $this->getTargetEntity()->language()->getId();
}
/**
* {@inheritdoc}
*/
public function getFromState(): ?WorkflowState {
$state = $this->{'from_sid'}->entity ?? NULL;
$state ??= $this->getWorkflow()?->getState($this->getFromSid());
return $state;
}
/**
* {@inheritdoc}
*/
public function getToState(): ?WorkflowState {
$state = $this->{'to_sid'}->entity ?? NULL;
$state ??= $this->getWorkflow()->getState($this->getToSid());
return $state;
}
/**
* {@inheritdoc}
*/
public function getFromSid(): string {
// BaseField is defined as 'list_string'.
$sid = $this->{'from_sid'}->value ?? NULL;
// BaseField is defined as 'entity_reference'.
$sid ??= $this->{'from_sid'}->target_id ?? '';
return $sid;
}
/**
* {@inheritdoc}
*/
public function getToSid(): string {
// BaseField is defined as 'list_string'.
$sid = $this->{'to_sid'}->value ?? NULL;
// BaseField is defined as 'entity_reference'.
$sid ??= $this->{'to_sid'}->target_id ?? '';
return $sid;
}
/**
* {@inheritdoc}
*/
public function getWorkflowId(): ?string {
if (!empty($this->wid)) {
return $this->wid;
}
try {
$value = $this->get('wid');
$wid = match (TRUE) {
// 'entity_reference' in WorkflowTransition.
is_object($value) => $value->{'target_id'} ?? '',
// 'list_string' in WorkflowTransition.
is_string($value) => $value,
};
if (empty($wid)) {
// Field name can be empty when attaching fields to WT in Field UI.
if ($field_name = $this->getFieldName()) {
$state = $this->getFromState();
$wid = $state?->getWorkflowId();
}
}
$this->setWorkflowId($wid);
}
catch (\UnhandledMatchError $e) {
workflow_debug(__FILE__, __FUNCTION__, __LINE__, '', '');
}
return $wid;
}
/**
* {@inheritdoc}
*/
public function getPossibleValues(?AccountInterface $account = NULL) {
return array_keys($this->getPossibleOptions($account));
}
/**
* {@inheritdoc}
*/
public function getPossibleOptions(?AccountInterface $account = NULL) {
// Prepare user for WorkflowState::getTransitions();
// $user->hasPermission("bypass $type_id workflow_transition access").
$user = workflow_current_user($account);
$user = $user->addSuperUserRole($this);
return $this->getSettableOptions($user);
}
/**
* {@inheritdoc}
*/
public function getSettableValues(?AccountInterface $account = NULL) {
return array_keys($this->getSettableOptions($account));
}
/**
* {@inheritdoc}
*/
public function getSettableOptions(?AccountInterface $account = NULL, string $field_name = 'to_sid'): array {
$allowed_options = [];
$from_state = $this->getFromState();
$to_state = $this->getToState();
// Early return for executed transitions.
if ($this->isExecuted()) {
// We are on the Workflow History page/view
// (or any other Views display displaying State names)
// or are editing an existing/executed/not-scheduled transition,
// where only the comments may be changed!
// Both From state and To state may not be changed anymore.
$state = match ($field_name) {
'from_sid' => $from_state,
'to_sid' => $to_state,
};
$allowed_options = [$state->id() => $state->label()];
return $allowed_options;
}
$allowed_options = match ($field_name) {
'from_sid' => $from_state
// From_state only has 1 option: its own value.
? [$from_state->id() => $from_state->label()]
: [],
'to_sid' => $from_state
// Caveat: For $to_sid, get the options from $from_sid.
? $from_state->getOptions($this, $field_name, $account)
: $this->getWorkflow()->getStates(),
default => [],
};
return $allowed_options;
}
/**
* {@inheritdoc}
*/
public function getComment(): ?string {
return $this->get('comment')->value;
}
/**
* {@inheritdoc}
*/
public function setComment($value): static {
$this->set('comment', $value);
return $this;
}
/**
* {@inheritdoc}
*/
public static function getDefaultRequestTime(?WorkflowTransitionInterface $transition = NULL, ?BaseFieldDefinition $definition = NULL) {
$timestamp = \Drupal::time()->getRequestTime();
if ($definition) {
// Called from object creation.
// Round timestamp to previous minute. This way:
// - the widget can be displayed without seconds;
// - is the default time always in the past, and not 'scheduled'.
$timestamp = floor($timestamp / 60) * 60;
}
return $timestamp;
}
/**
* {@inheritdoc}
*/
public static function getDefaultStateId(WorkflowTransitionInterface $transition, BaseFieldDefinition $definition) {
$sid = '';
$field_name = $transition->getFieldName();
switch ($definition->getName()) {
case 'from_sid':
$entity = $transition->getTargetEntity();
if ($entity) {
$sid = workflow_node_current_state($entity, $field_name);
if (!$sid) {
\Drupal::logger('workflow_action')->notice('Unable to get current workflow state of entity %id.', ['%id' => $entity->id()]);
}
}
else {
// Entity is not set when adding a field on
// admin/config/workflow/workflow/TYPE/add-field/workflow_transition/FIELD_NAME .
$sid = $transition->getWorkflow()->getCreationState()->id();
}
break;
case 'to_sid':
$current_state = $transition->getFromState();
if ($current_state) {
$sid = match ($current_state->isCreationState()) {
FALSE => $current_state->id(),
TRUE => $current_state->getWorkflow()->getFirstSid(
$transition,
$field_name,
$transition->getOwner()),
};
}
break;
default:
// Error. Should not happen.
break;
}
return $sid;
}
/**
* {@inheritdoc}
*/
public function getTimestamp(): int {
$timestamp = $this->get('timestamp')->value;
if (is_string($timestamp)) {
// @todo Why/When is timestamp set as string?
return (int) $timestamp;
}
if ($timestamp instanceof DrupalDateTime) {
$timezone = $this->get('timestamp')->timezone ?? NULL;
// N.B. keep aligned: WorkflowTransition::getTimestamp()
// and Workflow DateTimeZoneWidget::massageFormValues.
// We now override the value with the entered value converted into the
// selected timezone, and then DateTimeWidgetBase converts this value
// into UTC for storage.
$timestamp = new DrupalDateTime(
$timestamp->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT),
new \DateTimezone($timezone));
$timestamp = $timestamp->getTimestamp();
}
return $timestamp;
}
/**
* {@inheritdoc}
*/
public function getTimestampFormatted(?int $timestamp = NULL): string {
$timestamp ??= $this->getTimestamp();
return \Drupal::service('date.formatter')->format($timestamp);
}
/**
* {@inheritdoc}
*/
public function setTimestamp(int $timestamp): static {
$this->set('timestamp', $timestamp);
$request_time = $this->getDefaultRequestTime();
// The timestamp determines if the Transition is scheduled or not.
$is_scheduled = ($timestamp - 60) > $request_time;
$this->schedule($is_scheduled);
return $this;
}
/**
* {@inheritdoc}
*/
public function isRevertible(): bool {
// Some states are useless to revert.
if (!$this->hasStateChange()) {
return FALSE;
}
// Some states are not fit to revert to.
$from_state = $this->getFromState();
if (!$from_state
|| !$from_state->isActive()
|| $from_state->isCreationState()) {
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function schedule(bool $schedule): static {
return $this->set('scheduled', (int) $schedule);
}
/**
* {@inheritdoc}
*/
public function isScheduled(): bool {
return $this->get('scheduled')->value ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function setExecuted(bool $isExecuted = TRUE): static {
return $this->set('executed', $isExecuted);
}
/**
* {@inheritdoc}
*/
public function isExecuted(): bool {
return $this->get('executed')->value ?? FALSE;
}
/**
* {@inheritdoc}
*/
public function force(bool $force = TRUE): static {
return $this->set('force', $force);
}
/**
* {@inheritdoc}
*/
public function isForced(): bool {
return $this->get('force')->value ?? FALSE;
}
/**
* Implementing interface FieldableEntityInterface extends EntityInterface.
*/
/**
* Get additional fields of workflow(_scheduled)_transition.
*
* {@inheritdoc}
*
* @internal Manipulation of (attached) fields.
*/
public function getFieldDefinitions(): array {
return parent::getFieldDefinitions();
}
/**
* {@inheritdoc}
*
* @internal Manipulation of (attached) fields.
*/
public function getAttachedFieldDefinitions(): array {
// Determine the fields added by Field UI.
$fields = $this->getFieldDefinitions();
$attached_fields = array_filter($fields, fn($field)
=> $field instanceof FieldConfig
);
return $attached_fields;
}
/**
* Adds the attached fields from the element to the transition.
*
* Caveat: This works automatically on a Workflow Form,
* but only with a hack on a widget.
*
* @todo This line seems necessary for node edit, not for node view.
* @todo Support 'attached fields' in ScheduledTransition.
*
* @param array $form
* The form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return \Drupal\workflow\Entity\WorkflowTransitionInterface
* The Transition object.
*
* @internal Manipulation of (attached) fields.
* @todo For Scheduled transition, also add attached fields on the form.
* @deprecated in workflow:2.1.9 and is removed from workflow:3.0.0.
*/
public function copyAttachedFields(array $form, FormStateInterface $form_state): static {
// @todo Nested WT, like User with Paragraphs with Workflow.
// Following line may generate Warning: Undefined array key.
// $values = $form_state->getValues()[$this->getFieldName()];
$values = $form_state->getValues();
$attached_field_definitions = $this->getAttachedFieldDefinitions();
foreach ($attached_field_definitions as $field_name => $field) {
// As per v2.1.8, widget behaves as per core standards.
// The following line will remove values from $transition,
// So they are removed.
// Instead, $values is additionally passed to hook.
if (isset($values[$field_name])) {
// $field_values = $values[$field_name];
// $this->{$field_name} = $field_values;
// if ($item = $this->{$field_name}->first()) {
// if ($item && !$item->isEmpty()) {
// $main_property = $item?->mainPropertyName();
// $value = $item->__get($main_property);
// }
// }
}
// For each field, let other modules modify the copied values,
// as a workaround for not-supported attached field types.
// @see https://www.drupal.org/project/workflow/issues/2899025
$input ??= $form_state->getUserInput();
$context = [
'form' => $form,
'form_state' => $form_state,
'field' => $field,
'field_name' => $field_name,
'user_input' => $input[$field_name] ?? [],
'values' => $values,
'item' => $values,
];
// Wrongly named alter hook until version 2.1.7.
\Drupal::moduleHandler()->alter('copy_form_values_to_transition_field', $this, $context);
// Correctly named alter hook from version 2.1.8.
\Drupal::moduleHandler()->alter('workflow_copy_form_values_to_transition_field', $this, $context);
}
return $this;
}
/**
* {@inheritdoc}
*/
public static function baseFieldDefinitions(EntityTypeInterface $entity_type): array {
$fields = [];
$fields['hid'] = BaseFieldDefinition::create('integer')
->setLabel(t('Transition ID'))
->setDescription(t('The transition ID.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$fields['wid'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Workflow Type'))
->setDescription(t('The workflow type the transition relates to.'))
->setRequired(TRUE)
->setSetting('target_type', 'workflow_type')
->setRevisionable(FALSE)
->setTranslatable(FALSE);
$fields['entity_type'] = BaseFieldDefinition::create('string')
->setLabel(t('Entity type'))
->setDescription(t('The Entity type this transition belongs to.'))
->setReadOnly(TRUE)
->setSetting('is_ascii', TRUE)
->setSetting('max_length', EntityTypeInterface::ID_MAX_LENGTH);
// An entity reference,
// which allows to access the entity ID with $node->entity_id->target_id
// and to access the entity itself with $node->uid->entity.
$fields['entity_id'] = BaseFieldDefinition::create('entity_reference')
->setLabel(t('Entity ID'))
->setDescription(t('The Entity ID this record is for.'))
->setReadOnly(TRUE)
->setRequired(TRUE);
$fields['revision_id'] = BaseFieldDefinition::create('integer')
->setLabel(t('Revision ID'))
->setDescription(t('The current version identifier.'))
->setReadOnly(TRUE)
->setSetting('unsigned', TRUE);
$fields['field_name'] = BaseFieldDefinition::create('list_string')
->setLabel(t('Field name'))
->setDescription(t('The name of the field the transition relates to.'))
->setCardinality(1)
// Field name is technically required, but in widget is not.
->setRequired(FALSE)
->setDisplayConfigurable('form', FALSE)
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => -1,
])
->setSetting('allowed_values_function', 'workflow_field_allowed_values')
// Value must be set by parameters upon creation.
// ->setDefaultValueCallback(static::getName(...))
->setRevisionable(FALSE)
->setTranslatable(FALSE);
$fields['langcode'] = BaseFieldDefinition::create('language')
->setLabel(t('Language'))
->setDescription(t('The entity language code.'))
->setTranslatable(TRUE);
$fields['delta'] = BaseFieldDefinition::create('integer')
->setLabel(t('Delta'))
->setDescription(t('The sequence number for this data item, used for multi-value fields.'))
->setReadOnly(TRUE)
// Only single value is supported.
->setDefaultValue(0);
// Set $fields['uid'].
// The uid is an entity reference to the user entity type,
// which allows to access the user ID with $node->uid->target_id
// and to access the user entity with $node->uid->entity.
$fields += static::ownerBaseFieldDefinitions($entity_type);
$fields['uid']
->setDescription(t('The user ID of the transition author.'))
// ->setDefaultValueCallback('workflow_current_user')
->setDefaultValueCallback(static::class . '::getDefaultEntityOwner')
->setRevisionable(TRUE);
$fields['from_sid'] = BaseFieldDefinition::create('list_string')
->setLabel(t('Current state'))
->setDescription(t('The current/previous state of the the entity.'))
->setCardinality(1)
->setDefaultValueCallback(static::class . '::getDefaultStateId')
// The 'required' asterisk from BaseField will be removed in the form.
->setRequired(TRUE)
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => -1,
])
->setSetting('target_type', 'workflow_state')
// Don't change. @see https://www.drupal.org/project/drupal/issues/2643308
// Note: this is not used for entity_reference fields, only list_* fields.
->setSetting('allowed_values_function', 'workflow_state_allowed_values')
->setReadOnly(TRUE);
$fields['to_sid'] = BaseFieldDefinition::create('list_string')
->setLabel(t('To state'))
->setDescription(t('The new state of the entity.'))
->setCardinality(1)
->setDefaultValueCallback(static::class . '::getDefaultStateId')
// The 'required' asterisk from BaseField will be removed in the form.
->setRequired(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'options_select',
'weight' => 0,
])
->setSetting('target_type', 'workflow_state')
// Don't change. @see https://www.drupal.org/project/drupal/issues/2643308
// Note: this is not used for entity_reference fields, only list_* fields.
->setSetting('allowed_values_function', 'workflow_state_allowed_values');
$fields['scheduled'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Schedule the state change'))
->setDescription(t('A scheduled transition
will be executed automatically on a later moment of time.'))
->setCardinality(1)
->setComputed(TRUE)
// Use int/string '0', not boolean FALSE, for select element.
->setDefaultValue(0)
// The 'required' asterisk from BaseField will be removed in the form.
->setRequired(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
// 'options_buttons', 'options_select', 'boolean_checkbox'.
// For regression reasons, use radios, but checkbox is nicer.
'type' => 'options_buttons',
// 'type' => 'boolean_checkbox',
// @todo Setting 'display_label' => FALSE does not seem to work.
'weight' => 1,
])
->setSettings([
'on_label' => t('Schedule for state change'),
'off_label' => t('Immediately'),
])
->setRevisionable(FALSE);
$fields['timestamp'] = BaseFieldDefinition::create('created')
->setLabel(t('Timestamp'))
->setDescription(t('The time that the current transition was executed.'))
->setCardinality(1)
->setDefaultValueCallback(static::class . '::getDefaultRequestTime')
->setDisplayConfigurable('form', FALSE)
// @todo Make configurable, but align/overwrite setting vs.FormDisplay
// So schedule/timezone can be set via 'Manage form display' settings.
// ->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'workflow_datetime_timestamp_timezone',
// The 'scheduled' checkbox is directly above 'timestamp' widget.
'weight' => 1.005,
])
->setRevisionable(TRUE);
$fields['comment'] = BaseFieldDefinition::create('string_long')
->setLabel(t('Comment'))
->setDescription(t('Briefly describe the changes you have made.'))
->setCardinality(1)
->setDefaultValue('')
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'textarea',
'weight' => 2,
])
->setRevisionable(TRUE)
->setTranslatable(TRUE);
$fields['force'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Force transition'))
->setDescription(t('If this box is checked, the new state will be
assigned even if workflow permissions disallow it.'))
->setCardinality(1)
->setComputed(TRUE)
// Use int/string '0', not boolean FALSE, for select element.
->setDefaultValue(0)
// The 'required' asterisk from BaseField will be removed in the form.
->setRequired(TRUE)
->setDisplayConfigurable('form', TRUE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
'weight' => 3,
])
->setRevisionable(FALSE);
$fields['executed'] = BaseFieldDefinition::create('boolean')
->setLabel(t('Transition is executed'))
->setDescription(t('The transition
is already executed in a previous moment of time.'))
->setCardinality(1)
->setComputed(TRUE)
// Do not show on form.
->setDisplayConfigurable('form', FALSE)
->setDisplayOptions('form', [
'type' => 'boolean_checkbox',
])
->setDefaultValue(FALSE)
->setRevisionable(FALSE);
return $fields;
}
/**
* Generate a Logger error.
*
* @param string $message
* The message.
* @param string $level
* The message type {'error' | 'notice'}.
* @param string $from_sid
* The old State ID.
* @param string $to_sid
* The new State ID.
*/
public function logError($message, $level = 'error', $from_sid = '', $to_sid = '') {
// Prepare an array of arguments for error messages.
$entity = $this->getTargetEntity();
$context = [
'%user' => ($user = $this->getOwner()) ? $user->getDisplayName() : '',
'%sid1' => ($from_sid || !$this->getFromState()) ? $from_sid : $this->getFromState()->label(),
'%sid2' => ($to_sid || !$this->getToState()) ? $to_sid : $this->getToState()->label(),
'%entity_id' => $this->getTargetEntityId() ?? '',
'%entity_label' => $entity?->label() ?? '',
'@entity_type' => $entity?->getEntityTypeId() ?? '',
'@entity_type_label' => $entity?->getEntityType()->getLabel() ?? '',
'link' => ($entity->id() && $entity->hasLinkTemplate('canonical'))
? $entity->toLink($this->t('View'))->toString()
: '',
];
$this->getLogger('workflow')->log($level, $message, $context);
}
/**
* {@inheritdoc}
*
* @internal For testing purposes.
*/
public function dpm($function = NULL): static {
if (!function_exists('dpm')) {
return $this;
}
$stack = debug_backtrace();
$function ??= $stack[2]['function'] . '/' . ($stack[1]['line'] ?? '??')
. ' > ' . $stack[1]['function'] . '/' . ($stack[0]['line'] ?? '??');
$transition = $this;
$transition_id = $this->id() ?: 'NEW';
$transition_type = $transition->getEntityTypeId();
$entity = $transition->getTargetEntity();
$type_id = $this->getTargetEntityTypeId();
$bundle = $entity?->bundle() ?? '___';
$id = $entity?->id() ?? '_';
$vid = ($entity instanceof RevisionableInterface)
/** @var \Drupal\Core\Entity\RevisionableInterface $entity */
? $entity->getRevisionId() ?? 'null'
: '_';
$time = \Drupal::service('date.formatter')->format($transition->getTimestamp() ?? 0);
$user = $transition->getOwner();
$user_name = $user?->getDisplayName() ?? 'unknown username';
$spaces = ' ';
$t_string = "$transition_type $transition_id for workflow_type <i>{$this->getWorkflowId()}</i> in function '$function'";
$output[] = "Entity type/bundle/id/vid = $type_id/$bundle/$id/$vid @ $time";
$output[] = "Field = {$transition->getFieldName()}";
$output[] = "From/To = {$transition->getFromSid()} > {$transition->getToSid()}"
. $spaces . "From/To = {$transition->getFromState()} > {$transition->getToState()}";
// $output[] = "From/To = {$transition->getFromState()} > {$transition->getToState()}";
$output[] = "Comment = {$user_name} says: {$transition->getComment()}";
$output[] = "Scheduled = " . ($transition->isScheduled() ? 'yes' : 'no')
. "; Forced = " . ($transition->isForced() ? 'yes' : 'no')
. "; Executed = " . ($transition->isExecuted() ? 'yes' : 'no');
foreach ($this->getAttachedFieldDefinitions() as $field_name => $field) {
$empty_string = 'value not found' . ($this->isScheduled() ? ' (for scheduled transition?)' : '');
$value = $empty_string;
if ($item = $this->{$field_name}->first()) {
$values = [];
foreach ($this->{$field_name} as $id => $item) {
if ($item && !$item->isEmpty()) {
$main_property = $item?->mainPropertyName();
$values[] = $item->__get($main_property);
}
}
$value = implode(', ', $values);
}
$output[] = "$field_name = $value";
}
// @phpstan-ignore-next-line
// phpcs:ignore Drupal.Functions.DiscouragedFunctions.Discouraged
dpm($output, $t_string); // In Workflow->dpm().
return $this;
}
}
