Making namespaced callbacks work in Drupal 7 (without hacking core and with bound parameters)

Making namespaced callbacks work in Drupal 7 (without hacking core and with bound parameters)

Difficulty: 
Let's Rock

What is the best way to prepare for Drupal 8 and make your projects easy (and cheap) to migrate to D8? Start using Drupal 8 programming patterns now as much as D7 allows you to....

I guess that most of you are already doing that - and have done for a few years now - with custom crafted frameworks that, as much as possible, use modern design patterns not stuck in 20 y/o spaguetty code. D7 is spaguetty, your custom modules and code need not to be so.

In example, to add a block using the Fast Development Framework for Drupal 7 you simply create a class in your module such as this one:

class BlockCourseActions extends BlockGeneric implements IBlock {
  public static function Info() {
    return \t('Acciones de curso.');
  }

  public static function Cache() {
    return DRUPAL_CACHE_PER_PAGE;
  }

  public static function Classes() {
    return array(static::className(), 'embed', 'important');
  }
  
  public static function View() {
    $block = array();
    $block['subject'] = 'My Subject';
    $block['content'] = array(); // whatever
    return $block;
  }
}

No need to implement hooks, and everything related to your block is kept in a single class instead of being spread all over the place with procedural code. This block class is going to be auto discovered by the framework and  the proper hooks will be consumed to make this a working D7 block.

One of the issues of using sane programming design patterns in D7 is that Drupal 7 core (and contrib) usually rely on string based callbacks. For example to define a menu callback you would use something like this:

function MYMODULE_menu() {
    $items = array();
    $items[static::path_CatalogoCursoReservar('%entity_object')] = array(
      'title' => 'Inscripción al curso',
      'page callback' => 'my_spaguetty_global_function',
      'access callback' => TRUE,
    );
    return $items;
}

In this case 'my_spaguetty_global_function' would be used as a callback target. Sometimes you can even pass an ".inc" file to the caller so that it will know what script to include before calling your global function. 

But what if I want to define the callback in a static method inside a class and have some autoloading magic? You could guess this will work:

function MYMODULE_menu() {
    $items = array();
    $items[static::path_CatalogoCursoReservar('%entity_object')] = array(
      'title' => 'Inscripción al curso',
      // We would not really use a literal string here... but for the sake of simplicity...
      'page callback' => '\Drupal\MYMODULE\router\router1::MyMethod',
      'access callback' => TRUE,
    );
    return $items;
}

The truth is that it works depending on how the callback is consumed.

These are (some of) the possible callback consumption patterns:

$callback = '\Drupal\MYMODULE\MYCLASS::MyCallback';

// Method 1: function_exists + invoke
// Only works with global functions
if (function_exists($callback)) {
  $result = $callback();
}

// Method 2: direct invoke
// Only works with global functions
$result = $callback();

// Method 3: call user func or call_user_func_array
// Works with namespaced global functions
call_user_func($callback, $param1, $param2);

// Method 4: call_user_func_array
// Works with namespaced global functions
call_user_func_array($callback, array($param1, $param2))

The methods that do not work fail because:

  • Static methods in classes cannot be directly invoked, and need to be called with call_user_func or call_user_func_array. Otherwise autoloading will not work.
  • function_exists only works for global functions, and is_callable should be used instead

The solution is easy:

  • Replace all direct string invocations and replace them with a call_user_func() or call_user_fun_array()
  • Replace all calls to function_exists with is_callable.

Is there a way to workaround this situation without hacking core?

Isac, one of our team members, came up with a nice proof of concept code.

Place this helper function in the .module file of your custom framework:

/**
 * Helper to call FQN Methods
 * 
 * @param string $fqn FQN of a method
 * @return CallableHelper
 */
function fdf_call_fqn($fqn, ...$args){
  $result = new CallableHelper($fqn, $args);
  return $result;
}

This is what CallableHelper looks like:

namespace Drupal\fdf\Utilities;

class CallableHelper {

  private $callable = NULL;
  private $args = array();

  /**
   * Constructor
   * @param string $callable_name
   */
  function __construct($callable_name, array $args){
    if (!is_callable($callable_name)) {
      throw new \Exception('Method cannot be used as a callback. Make sure it is static and public!');
    }
    $this->callable = $callable_name;
    $this->args = $args;
  }

