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

496 Views Asked by At

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.

1

There are 1 best solutions below

0
On BEST ANSWER

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...