flow-1.0.0-beta8/src/Plugin/flow/Qualifier/Congruent.php
src/Plugin/flow/Qualifier/Congruent.php
<?php
namespace Drupal\flow\Plugin\flow\Qualifier;
use Drupal\Component\Utility\NestedArray;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\Entity\EntityFormDisplay;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\RevisionableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\PluginFormInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\flow\Helpers\EntityContentConfigurationTrait;
use Drupal\flow\Helpers\EntityFromStackTrait;
use Drupal\flow\Helpers\EntitySerializationTrait;
use Drupal\flow\Helpers\EntityTypeManagerTrait;
use Drupal\flow\Helpers\FormBuilderTrait;
use Drupal\flow\Helpers\ModuleHandlerTrait;
use Drupal\flow\Helpers\SingleTaskOperationTrait;
use Drupal\flow\Helpers\TokenTrait;
use Drupal\flow\Helpers\UserAccount;
use Drupal\flow\Plugin\FlowQualifierBase;
use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Qualifies an entity when having congruent field values.
*
* @FlowQualifier(
* id = "congruent",
* label = @Translation("Congruent content"),
* deriver = "Drupal\flow\Plugin\flow\Derivative\Qualifier\CongruentDeriver"
* )
*/
class Congruent extends FlowQualifierBase implements PluginFormInterface {
use EntityContentConfigurationTrait {
buildConfigurationForm as buildContentConfigurationForm;
submitConfigurationForm as submitContentConfigurationForm;
}
use EntityFromStackTrait;
use EntitySerializationTrait;
use EntityTypeManagerTrait;
use FormBuilderTrait;
use ModuleHandlerTrait;
use SingleTaskOperationTrait;
use StringTranslationTrait;
use TokenTrait;
/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
/** @var \Drupal\flow\Plugin\flow\Qualifier\Congruent $instance */
$instance = parent::create($container, $configuration, $plugin_id, $plugin_definition);
$instance->setStringTranslation($container->get('string_translation'));
$instance->setModuleHandler($container->get(self::$moduleHandlerServiceName));
$instance->setFormBuilder($container->get(self::$formBuilderServiceName));
$instance->setEntityTypeManager($container->get(self::$entityTypeManagerServiceName));
$instance->setSerializer($container->get(self::$serializerServiceName));
$instance->setToken($container->get(self::$tokenServiceName));
if (empty($instance->settings['values'])) {
$default_config = $instance->defaultConfiguration();
$instance->settings += $default_config['settings'];
}
$instance->initEntityFromStack();
$instance->initConfiguredContentEntity();
return $instance;
}
/**
* {@inheritdoc}
*/
public function qualifies(ContentEntityInterface $entity): bool {
$source = $this->initConfiguredContentEntity($entity);
$target = $entity;
$admission_method = $this->settings['method']['admission'] ?? 'new+changed';
switch ($admission_method) {
case 'everytime':
return $this->isCongruent($source, $target);
case 'new+changed':
if (!$this->isCongruent($source, $target)) {
return FALSE;
}
if ($target->isNew()) {
return TRUE;
}
if (isset($target->original)) {
return !$this->isCongruent($source, $target->original);
}
$storage = $this->getEntityTypeManager()->getStorage($target->getEntityTypeId());
$current = $storage->load($target->id());
if ($current && ($current !== $target) && !$this->isCongruent($source, $current)) {
return TRUE;
}
$original = $storage->loadUnchanged($target->id());
if ($original && ($original !== $target) && !$this->isCongruent($source, $original)) {
return TRUE;
}
if ($target instanceof RevisionableInterface) {
$rids = array_keys($storage
->getQuery()
->accessCheck(FALSE)
->condition($target->getEntityType()->getKey('id'), $target->id())
->condition($target->getEntityType()->getKey('revision'), $target->getLoadedRevisionId(), '<')
->condition($target->getEntityType()->getKey('langcode'), $target->language()->getId())
->sort($target->getEntityType()->getKey('revision'), 'DESC')
->range(0, 1)
->allRevisions()
->execute());
sort($rids);
$previous_rid = end($rids);
if (($previous_rid !== FALSE) && ($previous_revision = $storage->loadRevision($previous_rid))) {
return !$this->isCongruent($source, $previous_revision);
}
}
return TRUE;
default:
return FALSE;
}
}
/**
* Checks whether source and target are congruent.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $source
* The source entity, coming from this plugin's configuration.
* @param \Drupal\Core\Entity\ContentEntityInterface $target
* The target entity to evaluate against.
*
* @return bool
* Returns TRUE if both entities are congruent, FALSE otherwise.
*/
protected function isCongruent(ContentEntityInterface $source, ContentEntityInterface $target): bool {
$multi_method = $this->settings['method']['multi'] ?? 'or';
$field_names = $this->settings['fields'] ?? [];
$is_congruent = FALSE;
foreach ($field_names as $field_name) {
$is_congruent = FALSE;
if (!$target->hasField($field_name) || !$source->hasField($field_name)) {
break;
}
$source_item_list = $source->get($field_name);
$source_item_list->filterEmptyItems();
$target_item_list = $target->get($field_name);
$target_item_list->filterEmptyItems();
if ($source_item_list->isEmpty() && !$target_item_list->isEmpty()) {
break;
}
if (!$source_item_list->isEmpty() && $target_item_list->isEmpty()) {
break;
}
if ($source_item_list->isEmpty() && $target_item_list->isEmpty()) {
$is_congruent = TRUE;
continue;
}
$source_values = $source_item_list->getValue();
$target_values = $target_item_list->getValue();
// Determine if we have congruent values.
// @todo Find a better way to determine this.
$comparison_source_values = $comparison_target_values = [];
/** @var \Drupal\Core\Field\FieldItemInterface $source_item */
foreach ($source_item_list as $i => $source_item) {
$property_name = $source_item->mainPropertyName();
$source_value = isset($property_name) && !is_null($source_item->$property_name) ? $source_item->$property_name : ($source_values[$i] ?? $source_item->getValue());
if (is_string($source_value)) {
$source_value = nl2br(trim($source_value));
}
elseif (is_array($source_value) && isset($source_value['entity'])) {
$source_value = $source_value['entity'];
}
$comparison_source_values[$i] = $source_value;
}
/** @var \Drupal\Core\Field\FieldItemInterface $target_item */
foreach ($target_item_list as $i => $target_item) {
$target_value = isset($property_name) && !is_null($target_item->$property_name) ? $target_item->$property_name : ($target_values[$i] ?? $target_item->getValue());
if (is_string($target_value)) {
$target_value = nl2br(trim($target_value));
}
elseif (is_array($target_value) && isset($target_value['entity'])) {
$target_value = $target_value['entity'];
}
$comparison_target_values[$i] = $target_value;
}
// When comparing new entities, use a normalized array representation and
// compare for these values.
$needs_array_conversion = FALSE;
foreach ($comparison_source_values as $i => $source_value) {
$source_item = $source_item_list->get($i);
$entity = $source_value instanceof EntityInterface ? $source_value : ($source_item && isset($source_item->entity) && ($source_item->entity instanceof EntityInterface) ? $source_item->entity : NULL);
if ($entity && $entity->isNew()) {
$needs_array_conversion = TRUE;
$comparison_source_values[$i] = $this->toConfigArray($entity);
array_walk_recursive($comparison_source_values[$i], function (&$v) {
if (is_string($v)) {
$v = nl2br(trim($v));
}
});
if (!isset($configured_keys)) {
$configured_keys = array_flip(array_keys($comparison_source_values[$i]));
}
}
}
if ($needs_array_conversion) {
foreach ($comparison_target_values as $i => $current_value) {
$current_item = $target_item_list->get($i);
$entity = $current_value instanceof EntityInterface ? $current_value : ($current_item && isset($current_item->entity) && ($current_item->entity instanceof EntityInterface) ? $current_item->entity : NULL);
if ($entity) {
$comparison_target_values[$i] = array_intersect_key($this->toConfigArray($entity), $configured_keys);
array_walk_recursive($comparison_target_values[$i], function (&$v) {
if (is_string($v)) {
$v = nl2br(trim($v));
}
});
}
}
}
foreach ($comparison_source_values as $source_value) {
$is_congruent = FALSE;
foreach ($comparison_target_values as $target_value) {
if (($source_value === $target_value) || (is_scalar($source_value) && is_scalar($target_value) && (((string) $source_value === (string) $target_value) || ($source_value === FALSE && $target_value === '0')))) {
$is_congruent = TRUE;
if ($multi_method === 'or') {
break 2;
}
}
}
if (!$is_congruent && ($multi_method === 'and')) {
break 2;
}
}
if (!$is_congruent) {
break;
}
}
return $is_congruent;
}
/**
* {@inheritdoc}
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form += $this->buildContentConfigurationForm($form, $form_state);
if (isset($form['values'])) {
$form['values']['#process'][] = [$this, 'filterFormFields'];
}
$weight = -100000;
$entity_type = $this->configuredContentEntity->getEntityType();
$form_display = EntityFormDisplay::collectRenderDisplay($this->configuredContentEntity, $this->entityFormDisplay, TRUE);
$available_fields = array_keys($form_display->getComponents());
$available_fields = array_combine($available_fields, $available_fields);
$selected_fields_to_merge = isset($this->settings['fields']) ? array_combine($this->settings['fields'], $this->settings['fields']) : [];
$langcode_key = $entity_type->hasKey('langcode') ? $entity_type->getKey('langcode') : 'langcode';
unset($available_fields[$langcode_key], $available_fields['default_langcode']);
$field_options = [];
foreach ($available_fields as $field_name) {
if (!$this->configuredContentEntity->hasField($field_name)) {
continue;
}
$field_options[$field_name] = $this->configuredContentEntity->get($field_name)->getFieldDefinition()->getLabel();
}
if ($entity_type->id() === 'user') {
$field_options += UserAccount::getAvailableFields();
}
$weight += 10;
$form['fields'] = [
'#type' => 'checkboxes',
'#title' => $this->t('Congruency fields'),
'#options' => $field_options,
'#default_value' => $selected_fields_to_merge,
'#weight' => $weight++,
'#ajax' => [
'callback' => [static::class, 'filterFormFieldsAjax'],
'wrapper' => $form['values']['#wrapper_id'],
'method' => 'html',
]
];
$weight += 10;
$form['method'] = [
'#weight' => $weight++,
];
$admission_options = [
'everytime' => $this->t('Everytime current values match up'),
'new+changed' => $this->t('Only when new or changed values match up'),
];
$form['method']['admission'] = [
'#type' => 'select',
'#title' => $this->t('Congruency admission'),
'#description' => $this->t('The admission defines at which circumstances a subject item may be qualified.'),
'#required' => TRUE,
'#options' => $admission_options,
'#default_value' => $this->settings['method']['admission'] ?? 'everytime',
'#weight' => 10,
];
$multi_options = [
'or' => $this->t('At least one value must match up'),
'and' => $this->t('All values must match up'),
];
$form['method']['multi'] = [
'#type' => 'select',
'#title' => $this->t('Congruency on multi-value fields'),
'#required' => TRUE,
'#options' => $multi_options,
'#default_value' => $this->settings['method']['multi'] ?? 'or',
'#weight' => 20,
];
return $form;
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->submitContentConfigurationForm($form, $form_state);
$this->settings['method'] = $form_state->getValue('method');
$this->settings['fields'] = array_keys(array_filter($form_state->getValue('fields'), function ($value) {
return !empty($value);
}));
// Filter field values that are not selected in the form.
$entity_type = $this->configuredContentEntity->getEntityType();
$entity_keys = $entity_type->getKeys();
foreach (array_keys($this->settings['values']) as $k_1) {
if (!in_array($k_1, $entity_keys) && !in_array($k_1, $this->settings['fields'])) {
unset($this->settings['values'][$k_1]);
}
}
}
/**
* Process callback for only displaying selected fields in the form.
*
* @param array &$form
* The form element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
* @param array &$complete_form
* The complete form.
*
* @return array
* The form element, enriched by the entity form.
*/
public function filterFormFields(array &$form, FormStateInterface $form_state, array &$complete_form): array {
$entity_type = $this->configuredContentEntity->getEntityType();
$form_display = EntityFormDisplay::collectRenderDisplay($this->configuredContentEntity, $this->entityFormDisplay, TRUE);
$available_fields = array_keys($form_display->getComponents());
if ($entity_type->id() === 'user') {
$available_fields = array_merge($available_fields, array_keys(UserAccount::getAvailableFields()));
}
$available_fields = array_combine($available_fields, $available_fields);
$selected_fields_to_merge = isset($this->settings['fields']) ? array_combine($this->settings['fields'], $this->settings['fields']) : [];
$langcode_key = $entity_type->hasKey('langcode') ? $entity_type->getKey('langcode') : 'langcode';
unset($available_fields[$langcode_key], $available_fields['default_langcode']);
foreach ($available_fields as $field_name) {
if (!$this->configuredContentEntity->hasField($field_name)) {
continue;
}
if (isset($form[$field_name])) {
$form[$field_name]['#access'] = isset($selected_fields_to_merge[$field_name]);
}
}
return $form;
}
/**
* Ajax callback for only displaying selected fields in the form.
*
* @param array $form
* The form array.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*
* @return array
* The filtered field values element.
*/
public static function filterFormFieldsAjax(array $form, FormStateInterface $form_state) {
$checkbox = $form_state->getTriggeringElement();
$element = &NestedArray::getValue($form, array_slice($checkbox['#array_parents'], 0, -2));
$element = $element['values'];
unset($element['#prefix'], $element['#suffix']);
$user_input = $form_state->getUserInput();
$field_name = end($checkbox['#array_parents']);
$is_selected = (bool) NestedArray::getValue($user_input, $checkbox['#array_parents']);
$element[$field_name]['#access'] = $is_selected;
return $element;
}
/**
* {@inheritdoc}
*/
public function calculateDependencies() {
$dependencies = parent::calculateDependencies();
if (($entity = $this->getConfiguredContentEntity()) && !empty($this->settings['fields'])) {
if ($bundle_entity_type_id = $entity->getEntityType()->getBundleEntityType()) {
if ($bundle_type = $this->getEntityTypeManager()->getStorage($bundle_entity_type_id)->load($entity->bundle())) {
$dependencies[$bundle_type->getConfigDependencyKey()][] = $bundle_type->getConfigDependencyName();
}
}
foreach ($this->settings['fields'] as $field_name) {
if (!$entity->hasField($field_name)) {
continue;
}
if ($field_config = $this->getEntityTypeManager()->getStorage('field_config')->load($entity->getEntityTypeId() . '.' . $entity->bundle() . '.' . $field_name)) {
$dependencies[$field_config->getConfigDependencyKey()][] = $field_config->getConfigDependencyName();
}
if ($field_storage_config = $this->getEntityTypeManager()->getStorage('field_storage_config')->load($entity->getEntityTypeId() . '.' . $field_name)) {
$dependencies[$field_storage_config->getConfigDependencyKey()][] = $field_storage_config->getConfigDependencyName();
}
}
}
return $dependencies;
}
}