  /**
   * @return mixed
   */
  public function __invoke(&...$args) {
    $result = call_user_func_array($this->callable, array_merge($this->args, $args));
    return $result;
  }
}

You can now use any static method in any class as a callback without worrying how the consuming code is doing it's job. You can even pass custom pre bound parameters for it, as long as they are serializable types (no closures!).

For example, to build a form element using FAPI with an element validation function placed in a static function you can now use this:

    $form['campusconfig']['formacion_url'] = array(
      '#type' => 'textfield',
      '#title' => t('URL Base del sitio'),
      '#size' => 60,
      '#maxlength' => 255,
      '#required' => TRUE,
      '#rules' => array('url'),
       // You'd rather use \Drupal\MYMODULE\MYCLASS::class with a custom verifier. 
      '#element_validate' => array(fdf_call_fqn('\Drupal\MYMODULE\MYCLASS::MYVALIDATIONCALLBACK')),
      '#element_validation' => array('url'),
    );

UPDATE: For major resons we had to backport this code to PHP 5.5

The only incompatible parts are the variadic arguments usage (...$args)

Most of the conversion is trivial, except for the Invoke() method in CallableHelper - in order to maintain the pass by reference behaviour.

  public function __invoke(&$p0 = NULL, &$p1 = NULL, &$p2 = NULL, &$p3 = NULL, &$p4 = NULL, &$p5 = NULL, &$p6 = NULL, &$p7 = NULL, &$p8 = NULL, &$p9 = NULL, &$p10 = NULL, &$p11 = NULL) {
    // This mess is not necessary if you use variadic arguments (...$args)
    // but we need this to be PHP 5.5 compatible. Some callbacks require
    // arguments to work by reference.
    $arguments = array();
    foreach ($this->args as &$arg) {
      $arguments[] = &$arg;
    }
    for ($x = 0; $x < func_num_args(); $x++) {
      eval("\$arguments[] = &\$p{$x};");
    }
    $result = call_user_func_array($this->callable, $arguments);
    return $result;
  }

 

Comments

... is PHP 5.6. Just sayin'

In light of https://www.drupal.org/node/2470679#comment-10001385 you are literally saying "D7 is fast, your custom modules and code need not to be so"

Slow down with the OOP kool aid, in PHP userspace it has a performance drawback.

This, of course, is much less pontificated than the "spaghettiness". Also, using functions instead of methods has *nothing* to do with spaghettiness, cyclomatic complexity has. https://github.com/symfony/DependencyInjection/blob/master/Loader/YamlFi... how is this not spaghetti?

Or if you think drupal_render was bad, then what will you say about https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Render%21... the Drupal 8 equivalent?

Yes, many Drupal 8 features are awesome but the design made it inherently slow and incredibly hard to comprehend and the caching hacks piled on top to make it fast again made it absolutely impossible to grok.

In light of https://www.drupal.org/node/2470679#comment-10001385 you are literally saying "D7 is fast, your custom modules and code need not to be so"

Fast to run or fast to code? OOP makes complex systems easier to maintain, more robust and more sane to understand. In other words, it makes big, complex and mission critical systems cheaper to develop and maintain and with a higher level of quality in every sense of what quality can mean. 

Slow down with the OOP kool aid, in PHP userspace it has a performance drawback.

I don't want to raise the argument into how badly broken (and more stuff that just feels bad to say) is PHP itself or the PHP ecosystem. You are very right in pointing out that PHP - more specifically the Zend Engine - is the hell of an unperformant piece of software. OOP was invented to save development time (and many other things). Other languages were designed from the ground with OOP in mind, not patched to support it as it happened with PHP.

This, of course, is much less pontificated than the "spaghettiness". Also, using functions instead of methods has *nothing* to do with spaghettiness, cyclomatic complexity has. https://github.com/symfony/DependencyInjection/blob/master/Loader/YamlFi... how is this not spaghetti?

I probably missused the term spaghetti:

a white, starchy pasta of Italian origin that is made in the form of longstrings, boiled, and served with any of a variety of meat, tomato, or other sauces.

What I meant to say with the 20y/o spaghetti code statement is not that spaghetti code is something of the past, it is that many things in PHP, including Drupal 7, feel like 20y/o. And I really mean it. I probably was 10 when I started programming with Visual Basic in a Windows 3.1 machine and every time I have to touch PHP or D7 code it brings me some good memories from that time.

Add new comment

By: david_garcia Sunday, June 7, 2015 - 00:00