3 namespace Drupal\content_moderation;
5 use Drupal\content_moderation\Plugin\Field\ModerationStateFieldItemList;
6 use Drupal\Core\Entity\BundleEntityFormBase;
7 use Drupal\Core\Entity\ContentEntityFormInterface;
8 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
9 use Drupal\Core\Entity\ContentEntityTypeInterface;
10 use Drupal\Core\Entity\EntityInterface;
11 use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
12 use Drupal\Core\Entity\EntityTypeInterface;
13 use Drupal\Core\Entity\EntityTypeManagerInterface;
14 use Drupal\Core\Field\BaseFieldDefinition;
15 use Drupal\Core\Form\FormInterface;
16 use Drupal\Core\Form\FormStateInterface;
17 use Drupal\Core\Session\AccountInterface;
18 use Drupal\Core\StringTranslation\StringTranslationTrait;
19 use Drupal\Core\StringTranslation\TranslationInterface;
20 use Drupal\content_moderation\Entity\Handler\BlockContentModerationHandler;
21 use Drupal\content_moderation\Entity\Handler\ModerationHandler;
22 use Drupal\content_moderation\Entity\Handler\NodeModerationHandler;
23 use Drupal\content_moderation\Entity\Routing\EntityModerationRouteProvider;
24 use Symfony\Component\DependencyInjection\ContainerInterface;
27 * Manipulates entity type information.
29 * This class contains primarily bridged hooks for compile-time or
30 * cache-clear-time hooks. Runtime hooks should be placed in EntityOperations.
34 class EntityTypeInfo implements ContainerInjectionInterface {
36 use StringTranslationTrait;
39 * The moderation information service.
41 * @var \Drupal\content_moderation\ModerationInformationInterface
43 protected $moderationInfo;
46 * The entity type manager.
48 * @var \Drupal\Core\Entity\EntityTypeManagerInterface
50 protected $entityTypeManager;
53 * The bundle information service.
55 * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
57 protected $bundleInfo;
62 * @var \Drupal\Core\Session\AccountInterface
64 protected $currentUser;
67 * The state transition validation service.
69 * @var \Drupal\content_moderation\StateTransitionValidationInterface
74 * A keyed array of custom moderation handlers for given entity types.
76 * Any entity not specified will use a common default.
80 protected $moderationHandlers = [
81 'node' => NodeModerationHandler::class,
82 'block_content' => BlockContentModerationHandler::class,
86 * EntityTypeInfo constructor.
88 * @param \Drupal\Core\StringTranslation\TranslationInterface $translation
89 * The translation service. for form alters.
90 * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
91 * The moderation information service.
92 * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
93 * Entity type manager.
94 * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
95 * Bundle information service.
96 * @param \Drupal\Core\Session\AccountInterface $current_user
99 public function __construct(TranslationInterface $translation, ModerationInformationInterface $moderation_information, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $bundle_info, AccountInterface $current_user, StateTransitionValidationInterface $validator) {
100 $this->stringTranslation = $translation;
101 $this->moderationInfo = $moderation_information;
102 $this->entityTypeManager = $entity_type_manager;
103 $this->bundleInfo = $bundle_info;
104 $this->currentUser = $current_user;
105 $this->validator = $validator;
111 public static function create(ContainerInterface $container) {
113 $container->get('string_translation'),
114 $container->get('content_moderation.moderation_information'),
115 $container->get('entity_type.manager'),
116 $container->get('entity_type.bundle.info'),
117 $container->get('current_user'),
118 $container->get('content_moderation.state_transition_validation')
124 * Adds Moderation configuration to appropriate entity types.
126 * @param \Drupal\Core\Entity\EntityTypeInterface[] $entity_types
127 * The master entity type list to alter.
129 * @see hook_entity_type_alter()
131 public function entityTypeAlter(array &$entity_types) {
132 foreach ($entity_types as $entity_type_id => $entity_type) {
133 // The ContentModerationState entity type should never be moderated.
134 if ($entity_type->isRevisionable() && $entity_type_id != 'content_moderation_state') {
135 $entity_types[$entity_type_id] = $this->addModerationToEntityType($entity_type);
141 * Modifies an entity definition to include moderation support.
143 * This primarily just means an extra handler. A Generic one is provided,
144 * but individual entity types can provide their own as appropriate.
146 * @param \Drupal\Core\Entity\ContentEntityTypeInterface $type
147 * The content entity definition to modify.
149 * @return \Drupal\Core\Entity\ContentEntityTypeInterface
150 * The modified content entity definition.
152 protected function addModerationToEntityType(ContentEntityTypeInterface $type) {
153 if (!$type->hasHandlerClass('moderation')) {
154 $handler_class = !empty($this->moderationHandlers[$type->id()]) ? $this->moderationHandlers[$type->id()] : ModerationHandler::class;
155 $type->setHandlerClass('moderation', $handler_class);
158 if (!$type->hasLinkTemplate('latest-version') && $type->hasLinkTemplate('canonical')) {
159 $type->setLinkTemplate('latest-version', $type->getLinkTemplate('canonical') . '/latest');
162 $providers = $type->getRouteProviderClasses() ?: [];
163 if (empty($providers['moderation'])) {
164 $providers['moderation'] = EntityModerationRouteProvider::class;
165 $type->setHandlerClass('route_provider', $providers);
172 * Gets the "extra fields" for a bundle.
175 * A nested array of 'pseudo-field' elements. Each list is nested within the
176 * following keys: entity type, bundle name, context (either 'form' or
177 * 'display'). The keys are the name of the elements as appearing in the
178 * renderable array (either the entity form or the displayed entity). The
179 * value is an associative array:
180 * - label: The human readable name of the element. Make sure you sanitize
181 * this appropriately.
182 * - description: A short description of the element contents.
183 * - weight: The default weight of the element.
184 * - visible: (optional) The default visibility of the element. Defaults to
186 * - edit: (optional) String containing markup (normally a link) used as the
187 * element's 'edit' operation in the administration interface. Only for
189 * - delete: (optional) String containing markup (normally a link) used as
190 * the element's 'delete' operation in the administration interface. Only
191 * for 'form' context.
193 * @see hook_entity_extra_field_info()
195 public function entityExtraFieldInfo() {
197 foreach ($this->getModeratedBundles() as $bundle) {
198 $return[$bundle['entity']][$bundle['bundle']]['display']['content_moderation_control'] = [
199 'label' => $this->t('Moderation control'),
200 'description' => $this->t("Status listing and form for the entity's moderation state."),
210 * Returns an iterable list of entity names and bundle names under moderation.
212 * That is, this method returns a list of bundles that have Content
213 * Moderation enabled on them.
216 * A generator, yielding a 2 element associative array:
217 * - entity: The machine name of an entity type, such as "node" or
219 * - bundle: The machine name of a bundle, such as "page" or "article".
221 protected function getModeratedBundles() {
222 $entity_types = array_filter($this->entityTypeManager->getDefinitions(), [$this->moderationInfo, 'canModerateEntitiesOfEntityType']);
223 foreach ($entity_types as $type_name => $type) {
224 foreach ($this->bundleInfo->getBundleInfo($type_name) as $bundle_id => $bundle) {
225 if ($this->moderationInfo->shouldModerateEntitiesOfBundle($type, $bundle_id)) {
226 yield ['entity' => $type_name, 'bundle' => $bundle_id];
233 * Adds base field info to an entity type.
235 * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
236 * Entity type for adding base fields to.
238 * @return \Drupal\Core\Field\BaseFieldDefinition[]
239 * New fields added by moderation state.
241 * @see hook_entity_base_field_info()
243 public function entityBaseFieldInfo(EntityTypeInterface $entity_type) {
244 if (!$this->moderationInfo->canModerateEntitiesOfEntityType($entity_type)) {
249 $fields['moderation_state'] = BaseFieldDefinition::create('string')
250 ->setLabel(t('Moderation state'))
251 ->setDescription(t('The moderation state of this piece of content.'))
253 ->setClass(ModerationStateFieldItemList::class)
254 ->setDisplayOptions('view', [
256 'region' => 'hidden',
259 ->setDisplayOptions('form', [
260 'type' => 'moderation_state_default',
264 ->addConstraint('ModerationState', [])
265 ->setDisplayConfigurable('form', TRUE)
266 ->setDisplayConfigurable('view', FALSE)
268 ->setTranslatable(TRUE);
274 * Replaces the entity form entity object with a proper revision object.
276 * @param \Drupal\Core\Entity\EntityInterface $entity
277 * The entity being edited.
278 * @param string $operation
279 * The entity form operation.
280 * @param \Drupal\Core\Form\FormStateInterface $form_state
283 * @see hook_entity_prepare_form()
285 public function entityPrepareForm(EntityInterface $entity, $operation, FormStateInterface $form_state) {
286 /** @var \Drupal\Core\Entity\EntityFormInterface $form_object */
287 $form_object = $form_state->getFormObject();
289 if ($this->isModeratedEntityEditForm($form_object) && !$entity->isNew()) {
290 // Generate a proper revision object for the current entity. This allows
291 // to correctly handle translatable entities having pending revisions.
292 /** @var \Drupal\Core\Entity\ContentEntityStorageInterface $storage */
293 $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
294 /** @var \Drupal\Core\Entity\ContentEntityInterface $new_revision */
295 $new_revision = $storage->createRevision($entity, FALSE);
297 // Restore the revision ID as other modules may expect to find it still
298 // populated. This will reset the "new revision" flag, however the entity
299 // object will be marked as a new revision again on submit.
300 // @see \Drupal\Core\Entity\ContentEntityForm::buildEntity()
301 $revision_key = $new_revision->getEntityType()->getKey('revision');
302 $new_revision->set($revision_key, $new_revision->getLoadedRevisionId());
303 $form_object->setEntity($new_revision);
308 * Alters bundle forms to enforce revision handling.
311 * An associative array containing the structure of the form.
312 * @param \Drupal\Core\Form\FormStateInterface $form_state
313 * The current state of the form.
314 * @param string $form_id
317 * @see hook_form_alter()
319 public function formAlter(array &$form, FormStateInterface $form_state, $form_id) {
320 $form_object = $form_state->getFormObject();
321 if ($form_object instanceof BundleEntityFormBase) {
322 $config_entity_type = $form_object->getEntity()->getEntityType();
323 $bundle_of = $config_entity_type->getBundleOf();
325 && ($bundle_of_entity_type = $this->entityTypeManager->getDefinition($bundle_of))
326 && $this->moderationInfo->canModerateEntitiesOfEntityType($bundle_of_entity_type)) {
327 $this->entityTypeManager->getHandler($config_entity_type->getBundleOf(), 'moderation')->enforceRevisionsBundleFormAlter($form, $form_state, $form_id);
330 elseif ($this->isModeratedEntityEditForm($form_object)) {
331 /** @var \Drupal\Core\Entity\ContentEntityFormInterface $form_object */
332 /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
333 $entity = $form_object->getEntity();
334 if ($this->moderationInfo->isModeratedEntity($entity)) {
335 $this->entityTypeManager
336 ->getHandler($entity->getEntityTypeId(), 'moderation')
337 ->enforceRevisionsEntityFormAlter($form, $form_state, $form_id);
339 // Submit handler to redirect to the latest version, if available.
340 $form['actions']['submit']['#submit'][] = [EntityTypeInfo::class, 'bundleFormRedirect'];
342 // Move the 'moderation_state' field widget to the footer region, if
344 if (isset($form['footer'])) {
345 $form['moderation_state']['#group'] = 'footer';
348 // If the publishing status exists in the meta region, replace it with
349 // the current state instead.
350 if (isset($form['meta']['published'])) {
351 $form['meta']['published']['#markup'] = $this->moderationInfo->getWorkflowForEntity($entity)->getTypePlugin()->getState($entity->moderation_state->value)->label();
358 * Checks whether the specified form allows to edit a moderated entity.
360 * @param \Drupal\Core\Form\FormInterface $form_object
364 * TRUE if the form should get form moderation, FALSE otherwise.
366 protected function isModeratedEntityEditForm(FormInterface $form_object) {
367 return $form_object instanceof ContentEntityFormInterface &&
368 in_array($form_object->getOperation(), ['edit', 'default'], TRUE) &&
369 $this->moderationInfo->isModeratedEntity($form_object->getEntity());
373 * Redirect content entity edit forms on save, if there is a pending revision.
375 * When saving their changes, editors should see those changes displayed on
379 * An associative array containing the structure of the form.
380 * @param \Drupal\Core\Form\FormStateInterface $form_state
381 * The current state of the form.
383 public static function bundleFormRedirect(array &$form, FormStateInterface $form_state) {
384 /* @var \Drupal\Core\Entity\ContentEntityInterface $entity */
385 $entity = $form_state->getFormObject()->getEntity();
387 $moderation_info = \Drupal::getContainer()->get('content_moderation.moderation_information');
388 if ($moderation_info->hasPendingRevision($entity) && $entity->hasLinkTemplate('latest-version')) {
389 $entity_type_id = $entity->getEntityTypeId();
390 $form_state->setRedirect("entity.$entity_type_id.latest_version", [$entity_type_id => $entity->id()]);