Skip to main content

Object-oriented page callbacks for Drupal 7

In Drupal we use object-oriented page and form callbacks to ease our programmning burden This is a nice improvement that allows us to encapsulate the functionality of one or many page callbacks into objects, with all the benefits that brings. Is it possible for us to us object-oriented page callbacks in Drupal 7? With a few tricks, yes it is. This article shows you how.

This is part of a continuing series of using Drupal 8 programming techniques in Drupal 7.

by chris.skene /

Before you start

You should be familiar with using PHP namespaces and PSR-0 autoloading with X Autoload. See Lee Rowlands' introductory blog post for details.

Object-oriented page callbacks

Drupal 8 page callbacks are now handled by Classes. This is a nice improvement that allows us to encapsulate the functionality of one or many page callbacks into objects, with all the benefits that brings. Is it possible for us to us object-oriented page callbacks in Drupal 7? We shall see...

Lets start with an example... In Drupal 8, the code which renders a Book now consists of a YAML routing file defining what used to be a hook_menu() implementation, and a class, \Drupal\book\Controller\BookController(), which handles several of the Book modules callbacks:

  • Book rendering
  • Administering Book outlines
  • Exporting Books

In addition to wrapping these three previously separate functions into a single Class, the BookController also implements Symfony's ContainerInjectionInterface, so it's two key services, BookManager and BookExport, can be injected. This kind of behaviour potentially improves the testability of a page controller. We can now write tests for it, and potentially mock the dependencies. Its better code reuse, it works better with IDE's and it makes everyones life easier.

There are limitations to the extent of testability in Drupal 8, but its better than what we have in Drupal 7, where we're still stuck with traditional, simple function callbacks. Modules defining multiple page callbacks are forced to organise them into multiple include files, or one monolithic module file, and sharing code between common callbacks is often minimal. Apart from being verbose, it also encourages the negative pattern of placing application logic inside page or submit callbacks.

However, a special technique allows us to write proper object-oriented page callbacks, in Drupal 7. Using a quirk of Drupal's menu routing functionality, we can use static class constructor methods directly from Drupal 8 to convert standard page callbacks into fully-fledged callback Controllers.

Static method page callbacks in hook_menu()

Normally, in Drupal 7, to specify a page callback in hook_menu(), we'd use a simple function callback…

  function robot_menu() {

    $menu['admin/config/robot/status'] = array(
      'page callback' => 'robot_page_status',

      ...
    );

    return $menu;
  }

  function robot_page_status() {
    …
    return $some_renderable_data;
  }

