Submitted by chrishu on Wed, 02/05/2014 - 15:11

Introduction

Recently I hit a roadblock on the long road to eventual mastery (hopefully) of Drupal 8, most of the things in tutorials and 'Hello World' type module examples are pretty straightforward. but now I have arrived at the bit where I have to map out an architecture for potentially fairly complex modules I have had to take a step back and spend more time thinking, investigating and learning about Symfony 2 etc..

I started this post a while ago and drastic changes were occurring as I did so, I think things are settling down a bit now and I am feeling ready to "grasp the nettle" again. Apologies the style of the post also changes over the two month intervening period.

Unfortunately Core and the current Drupal documentation don't really help to explain best practices for different Controller use cases. Guidelines that are there for Controllers are focused on Core Drupal at moment and not Contrib (imho).

This post is about  Controller classes in Drupal 8, particularly those that either service _content or _controller defaults in Routes. It also touches on Routes and Services and these three are probably the most important elements of the 'new' Drupal.

This post is a bit more technical than some of the previous ones, but the intention is not to present full technical solutions, or full examples of code, I am still feeling my way around these topics. in future I will try to post more specific information in an article based format when appropriate. The post is also pretty long by modern standards (apologies)  so it may just serve as a marshalling ground for my current thoughts. There are links though so it  could serve as a useful springboard. Any feedback including violent disagreement will be most useful and welcome.

After reviewing the post code examples are more related to things not to do, oh well eventually will have enough mileage to wrtie some technical articles and right now am itching to get coding again in D8.

I am going to present a few thoughts and ideas and invite comment.

Background 

This post is mainly about Controllers in Drupal 8 and in particular Controller classes and Thin Controllers but also involves Containers, ServicesRouting and Dependency Injection. These concepts all work together so there is considerable overlap in some of the resources linked to below. There are a lot of resources around, especially relating to Symfony, what is most useful to you will probably depend on your background, I have included a little background in case you have not had the chance to investigate Drupal 8 or any framework that uses these concepts.

Controller

The Symfony documentation is a good place to start learning about controllers, there are some important differences however. For example the responsibility of a Symfony controller is to return a Response, in Drupal, routing can also define that the controller will return a render array (to be rendered as main page content) or a form amongst other things. Symfony terminology can be a little confusing at times also because technically a controller/action can simply be a function that returns a Response, however controller classes (which may encapsulate a number of these) are also commonly just referred to as Controllers.

Container, Services and Dependency Injection

The Symfony 2 introduction to service containers is worth reading as an introduction. There is some developing Drupal documentation on Services and Dependency Injection in Drupal 8 also.

A particularly good (although slightly dated now) presentation on Dependency Injection by Kat Bailey at Portland Drupalcon is a good place to start, if you need to.

Services cover a very wide area, the Container will know about services for all kinds of purposes, as the Symfony Book describes a service as any PHP object that performs some sort of "global" task that is quite a generic definition. We can be sure that many Controllers will want to use a variety of services. The Symfony Book page linked to above is a good place to start learning about Services and the Container.

Much of the interesting custom functionality that you may be wrting is going t be found in services.

This post: Understanding Drupal 8, part 2: The Service Container is one of few that just focust on the container.

Routing

The Controller features heavily in Routing, this presentation Drupal 8 routing: The method in the madness by Tim Plunkett is a good intro. and touches on all areas.

Thin Controllers

Controllers are not meant to be the place for complex business logic, this Symfony related post helps to explain more, Thin Controllers are consistently touted as usual best practice for Symfony and Drupal. Heavy lifting should mostly be in services. In Drupal the ControllerBase abstract class is provided as a class that can be extended by a thin controller, this class is similar in purpose to the Controller class provided by the Symfony framework.

A Thin Controller doesn't require explicit testing, if business logic is in the Services the Controller is mainly acting as a target for the Route and then handing off the bulk of the work to a Service. A Thin Controller should be simple enough that any fundamental problems will quickly show up in integration testing. In mechanical terms it is like the linkage between a button and a complex clockwork mechanism. 

I think though that especially in Contrib land many controllers will end up overweight initially as if this kind of architecture is new to you then it will take time to sink in and get familiar with what code to put where.

Controllers need Services

Generally Controllers need Services to do anything useful. Services themselves can be nicely independent, they may need other Services to do their work but these can be configured to be passed to their constructors, so long as they get something that looks and behaves as they expect they are happy, subsequent code can reconfigure what they actually get and/or mock services can be provided for testing. 

