Search code examples
phpdrupal-7drupal-hooksdrupal-fieldsdrupal-field-api

Drupal-7 how to get hook_field_[formatter_]prepare_view() invoked without overwriting existing formatter


From my module, I'm looking for a way to change text-fields value during rendering process, but WITHOUT creating a new formatter, and BEFORE the currently affected formatter works.

In other words I want my changes always made on any text-field, as a generic preparatory step, regardless of which formatter will work afterwards.

For this to work:

  • I first considered using hook_field_formatter_prepare_view().

    To get it invoked, I wanted to use hook_field_formatter_info_alter() to add my module name to each involved formatter found here. But it appears that the "module" index only accepts a unique module-name, not an array.
    BTW I'm quite surprised by this lack: I seem it should make sense to allow a sequence of formatters, like are allowed a sequence of filters!

  • Then I considered using hook_field_prepare_view(), which seemed to be the best candidate since the doc sayd it runs before the formatters own hook_field_formatter_prepare_view(). But that doesn't work either: this hook is invoked only for a field created by the involved module (this issue had been discussed here).

Any idea? Thanks in advance.


Solution

  • I actually found a pretty way to do what I looked for.
    The method is quite invasive but works fine and may be re-used for different cases.

    1. To be as clear as possible, first I rephrase my question in terms of a general use case:

    In the rendering process, how to permit a module to change value of one or more fields (given field-id, given field-type...) before the formatter (if any) do its own job?

    2. The problem to accomplish this:

    We can't make the module define a new formatter, because only one may be defined at the same time for the same field

    3. The strategy which led me to the desired result:

    • use hook_field_formatter_info_alter() to run through existing formatters and "graft" my module inside of those where I wish to intervene
      (see detail under 4 below)
    • use hook_field_formatter_prepare_view() to:
      (a) execute the required changes in field values
      (the job my module is intended to: here it may be done or not, upon all fields of a given type or precisely identified fiels and so on, depending on any detailed needs)
      (b) again run through formatters list and, when involved, fire their own hook_field_formatter_prepare_view() if it exists
      (see detail under 5 below)
    • do the same job as in (b) above, successively for each of the other possibly involved hooks of any formatter:
      hook_field_formatter_view()
      hook_field_formatter_setting_form()
      hook_field_formatter_setting_summary()

    4. Detail about how to graft my module in the process:

    Whith hook_field_formatter_info_alter(&$info) we face the following $info structure:

    $info = array(
      ['formatter machine name'] = array(
        ['label'] => 'Human readable formatter description',
        ['field types'] => array(
          [0] => 'a_field_type,
          [1] => 'another_field_type',
          # ...
        ),
        ['settings'] => array(
          ['option A'] => 'option A value',
          ['option B'] => 'option B value',
          # ...
        ),
        ['module'] => 'formatter_module_name',
      ),
      ['formatter machine name'] = array(
        # ...
      ),
      # ...
    );
    

    We can easily run through the formatters list and look at "field types" index to select which ones are concerned by our needs.
    Then for each involved one, we can:

    1. substitute our own module name to formatter module name in "module" index
    2. add a new sub-index in "settings" index (say "our module graft") to register the original formatter module name

    So our hook_field_formatter_info_alter() will be something like:

    function mymodule_field_formatter_info_alter(&$info) {
      if($info) {
        foreach($info as $name=>$formatter) {
          if(
            !@$formatter['settings']['mymodule graft'] # or already grafted
          and
            array_intersect($formatter['field types'],
              array('text','text_long','text_with_summary')) # here it is for text fields only
          ) {
            # substitute mymodule to original module:
            $info[$name]['settings']['mymodule graft']=$formatter['module'];
            $info[$name]['module']='mymodule';
          }
        }
      }
    }
    

    Once flushing class registry, now all involved fields have their formatting phase redirected to our own module.
    NOTE: installing a new formatter now requires flushing class registry again, in order our module to take it in hand also.

    5. Detail about how to make original formatters to work after us:

    As stated above, now it is our own module which is notified when a field has to been formatted, rather than the originally affected formatter.
    So we must react in our hook_field_formatter_prepare_view(), which should look like:

    function mymodule_field_formatter_prepare_view(
      $entity_type,$entities,$field,$instances,$langcode,&$items,$displays
    ) {
      # here we do our own job with field values:
      if($items) {
        foreach($items as $nid=>$node_data) {
          # ...
        }
      }
      # then we give original formatter a chance to execute its own hook:
      foreach($displays as $display) {
        $hook=
          $display['settings']['mymodule graft'].'_field_formatter_prepare_view';
        if(function_exists($hook)) {
          $hook(
            $entity_type,$entities,$field,$instances,$langcode,$items,$displays
          );
        }
      }
    }
    

    Finally we also must give a chance to other formatters hooks to execute.
    For each one, it should look like (replace HOOK and ARGS by the right data for each hook):

    function mymodule_field_formatter_HOOK(ARGS) {
      $hook=$display['settings']['mymodule graft'].'_field_formatter_HOOK';
      if(function_exists($hook)) {
        return $hook(ARGS);
      }
    }
    

    Hope this helps...