- Issue created by @jurgenhaas
- πΊπΈUnited States freelock Seattle
Hi,
Just saw this issue referenced on an old Slack thread -- I did end up creating a custom action plugin for our clients, so this can be the start of a more generic one.
With this implementation, you can get either a list of fields that differ, or a list of the different values with the field names as keys. You can pass in two entirely different entities, or an entity and its :original.
We are only using this to get a list of field names, I have not tested the value option.
namespace Drupal\my_module\Plugin\Action; use Drupal\Component\Utility\DiffArray; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Session\AccountInterface; use Drupal\eca\Plugin\Action\ConfigurableActionBase; /** * Provides a Load Diffs action. * * @Action( * id = "my_module_load_diffs", * label = @Translation("Load Diffs"), * description = @Translation("Compare 2 entities and return a list of fields that differ"), * type = "entity" * ) * */ class LoadDiffs extends ConfigurableActionBase { /** * {@inheritDoc} */ public function defaultConfiguration(): array { return [ 'token_name' => '', 'compare' => null, 'return_values' => FALSE, 'exclude_fields' => [], 'include_fields' => [], ] + parent::defaultConfiguration(); } /** * {@inheritdoc} */ public function buildConfigurationForm(array $form, FormStateInterface $form_state): array { $form['token_name'] = [ '#type' => 'textfield', '#title' => $this->t('Name of token'), '#default_value' => $this->configuration['token_name'], '#description' => $this->t('Provide the name of a token that holds the new list.'), '#weight' => -60, ]; $form['compare'] = [ '#type' => 'textfield', '#title' => $this->t('Compare'), '#description' => $this->t('Provide the name of a token that holds the original entity.'), '#default_value' => $this->configuration['compare'], '#weight' => 30, ]; $form['return_values'] = [ '#type' => 'checkbox', '#title' => $this->t('Return values'), '#default_value' => $this->configuration['return_values'], '#description' => $this->t('If checked, the list will return values. If unchecked, it will only return the different machine names of changed fields.'), ]; $form['exclude_fields'] = [ '#type' => 'textarea', '#title' => 'Exclude fields', '#description' => $this->t('List field machine names to remove from difference/ignore'), '#default_value' => implode("\n", $this->configuration['exclude_fields']), '#weight' => 40, ]; $form['include_fields'] = [ '#type' => 'textarea', '#title' => 'Include fields', '#description' => $this->t('List field machine names to include in difference -- all others will be ignored.'), '#default_value' => implode("\n", $this->configuration['include_fields']), '#weight' => 40, ]; return $form; } /** * {@inheritdoc} */ public function submitConfigurationForm(array &$form, FormStateInterface $form_state): void { $this->configuration['token_name'] = $form_state->getValue('token_name'); $this->configuration['compare'] = $form_state->getValue('compare'); $this->configuration['return_values'] = $form_state->getValue('return_values'); $this->configuration['exclude_fields'] = explode("\n", $form_state->getValue('exclude_fields')); $this->configuration['include_fields'] = explode("\n", $form_state->getValue('include_fields')); parent::submitConfigurationForm($form, $form_state); } /** * {@inheritdoc} */ public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { /** @var ContentEntityInterface $object */ $access = $object->access('view', $account, TRUE); return $return_as_object ? $access : $access->isAllowed(); } /** * {@inheritdoc} */ public function execute($entity = NULL) { $compare = $this->tokenServices->getTokenData($this->configuration['compare']); $diff = DiffArray::diffAssocRecursive($entity->toArray(), $compare->toArray()); $exclude_fields = $this->configuration['exclude_fields']; if (count($exclude_fields)) { foreach ($exclude_fields as $field) { unset($diff[$field]); } } $include_fields = $this->configuration['include_fields']; if (count($include_fields)) { $included = []; foreach ($include_fields as $field) { if (isset ($diff[$field])) { $included[$field] = $diff[$field]; } } $diff = $included; } if (!$this->configuration['return_values']) { $diff = array_keys($diff); } $this->tokenServices->addTokenData($this->configuration['token_name'], $diff); } }
- π©πͺGermany jurgenhaas Gottmadingen
This is nice, thanks a lot @freelock, will turn this into an MR so that we can build on top of this with further details and some tests. Amazing input!!
- last update
over 1 year ago 287 pass - @jurgenhaas opened merge request.
- Status changed to Needs work
over 1 year ago 11:57am 2 June 2023 - π©πͺGermany jurgenhaas Gottmadingen
Moved the code from @freelock into an issue fork and started an MR. Also updated the OP to highlight the remaining tasks.
- π©πͺGermany jurgenhaas Gottmadingen
Please see the discussion at π Condition: field value changed - not working for booleans Fixed where we realized, that comparing field values may not be as straight forward as we thought. To compare entities and their field values, we may even re-use code from that condition plugin, which is discussed there.
- πΊπΈUnited States freelock Seattle
Well, our custom plugin does seem to be working fine for us --
We're using it like this:
- Update Content Entity event
- Entity: Load - token: originalentity, Current scope, latest revision no, unchanged values yes, entity: entity
- Load Diffs - token: diffs, return values no, entity: entity, compare: originalentity, exclude fields: changed, a couple others
- Compare number of list items - token: diffs, operator: greater than, second value: 0
- Tamper: implode - data: [diffs], glue: %n, result_token: diff_text
We're then dropping that in an email notification. It provides a list of fields that have actual changes between the unsaved and saved version of the entity.
We're using it for both user entities and nodes -- looks like one of our node ones is using a list of include fields instead of exclude.
I have not tried to use the values (the return values option), just the keys.
- πΊπΈUnited States freelock Seattle
Maybe Drupal\Component\Utility\DiffArray::diffAssocRecursive() is sorting out these issues internally? Or maybe the $entity->toArray() calls are putting them in the same format?
- π©πͺGermany jurgenhaas Gottmadingen
Do you have boolean fields or entity references in your entities that you compare?
- Assigned to jurgenhaas
- π©πͺGermany jurgenhaas Gottmadingen
OK, then Drupal core loads entities differently, otherwise the problem in the other issue won't be possible. The diff functions being used here won't be able to resolve the different data structure especially for entity references as shown in this comment π Condition: field value changed - not working for booleans Fixed .
- last update
over 1 year ago 289 pass - Issue was unassigned.
- Status changed to Needs review
over 1 year ago 12:39pm 16 June 2023 - π©πͺGermany jurgenhaas Gottmadingen
I have now implemented the action and the condition plugin. Both are ready for review. Afterwards, we then also need tests - anyone up for it?
- Assigned to danielspeicher
- Status changed to Needs work
over 1 year ago 12:43pm 16 June 2023 - last update
over 1 year ago 294 pass - last update
over 1 year ago 295 pass - π©πͺGermany jurgenhaas Gottmadingen
Tagginng this for Dev Days next week.
- last update
over 1 year ago 296 pass - last update
over 1 year ago 296 pass - Status changed to Needs review
over 1 year ago 8:07am 13 July 2023 - π©πͺGermany danielspeicher Steisslingen
I have added tests for both plugins.
- Issue was unassigned.
- Status changed to RTBC
over 1 year ago 8:17am 13 July 2023 - last update
over 1 year ago 296 pass -
jurgenhaas β
committed 34ba58e0 on 1.2.x
Issue #3352901 by danielspeicher, jurgenhaas, freelock: New action/...
-
jurgenhaas β
committed 34ba58e0 on 1.2.x
- Status changed to Fixed
over 1 year ago 8:19am 13 July 2023 Automatically closed - issue fixed for 2 weeks with no activity.