Saturday, July 29, 2017

Changed Fields API

This is a simple API module for Drupal 7 and Drupal 8 which allows developers to react on changed fields in a node when it was updated. For example, you want to modify node object depends on its field values. Or you just want to know what fields were changed. Or finally, you need to check the difference between old and new field values and do some other thing depending on this difference.

Changed Fields API supports all the core's field types both for Drupal 7 and Drupal 8. But for Drupal 7 it supports even more. Please visit the project page to find out more information.

Changed Fields API is built on "Observer" pattern. The idea is pretty simple: attach observers to a node subject which will notify all of them about changes in node fields. If you are not familiar with this pattern yet then I suggest you read about it and consider this simple example. So let's find out how to use this API. An example below is based on changed_fields_basic_usage and changed_fields_extended_field_comparator demo modules that are the part of Changed Fields API module.

Observer


The first thing we need is an observer. Define a class that implements ObserverInterface interface. This interface provides two methods: ObserverInterface::getInfo() and ObserverInterface::update(SplSubject $nodeSubject). The first method should return an associative array keyed by content type names which in turn contain a list of field names you want to observe. A second method is a place where you can react on changed fields and do something with a node. It will be called only if some of the listed fields were changed.

<?php

/**
 * @file
 * Contains BasicUsageObserver.php.
 */

namespace Drupal\changed_fields_basic_usage;

use Drupal\changed_fields\ObserverInterface;
use SplSubject;

/**
 * Class BasicUsageObserver.
 */
class BasicUsageObserver implements ObserverInterface {

  /**
   * {@inheritdoc}
   */
  public function getInfo() {
    return [
      'article' => [
        'title',
        'body',
      ],
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function update(SplSubject $nodeSubject) {
    $node = $nodeSubject->getNode();
    $changedFields = $nodeSubject->getChangedFields();

    // Do something with $node depends on $changedFields.
  }

}

So here we defined that we want to listen to article content type and we want to check only title and body fields.

Node presave


In order to detect changed fields in a node we need to define hook_node_presave and there do several steps:
  1. Wrap a node object into an instance of NodeSubject class and set up the field comparator. Field comparator is an object which checks needed fields and returns differences between old and new field values. Default comparator is default_field_comparator but you can define your own by extending default one. 
  2. Then attach your observer BasicUsageObserver to instance of NodeSubject class.
  3. Finally, execute NodeSubject::notify() method and react on this event in BasicUsageObserver::update(SplSubject $nodeSubject) if some of the field values of registered node types have been changed.

<?php

/**
 * @file
 * Contains changed_fields_basic_usage.module.
 */

use Drupal\changed_fields\NodeSubject;
use Drupal\changed_fields_basic_usage\BasicUsageObserver;
use Drupal\node\NodeInterface;

/**
 * Implements hook_node_presave().
 */
function changed_fields_basic_usage_node_presave(NodeInterface $node) {
  // Create NodeSubject object that will check node fields by DefaultFieldComparator.
  $nodeSubject = new NodeSubject($node, 'default_field_comparator');

  // Add your observer object to NodeSubject.
  $nodeSubject->attach(new BasicUsageObserver());

  // Check if node fields have been changed.
  $nodeSubject->notify();
}

Basically, that's all but you can say "what about field types that is not supported by Changed Fields API. How can I handle them?". Well, you're right, API supports a limited bunch of field types but it's easy to extend it.

By default, Changed Fields API comes with default_field_comparator class which supports all the core Drupal's field types. But if you have a field type from a contrib module you have to tell the API how to compare that fields. In order to do that you have to write a custom field comparator.

Custom field comparator


In Drupal 8 version of the module field comparators are plugins and they should be placed into module_name/src/Plugin/FieldComparator directory. So we need to define a class, extend it from DefaultFieldComparator and override method DefaultFieldComparator::getDefaultComparableProperties(FieldDefinitionInterface $fieldDefinition). All that you need is return field properties to compare depends on a field type. For example for text_with_summary fields API returns array('value', 'summary').

<?php

/**
 * @file
 * Contains ExtendedFieldComparator.php.
 */

namespace Drupal\changed_fields_extended_field_comparator\Plugin\FieldComparator;

use Drupal\Core\Field\FieldDefinitionInterface;
use Drupal\changed_fields\Plugin\FieldComparator\DefaultFieldComparator;

/**
 * @Plugin(
 *   id = "extended_field_comparator"
 * )
 */
class ExtendedFieldComparator extends DefaultFieldComparator {

  /**
   * {@inheritdoc}
   */
  public function getDefaultComparableProperties(FieldDefinitionInterface $fieldDefinition) {
    $properties = [];

    // Return comparable field properties for extra or custom field type.
    if ($fieldDefinition->getType() == 'some_field_type') {
      $properties = [
        'some_field_property_1',
        'some_field_property_2',
      ];
    }

    return $properties;
  }

}

Here we assume that we have a custom field type some_field_type with properties some_field_property_1 and some_field_property_2. In order to compare such fields, we need to tell node subject that it should use our newly defined field comparator:

<?php

/**
 * @file
 * Contains changed_fields_extended_field_comparator.module.
 */

use Drupal\changed_fields\NodeSubject;
use Drupal\changed_fields_extended_field_comparator\ExtendedFieldComparatorObserver;
use Drupal\node\NodeInterface;

/**
 * Implements hook_node_presave().
 */
function changed_fields_extended_field_comparator_node_presave(NodeInterface $node) {
  // Create NodeSubject object that will check node fields by your ExtendedFieldComparator.
  $nodeSubject = new NodeSubject($node, 'extended_field_comparator');
  ...
}

Cool, now we know how to add support for custom or additional contrib fields. But there might be cases when you want, for example, compare text_with_summary fields only by value property (by default API compares it by format and summary properties as well). In this case you need to override DefaultFieldComparator::extendComparableProperties(FieldDefinitionInterface $fieldDefinition, array $properties) method like this one:

public function extendComparableProperties(FieldDefinitionInterface $fieldDefinition, array $properties) {
    if ($fieldDefinition->getType() == 'text_with_summary') {
      unset($properties[array_search('summary', $properties)]);
      unset($properties[array_search('format', $properties)]);
    }

    return $properties;
  }

When it's done changes in field's summary or format properties will not be taken into consideration by the API.

Additional info

No comments:

Post a Comment