Controllers sit in a slightly awkward place (hence occaisional 'holy wars' and differing ways to use them in many frameworks).  Services reside in and can be retrieved from the Container but to get at them the Controller needs access to and knowledge of the Container (which can make make people feel uncomfortable particularly when making ad hoc and not immediately apparent access to the container). By default Controller configuration occurs in routing and there is no way at that point to select services to inject into it (in fact the router should NOT need to know what the Controller requires to fulfill it's obligations).

One way to get a service into a Controller is through the static Drupal class (DO NOT DO THIS THOUGH) that has been provided for procedural style code to get access to the Service Container and/or useful services etc., this class contains some specific methods to get particular services for example:

\Drupal::cache()->delete('comment_entity_info');

There is also a more generic method to allow retrieval of any service, for example:

// Get BookManager service
$book_manager = \Drupal::service('book.manager');

Alternatively you can access the container directly, for example:

There are a few other bits of deprecated code lying around for static access to the Service Container, you may see reference to drupal_get_container() for example and the getContainer method on the Drupal class.

But however you should NOT be using any of these in your Controllers, if you are coding a Controller you are doing things the new way, so get with the program ;).

Modifying other peoples code

In Drupal 7 you had a number of ways in which you could surgically target and modify functionality in core and contrib code (because hacking it directly would be wrong of course). You could use menu alter hooks to swap in a new page callback, hook_module_implements_alter to change a hook implementation etc. etc.

In Drupal 8 you will have to start thinking a little differently, you can alter routing (to change what happens for a specific Route), you can replace Services in the Container with a new version but in most cases you have no easy way to swap out an entire Controller (I do finish with a suggestion).

Controllers in core

I have a question: is the best practice for controllers in core always best practice for controller in contrib modules? For many the assumption would be yes, I am not entirely.comfortable with that. Bear in mind that Controllers in core are in a state of flux and 'planned progression' in many cases, which leaves a bit of a vacum as most developers would like some clear examples they can follow, many Core controllers are unfinished.

Extending ControllerBase

ControllerBase class is similar in purpose to the Symfony Controller class providing a number of utility methods and Container access to reduce boilerplate code, in essence it provides bunch of useful core services. Initially it irritated me, but has changed recently so not quite so irritating (details to follow, essentially though I am glad to see that this issue has been committed).

In Symfony you may see people arguing against using the Controller class, the arguements are weaker when considering Drupal. Symfony is very componentised and in theory  somebody may want to use your code without using the FrameworkBundle (which they can't because you are coupled to it) instead you can use ContainerAware from the Dependency injection component.. This type of arguement is

I suspect ControllerBase will be the default starting point for most budding Controller authors it can even now be built up as it already implements the ContainerInjectionInterface (prior to this some core Controller were both extenting ControllerBase and implementing ContainerInjection interface which seemed rather clumsy.

Controller base does currently have a reference to the Container hidden away in there that you may be tempted to use directly (DON'T) this could vanish or change. If you need more Services than look to the ContainerInjection interface and get them handed to the constructor of your Controller.

Think about testing however. The documentation for Controller base does warn against it's use if the Controller class requires unit testing. Although as already mentioned above it is quite likely that you can make a good case for not testing your controller if you can keep it thin.

Implementing ContainerInjectionInterface

Possibly a more 'grownup' method of setting up your Controller is with the ContainerInjectionInterface this requires you to implement a static create method where you can define the Services that should  be passed to the constructor of your Controller (that trips off the tongue). 

The waters are somewhat clearer/muddier now that ControllerBase also implements ContainerInjectionInterface.

The best way to see what is happening is to follow the link and look at some of the Controllers that are implementing. A good controller to keep track of in all respects is the BookController  I have included it's current create method below and we can see that the Controller wishes the book.manager and book.export services to be passed to it's constructor.

 /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container) {
    return new static($container->get('book.manager'), $container->get('book.export'));
  }

Implementing ContainerAwareInterface

The Symfony ContainerAwareInterface is also available to use in Drupal and this allows you to recieve the Container in your Controller constructor a number of Core Controllers are doing this but I don't beleive it would be recommended or needed unlesss doing some pretty edgy, I have not had a chance to review them yet and work out eactly why they need to do this.

Controllers as Services

You can define your Controller as a Service, define your dependancies as you would for any Service, providing the added advantage that your entire Controller could easily be swapped out.

This would be my preferred way but has already kicked off the odd holy war in Symfony and does not appear to be advocated by any Drupalers. It does work though and can be done in a similar manner as with Symfony.   

Conclusion

Although getting better I don't think things are clear yet, I will try to forumlate a solid defense of my gut preference of defining my custom controllers of services but fear it is too late for that, often times best practice is common practice anyway as at least everyone knows what to expect.

The documentation around ControllerBase and ContainerInjectionInterface is a bit confusion and possilby contradictory now that ControllerBase implements ContainerInjectionInterface. 

Many of the litlte tutorials I see either ignore the whole Container and Services issue entirely and just write a bare class (which is fine for hello world) or they blindly extend ControllerBase without using any of it or stating why. 

For me the encouraging thing to glean from all of this is that whilst pondering the whichness of the way over Drupal internals and best practice I can finally start drawing on previous experiences and other frameworks for inspiration and help, and THAT is REFRESHING. 

 

Comments

Anonymous (not verified)
Fri, 02/14/2014 - 15:09

Seems like the author never heard about a thing like "controller" before they were introduced to D8.