The way Drupal returns content to the user in 7 is actually very simple. While there's lot's of additional work going on around it, the core of a page request is a simple call to call_user_func_array(). 

  $page_callback_result = call_user_func_array($router_item['page_callback'], 

While we can't pass an instantiated object to this function, we can use it to call at static method on a class. For example, 

	function robot_menu() {

	  $menu['admin/config/robot/status'] = array(
	    'page callback' => 'RobotPageController::status',
	    ...
	  );

	  return $menu;
	}

This will call the 'summary' method, on the following RobotAdminPageController class:

	class RobotPageController {

	  static public function status() {

	    return $some_renderable_data;
	  }
	}

Adding PSR-0 class loading to the mix

In Lee Rowland's previous article, he introduced the concept of using the X Autoload module and PSR-0 compatible classes to introduce autoloading into Drupal 7. PSR-0 and autoloading brings us several benefits, including that we can now specify parent classes and interfaces and have them load automatically, bringing us inheritance and other useful programming techniques. This, in itself, is not a new technique, but it is useful, and we can combine it with our static callbacks to get more value from our objects, and move closer to the Drupal 8 standard.

Apart from specifying X Autoload as a module dependency, you'll want to create a new Controller class to handle your callback. 

	namespace Drupal\robot\Controller;

	class RobotPageController {

	  static public function robotStatusPage() {

	    return $some_renderable_data;
	  }
	}

This file should be placed in the standard PSR-0 location for Drupal classes, which is /lib/Drupal/robot/Controller/AdminPageController.php 
You now need to update your menu callback accordingly...

	function robot_menu() {

	  $menu['admin/config/robot/status'] = array(
	    'page callback' =>      
	          '\Drupal\\robot\\Controller\\AdminPageController::robotStatusPage',
	    ...
	  );

	  return $menu;
	}

Note that we're using the full namespaces class name, and that we also need to escape all the backslashes (with another backslash) except for the first one. 

What happens next? Well, Drupal's routing system effectively tests if the page callback is "callable", and if it is, calls it. So, our static method works much like a regular function callback.

That only gets us part of the way. In theory, that means we could also use something like 'page callback' => array('SomeObject', 'someMethod'), however due to some programming quirks, that won't get saved properly, so we are left with static object methods. However, we can use a lazy instantiation trick from Drupal 8 to get us fully-loaded objects to work with, and its very simple. By using the static keyword in our static function, we can create a normally, fully populated object.

	namespace Drupal\robot\Controller;

	class RobotPageController {

	  static public function robotStatusPage() {
	    $controller = new static();
	    return $controller->getStatusPage();
	  }

	  public function __construct() {
	    // Do any setup.
	  }

	  public function getStatusPage() {
	    return $some_renderable_data;
	  }
	}	

The static method robotStatusPage() is now a factory function. It creates a fully instantiated version of itself, then calls our new getStatusPage method.

Why might this be useful? Well, we can now do a few extra things... While Drupal 7 doesn't have a dependency injection container, there's no reason why you couldn't add a fairly basic one at this point. Or, you could simply add some dependencies in either the static factory method or the constructor. For example, you might do something like the following, to ensure that every RobotPageController always has a Robot and a RobotStatusGetter fully loaded and ready to use:

	class RobotPageController {

  		static public function robotStatusPage() {
    			$robot = new Robot();
    			$statusGetter = new RobotStatusGetter();

	    		$controller = new static($robot, $statusGetter);

    			return $controller->getStatusPage();
  		}

  		public function __construct(Robot $robot, RobotStatusGetter $status) {
    			$this->robot = $robot;
    			$this->status = $status;
  		}
	}

Reusing our controller

Because we're now well in to our journey into OO page callbacks, we've already created a lot of opportunities for reuse. We can extend controllers and start to define Interfaces to assist reuse. However, we've still got one more trick up our sleeves…

One of the benefits of object oriented code is the ease with which we can reuse existing components, so lets reduce our reliance on boiler plate code by creating a reusable controller class we can extend for our individual page callbacks. 

The example below is a little more complicated, but show's how you can combine the 'page arguments' key in hook_menu() with a page controller class to produce a dynamic controller. This example is taken directly from the Page Controller module, which provides exactly this for Drupal 7.

	/**
	 * Implements hook_menu().
	 */
	function hook_menu() {

	  $menu = array();
	  $menu['my/page/callback'] = array(

	    // Your page callback should always be the same.
	    'page callback' => '\Drupal\\page_controller\\Controller\\PageController::createPage',

	    // Your page arguments should start with your Controller, and the method
	    // to call.
	    'page arguments' => array(
	      // Your page controller, which extends PageController.
	      '\Drupal\\my_module\\Pages\\ExampleController',
	      // The method to call.
	      'myPageControllerViewCallback',
	      // Any other arguments to pass to the callback.
	      'foo'
	    ),
	  );

	  return $menu;
	}

Then, in lib/Drupal/page_controller/Controller/PageController.php...

	namespace Drupal\page_controller\Controller;

	/**
	 * Class PageController
	 */
	class PageController {

	  /**
	   * Static factory function.
	   */
	  static public function createPage() {

	    $args = func_get_args();
	    $controller_name = array_shift($args);
	    $method = array_shift($args);

	    if (class_exists($controller_name)) {
	      $controller = new $controller_name();

	      return call_user_func_array(array($controller, $method), $args);
	    }

	    throw new \Exception('Invalid Page Controller');
	  }
	}

And finally, in lib/Drupal/page_controller/Example/ExampleController.php...

	namespace Drupal\page_controller\Example;

	use Drupal\page_controller\Controller\PageController;

	/**
	 * Class ExampleController
	 */
	class ExampleController extends PageController {

	  /**
	   * Example view callback.
	   */
	  public function myPageControllerViewCallback($arg1) {
	    // Do something with $args.
	    return 'Some output';
	  }
	}

Posted by chris.skene
Drupal consultant

Dated

Comments

Comment by Les Lim

Dated

How would you recommend handling hook_menu's "access callback" property? D7's _menu_check_access() function expects the access callback to be a function specifically, so passing a static class method doesn't seem to work.

Comment by chris.skene

Dated

I think you answered your own question. Lots of parts of the menu system in 7 don't cope with objects at all, for no real reason (the Form system is in the same boat). Without hacking core, we're stuck with it.

If you still want to use your object, for whatever reason - perhaps you want to preserve the use of a Drupal 8 compatible object, you could call it from the function using a similar same method to that outlined above. If you want to use a generic method, you could even pass the object name as an argument.

Comment by pedro.rocha

Dated

Good approach Christopher! I'm working a lot with a similar one throughout the last year, and i just uploaded to https://drupal.org/sandbox/pedrorocha/2238171

If you could give a feedback, would be great!

Comment by chris.skene

Dated

Thanks pedro.rocha. I see you've taken a slightly different, discovery-based approach with your module. My aim with Page Controllers was to see how closely and simply we could get Controllers working like Drupal 8, which has started to move away from event-based discovery systems for base configuration like menu's.

That said, while conventions are good, for something like a page controller I would prefer some sort of auto discovery anyway. I guess the limitation is that the implementer has very limited control over hook menu. You could combine the Page Controller approach of a lazy static factory on the object, and return a hook menu settings array as well. That might require the addition of a base abstract class, but it would encapsulate the menu functionality more fully within the object.

Pagination

Add new comment

Restricted HTML

  • Allowed HTML tags: <a> <em> <strong> <cite> <blockquote> <code> <ul> <ol> <li> <dl> <dt> <dd> <h2> <h3> <h4> <h5> <h6>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Not sure where to start? Try typing "hello" or "help" if you get stuck.