sites_group_overrides-1.x-dev/src/FormDecorator/EntityOverrideAbilityClues.php
src/FormDecorator/EntityOverrideAbilityClues.php
<?php
declare(strict_types=1);
namespace Drupal\sites_group_overrides\FormDecorator;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\DependencyInjection\DependencySerializationTrait;
use Drupal\Core\Entity\ContentEntityFormInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Render\Element;
use Drupal\Core\Render\Markup;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\form_decorator\ContentEntityFormDecoratorBase;
use Drupal\group\Entity\GroupInterface;
use Drupal\group\Entity\GroupRelationshipInterface;
use Drupal\sites\ContextProvider\CurrentSiteContextInterface;
use Drupal\sites_group_overrides\SitesGroupOverridesServiceInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
/**
* Implementation for all ContentEntityFormInterface forms.
*
* @FormDecorator()
*/
final class EntityOverrideAbilityClues extends ContentEntityFormDecoratorBase implements ContainerFactoryPluginInterface {
use StringTranslationTrait;
use DependencySerializationTrait;
/**
* Constructs a EntityOverrideAbilityClues form decorator.
*
* @param array $configuration
* A configuration array containing information about the plugin instance.
* @param string $plugin_id
* The plugin ID for the plugin instance.
* @param array $plugin_definition
* The plugin definition.
* @param \Drupal\sites_group_overrides\SitesGroupOverridesServiceInterface $sitesGroupOverridesService
* The sites group override service.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $eventDispatcher
* The event dispatcher.
* @param \Drupal\Core\Config\ConfigFactoryInterface $configFactory
* A config factory for retrieving required config objects.
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.
* @param \Drupal\sites\ContextProvider\CurrentSiteContextInterface $currentSite
* The current site context.
*/
public function __construct(
array $configuration,
$plugin_id,
$plugin_definition,
protected SitesGroupOverridesServiceInterface $sitesGroupOverridesService,
protected EventDispatcherInterface $eventDispatcher,
protected ConfigFactoryInterface $configFactory,
protected EntityTypeManagerInterface $entityTypeManager,
protected CurrentSiteContextInterface $currentSite,
) {
parent::__construct($configuration, $plugin_id, $plugin_definition);
}
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, $configuration, $plugin_id, $plugin_definition) {
return new self(
$configuration,
$plugin_id,
$plugin_definition,
$container->get('sites_group_overrides.service'),
$container->get('event_dispatcher'),
$container->get('config.factory'),
$container->get('entity_type.manager'),
$container->get('sites.current_site'),
);
}
/**
* {@inheritdoc}
*/
public function applies(): bool {
if (!$this->inner instanceof ContentEntityFormInterface) {
return FALSE;
}
// Do not modify group form - groups are never overridable.
if ($this->getEntity() instanceof GroupInterface) {
return FALSE;
}
if ($this->getEntity() instanceof GroupRelationshipInterface) {
return FALSE;
}
if (!$this->currentSite->getSiteFromContext()) {
return FALSE;
}
// For whatever reason some entites use the 'default' form operation (Term)
// And other have the 'edit' form opertaion (Nodes).
if (!in_array($this->inner->getOperation(), ['edit', 'default'])) {
return FALSE;
}
// Don't do this on create forms. They use 'default' as their operation.
if ($this->inner->getEntity()->isNew()) {
return FALSE;
}
// Decide by setting.
$apply_to_non_overideable_entities = $this->configFactory->get('sites_group_overrides.settings')->get('apply_to_non_overideable_entities');
$entity = $this->getEntity();
\Drupal::moduleHandler()->alter('sites_group_overrides_non_overideable_entities', $apply_to_non_overideable_entities, $entity);
if ($apply_to_non_overideable_entities) {
return TRUE;
}
$relationship = $this->sitesGroupOverridesService->getRelationship($this->getEntity());
if (!$relationship instanceof GroupRelationshipInterface) {
return FALSE;
}
if (empty($this->sitesGroupOverridesService->getSynchronizableFields($relationship, $this->getEntity()))) {
return FALSE;
}
return TRUE;
}
/**
* {@inheritdoc}
*/
public function buildForm(array $form, FormStateInterface $form_state, ...$args) {
$form = parent::buildForm($form, $form_state, ...$args);
$form['#process'][] = [$this, 'entityFormSharedElements'];
return $form;
}
/**
* Process callback: determines which elements get clue in the form.
*
* @see \Drupal\content_translation\ContentTranslationHandler::entityFormAlter()
*/
public function entityFormSharedElements($element, FormStateInterface $form_state, $form) {
static $ignored_types;
// @todo Find a more reliable way to determine if a form element concerns a
// multilingual value.
if (!isset($ignored_types)) {
$ignored_types = array_flip(['actions', 'value', 'hidden', 'vertical_tabs', 'token', 'details', 'link']);
}
/** @var \Drupal\Core\Entity\ContentEntityForm $form_object */
$form_object = $form_state->getFormObject();
/** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
$entity = $form_object->getEntity();
$hide_untranslatable_fields = $entity->isDefaultTranslationAffectedOnly() && !$entity->isDefaultTranslation();
$translation_form = $form_state->get(['content_translation', 'translation_form']);
$display_warning = FALSE;
// We use field definitions to identify untranslatable field widgets to be
// hidden. Fields that are not involved in translation changes checks should
// not be affected by this logic (the "revision_log" field, for instance).
// $field_definitions = array_diff_key($entity->getFieldDefinitions(), array_flip($this->getFieldsToSkipFromTranslationChangesCheck($entity)));
$fields = $field_definitions = [];
if ($relationship = $this->sitesGroupOverridesService->getRelationship($this->getEntity())) {
$field_definitions = $relationship->getFieldDefinitions();
$fields = $this->sitesGroupOverridesService->getSynchronizableFields($relationship, $this->getEntity());
// Reload relationship to have empty values for non-overriden fields.
/** @var \Drupal\group\Entity\GroupRelationshipInterface $relationship */
$relationship = $this->entityTypeManager->getStorage('group_relationship')->loadUnchanged($relationship->id());
try {
$relationship = $relationship->getTranslation($this->getEntity()->language()->getId());
}
catch (\InvalidArgumentException $e) {
return $element;
}
}
foreach (Element::children($element) as $key) {
$is_overridden = $is_overridable = FALSE;
if (!isset($element[$key]['#type'])) {
$this->entityFormSharedElements($element[$key], $form_state, $form);
}
else {
// Ignore non-widget form elements.
if (isset($ignored_types[$element[$key]['#type']])) {
continue;
}
if (!empty($element[$key]['widget'][0]['#supports_sites'])) {
continue;
}
// Elements are considered to be non multilingual by default.
// if (array_key_exists($key, $fields)) {
// $this->addOverrideStatus($key, $element[$key]);
// }
// If we are displaying a multilingual entity form we need to provide
// translatability clues, otherwise the non-multilingual form elements
// should be hidden.
if (!$translation_form) {
if (array_key_exists($key, $fields)) {
$is_overridable = TRUE;
if ($relationship instanceof GroupRelationshipInterface) {
$target_field_name = $this->sitesGroupOverridesService->getTargetFieldName($relationship, $key);
$is_overridden = $relationship->hasField($target_field_name) && !$relationship->get($target_field_name)->isEmpty();
}
}
$this->addOverrideAbilityClue($element[$key], $is_overridable, $is_overridden, $key, $key);
// Hide widgets for untranslatable fields.
if ($hide_untranslatable_fields && isset($field_definitions[$key])) {
$element[$key]['#access'] = FALSE;
$display_warning = TRUE;
}
}
else {
$element[$key]['#access'] = FALSE;
}
}
}
if ($display_warning && !$form_state->isSubmitted() && !$form_state->isRebuilding()) {
$url = $entity->getUntranslated()->toUrl('edit-form')->toString();
$this->messenger()->addWarning($this->t('Fields that apply to all languages are hidden to avoid conflicting changes. <a href=":url">Edit them on the original language form</a>.', [':url' => $url]));
}
return $element;
}
/**
* Adds a clue about the form element override-ability.
*
* If the given element does not have a #title attribute, the function is
* recursively applied to child elements.
*
* @param array $element
* A form element array.
* @param bool $is_overridable
* TRUE, if the field is overideable.
* @param bool $is_overridden
* TRUE if the field is overridden.
* @param mixed $key
* The key of the element.
* @param mixed $field_name
* The field name.
*
* @see \Drupal\content_translation\ContentTranslationHandler::addTranslatabilityClue
*/
protected function addOverrideAbilityClue(array &$element, bool $is_overridable, bool $is_overridden, $key, $field_name) {
static $suffix, $fapi_title_elements;
$apply = FALSE;
// Elements which can have a #title attribute according to FAPI Reference.
$text = $this->t('all sites');
if ($is_overridable) {
$text = $this->t('Original value');
if ($is_overridden) {
$text = $this->t('Overridden');
}
}
$suffix = ' <span class="override-entity-all-sites sites-group-overrides-clue--' . $field_name . '">(' . $text . ')</span>';
if (!isset($fapi_title_elements)) {
$fapi_title_elements = array_flip([
'checkbox',
'checkboxes',
'date',
'details',
'fieldset',
'file',
'item',
'password',
'password_confirm',
'radio',
'radios',
'select',
'text_format',
'textarea',
'textfield',
'weight',
]);
}
// If ($is_overridable) {
// Use an after build to set classes - some elemets e.g. textareas are not here yet.
// @todo avoid using ->get()
// $main_property = $this->getEntity()->get($field_name)->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
// $element['#main_property'] = $main_property;
// $element['#key'] = $key;
// $element['#field_name'] = $field_name;
// $element['#after_build'][] = [static::class, 'afterBuild'];
// }
// Update #title attribute for all elements that are allowed to have a
// #title attribute according to the Form API Reference. The reason for this
// check is because some elements have a #title attribute even though it is
// not rendered; for instance, field containers.
if (isset($element['#type']) && isset($fapi_title_elements[$element['#type']]) && isset($element['#title'])) {
$apply = TRUE;
}
elseif (isset($element['#title']) && !empty($element["#cardinality_multiple"])) {
$apply = TRUE;
}
// If the current element does not have a (valid) title, try child elements.
elseif ($children = Element::children($element)) {
foreach ($children as $delta) {
$this->addOverrideAbilityClue($element[$delta], $is_overridable, $is_overridden, $delta, $field_name);
}
}
// If there are no children, fall back to the current #title attribute if it
// exists.
elseif (isset($element['#title'])) {
$apply = TRUE;
}
if ($apply) {
// Using ['#markup' => ...] ends in a fatal some times -> use Markup::create.
$element['#title'] = Markup::create($element['#title'] . $suffix);
if (!$is_overridable) {
$config = $this->configFactory->get('sites_group_overrides.settings');
if ($config->get('disable_source_field_on_override')) {
$element['#disabled'] = TRUE;
}
if ($config->get('hide_source_field_on_override')) {
$element['#access'] = FALSE;
}
}
}
}
/**
* Adds the drupal settings the JS.
*
* @param string $field_name
* The field name to add the settings for.
* @param array $element
* The element of the field.
*/
protected function addOverrideStatus(string $field_name, array &$element) {
// @todo Avoid using ->get(..)
$original_entity = $this->entityTypeManager->getStorage($this->getEntity()->getEntityTypeId())->loadUnchanged($this->getEntity()->id());
$unsupported = ['metatag', 'entity_reference_revisions', 'link'];
if (in_array($original_entity->get($field_name)->getFieldDefinition()->getType(), $unsupported)) {
return;
}
$main_property = $original_entity->get($field_name)->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
$element['#attached']['drupalSettings']['sitesGroupOverrides'][$field_name] = [
'original_value' => $original_entity->get($field_name)->{$main_property} ?? '',
'label_selector' => '.sites-group-overrides-clue--' . $field_name,
'value_selector' => '.sites-group-overrides-value--' . $field_name,
'class' => 'sites-group-overrides-clue--' . $field_name,
];
$element['#attached']['library'][] = 'sites_group_overrides/overide_status';
}
/**
* Add classes to find label and value for all overideable fields.
*/
public static function afterBuild(array $element, FormStateInterface $form_state, $main_property = NULL, $key = NULL, $field_name = NULL) {
if (is_null($key)) {
$key = $element['#key'];
}
if (is_null($main_property)) {
$main_property = $element['#main_property'];
}
if (is_null($field_name)) {
$field_name = $element['#field_name'];
}
if ($key == $main_property) {
$element['#attributes']['class'][] = 'sites-group-overrides-value--' . $field_name;
}
elseif ($children = Element::children($element)) {
foreach ($children as $delta) {
$element[$delta] = self::afterBuild($element[$delta], $form_state, $main_property, $delta, $field_name);
}
}
return $element;
}
}
