Entity Diff: improve support for webform submissions

Created on 4 June 2024, 7 months ago
Updated 30 August 2024, 5 months ago

Problem/Motivation

It is often useful to get a list of fields that have changed, or the actual diffs, between two versions of an entity, or two different entities.

Drupal core provides a utility class that does the hard work of comparing two arrays, making such a plugin easy to implement.

Proposed resolution

Create an ECA action plugin that compares two entities, which can be used with an Update event comparing the original values of an entity with the changed values.

Remaining tasks

Get in correct name space/submodule. Create tests. Document.

User interface changes

New action plugin available, with options to specify two entities to compare, whether to show value changes or only the field names, and a list of specific fields/subkeys to either ignore or show.

✨ Feature request
Status

Fixed

Version

2.1

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States freelock Seattle

Live updates comments and jobs are added and updated live.
Sign in to follow issues

Merge Requests

Comments & Activities

  • Issue created by @freelock
  • πŸ‡ΊπŸ‡ΈUnited States freelock Seattle

    We created this plugin for a client, and have had it in production for a year now!

    
    namespace Drupal\qcyc_roster\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 = "qcyc_roster_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

    Thank you so much @freelock for this contribution, this is nicely implemented.

    One thought, though, whether we should make this slightly more generic. Currently, this compares 2 entities by casting them to arrays first. This would probably not work for webform submission data, I guess. And other arrays/objects may be needed to be compared in a similar way as well.

    We could turn this into an "Objects diff" action that accepts 2 objects as input. If those objects happen to be arrays already, this can simply continue. If not, the action tries to cast them to arrays first, just like you did with the entities.

    With that in place, we could then also add a condition that verifies if 2 objects are different.

    Both those plugins could be implemented in the eca_base sub-module and it wouldn't take too much extra effort when we build that on top of your code. I'll start that in an issue fork, then you and maybe others can review that.

  • Status changed to Closed: duplicate 7 months ago
  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen

    Oops, my IDE warned me about duplicate code when I started to implement this. I was surprised and reached out to the other instance, which looked identical. And guess what: we've already implemented that code from @freelock about a year ago, see ✨ New action/condition: entity diff Fixed .

    So, we can close this one as a duplicate. Unfortunately, we don't have the more generic approach in the existing implementation, but maybe webform submissions can be compared with the entity diff as well? We will do some tests and open a separate issue, if that doesn't work for some reason.

  • Status changed to Active 7 months ago
  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen

    I'm re-opening this as we have to improve the existing implementation to also support webform submission data.

  • Pipeline finished with Running
    7 months ago
    #191590
  • Status changed to Needs review 7 months ago
  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen

    This now works with webform submissions as well, please give it a try and set to RTBC, we can then still add this to ECA 2.0.0

  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen
  • Pipeline finished with Success
    7 months ago
    Total: 501s
    #197215
  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen

    I've just tested this again, the use statement is valid even without the webform being present. @freelock do you want to review that quickly as well?

  • Pipeline finished with Skipped
    5 months ago
    #255929
  • Status changed to Fixed 5 months ago
  • πŸ‡©πŸ‡ͺGermany jurgenhaas Gottmadingen

    I've now decided to merge this.

  • Automatically closed - issue fixed for 2 weeks with no activity.

  • Pipeline finished with Canceled
    about 2 months ago
    Total: 65s
    #347470
  • Pipeline finished with Success
    5 days ago
    Total: 1008s
    #392254
Production build 0.71.5 2024