Module Development in Drupal 8
I spent a couple of hours today doing something that I've been wanting to do for a while now, but just hadn't made the time -- going through the changelog for drupal 8, and seeing if I could port a few custom modules from D7 to D8. I thought I'd share what I've learned thus far.
As an example, let's take a look at this simple D7 module (that really does nothing other than serve as a nice example for this blog post) called 'test'.
test.info
; $Id:$<br>name = Test<br>description = "Test Module"<br>package = "Test"<br>core = 7.x<br>files[] = test.module
test.module
<span style="color: #000000"><span style="color: #0000BB"><?php <br /></span><span style="color: #FF8000">/**<br> * @file<br> * Test module<br> *<br> */<br>/*<br> * Implementation of hook_menu<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">test_menu</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$items </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'test/%'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Test'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'description' </span><span style="color: #007700">=> </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Test'</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'page callback' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'_test'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'page arguments' </span><span style="color: #007700">=> array(</span><span style="color: #0000BB">1</span><span style="color: #007700">),<br> </span><span style="color: #DD0000">'access callback' </span><span style="color: #007700">=> </span><span style="color: #0000BB">TRUE</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">MENU_CALLBACK<br> </span><span style="color: #007700">);<br> return </span><span style="color: #0000BB">$items</span><span style="color: #007700">;<br>}<br>function </span><span style="color: #0000BB">_test</span><span style="color: #007700">(</span><span style="color: #0000BB">$variable</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$output </span><span style="color: #007700">= </span><span style="color: #0000BB">$variable</span><span style="color: #007700">;<br> return </span><span style="color: #0000BB">$output</span><span style="color: #007700">;<br>}<br>function </span><span style="color: #0000BB">test_block_info</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$block </span><span style="color: #007700">= array();<br> </span><span style="color: #0000BB">$block</span><span style="color: #007700">[</span><span style="color: #DD0000">'test'</span><span style="color: #007700">][</span><span style="color: #DD0000">'info'</span><span style="color: #007700">] = </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Test Block'</span><span style="color: #007700">);<br> return </span><span style="color: #0000BB">$block</span><span style="color: #007700">;<br>}<br>function </span><span style="color: #0000BB">test_block_view</span><span style="color: #007700">(</span><span style="color: #0000BB">$delta </span><span style="color: #007700">= </span><span style="color: #DD0000">''</span><span style="color: #007700">) {<br> </span><span style="color: #0000BB">$block </span><span style="color: #007700">= array();<br> switch (</span><span style="color: #0000BB">$delta</span><span style="color: #007700">) {<br> case </span><span style="color: #DD0000">'test'</span><span style="color: #007700">:<br> </span><span style="color: #0000BB">$block</span><span style="color: #007700">[</span><span style="color: #DD0000">'subject'</span><span style="color: #007700">] = </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Test block'</span><span style="color: #007700">);<br> </span><span style="color: #0000BB">$block</span><span style="color: #007700">[</span><span style="color: #DD0000">'content'</span><span style="color: #007700">] = </span><span style="color: #0000BB">t</span><span style="color: #007700">(</span><span style="color: #DD0000">'Here is the content'</span><span style="color: #007700">);<br> }<br> return </span><span style="color: #0000BB">$block</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
As you can see, the module registers a menu callback at the path 'test/%'. The callback simply prints the argument passed in via the url. The other thing the test module does is implement a custom block with hook_block_info and hook_block_view. Let's look at how to port the test module to D8.
I guess it's worth noting first of all, that you can install D8 via git like so:
git clone --branch 8.x http://git.drupal.org/project/drupal.git
Take a look at https://drupal.org/node/3060/git-instructions/8.x for a more in-depth discussion on how to install D8.
Once your installation is finished and you're up and running, you can get down to some coding. The first thing to note is that your .info file is now going to be a yaml file. So you'll need to rename the test.info file to test.info.yml. You can get the specifics of yaml by clicking through the preceding link, but the jist of it isn't all that different from how .info files were set up in Drupal 7. Replace all '=' signs with ':'. If an array was called for in D7 (for example with dependencies[] = node, etc), you'll now use indention and a '-' like so:
dependencies<br> - node<br> - some_other_custom_module
You can get more information about yaml and how it specifically relates to Drupal 8 .info files by visiting https://drupal.org/node/2000204. Here's what the test.info.yml file should look like:
name: Test<br>description: Test module<br>core: 8.x<br>package: Test<br>type: module
Next let's move on to the test.module file:
<span style="color: #000000"><span style="color: #0000BB"><?php <br /><br></span><span style="color: #FF8000">/**<br> * @file<br> * Test<br> *<br> */<br><br>/**<br> * Implementation of hook_menu()<br> */<br></span><span style="color: #007700">function </span><span style="color: #0000BB">test_menu</span><span style="color: #007700">() {<br> </span><span style="color: #0000BB">$items </span><span style="color: #007700">= array();<br> <br> </span><span style="color: #0000BB">$items</span><span style="color: #007700">[</span><span style="color: #DD0000">'test/{variable}'</span><span style="color: #007700">] = array(<br> </span><span style="color: #DD0000">'title' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'Test'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'route_name' </span><span style="color: #007700">=> </span><span style="color: #DD0000">'test_page'</span><span style="color: #007700">,<br> </span><span style="color: #DD0000">'type' </span><span style="color: #007700">=> </span><span style="color: #0000BB">MENU_DEFAULT_LOCAL_TASK</span><span style="color: #007700">,<br> );<br> <br> return </span><span style="color: #0000BB">$items</span><span style="color: #007700">;<br>}<br></span><span style="color: #0000BB">?></span></span>
One of the first things you'll notice if you're paying attention is that there are no block hooks in the .module file anymore. That's because blocks in Drupal 8 are plugins. We'll get to that in a few moments. The other thing you'll notice is that the hook_menu implementation doesn't have any callback information. In Drupal 8, as part of the WSCCI, those old procedural callbacks have been swapped out for an OO Symfony2 routing system. hook_menu is still there, but now it's ONLY used to define menu items, NOT to define their paths, or routes as it were. You can see that there's a new key in the array called 'routing_name'. Remember this as it'll be important in a few minutes. You can also see that the old argument placeholder, %, is now replaced by a bracket syntax: {variable_name}. This variable_name will be automatically passed as $variable_name to the callback method that we will define in our route controller class. What's a route controller class you say? Well, the way it works now is that we'll create another yaml file (called modulename.routing.yml) to map any incoming request path to a method contained within a controller class for this module.
The route controller class is stored in the file system according to the much maligned PSR-0 standard for autoloading code only when it's needed. I won't get into this too much, but it basically consists of the idea that your file system should be structured in a standard way. For Drupal 8's routing controller, this means having a directory structure starting at your modulename root directory that looks like lib/Drupal/modulename/Controller/ControllerName.php. If you think having to adhere to such a long and redundant file structure is ridiculous, you're not alone. The plan as of now is to move to the slightly less-ridiculous forthcoming proposed PSR-4 for class loading, but I digress...
Let's take a look at the test.routing.yml file:
test_page:<br> pattern: '/test/{variable}'<br> defaults:<br> _content: 'Drupal\test\Controller\TestController::testPage'<br> requirements:<br> _permission: 'access content'
Remember the routing_name => 'test_page' in the $items[] array from test.module? You can see that it is used here as the unique name for this route. We then define the pattern to match for the incoming request, and as you can see it contains the {variable} placeholder. Next, in the _content variable of the defaults section we reference the Controller class and method that will be called when this route is matched. Referencing this method in '_content' means that Drupal will wrap whatever is returned from this method with the rest of the page layout. Lastly, we set the required permission needed to access this page.
Next we need to create the route controller class that we referenced in the test.routing.yml file. So, make sure you have the directory structure as I described above (to adhere to the PSR-0 “standard”). Starting at test (your module directory) the filesystem should be structured like 'test/lib/Drupal/test/Controller'. Inside that last 'Controller' directory, you'll create your TestController.php file like this:
<?php <br /><br>namespace Drupal\test\Controller;<br><br>class TestController {<br> <br> public function testPage($variable) {<br> $build = array(<br> '#type' => 'markup',<br> '#markup' => t($variable),<br> );<br> <br> return $build;<br> }<br>}
As you can see, this is a pretty standard php class file. I've defined the testPage method (as it was referenced in the test.routing.yml file), and it returns a build array. The interesting thing to note here is that as I described earlier, the $variable that is passed into the testPage method is the argument that comes in on the url when the route is fired. So if you now go to your drupal 8 site and navigate to admin/config/development/performance, then clear all caches, then navigate to admin/modules and enable the test.module, you should then be able to navigate to http://yoursite/test/it-works, and see a page that just prints out $variable (“it-works”). Please note that this is an extremely simplified example. I'm not taking dependency injection into account at all. That would require a bit more code. We'd need the TestController class to implement the ControllerInterface interface. For now, I'll leave that to you, but I might revisit dependency injection in a future blog post.
Now that we've sorted the routing, let's look at how to create a custom block in our test module. In Drupal 8, blocks are plugins, so to create a custom block we'll need to create a Plugin and a Block directory in our file structure. Change into the test/lib/Drupal/test directory and do
mkdir -p ./Plugin/Block
Now you should have a directory structure like 'test/lib/Drupal/test/Plugin/Block'. In that Block directory, we'll create our TestBlock.php file. The file looks like this:
<?php <br /><br>/**<br> * @file<br> * Contains \Drupal\test\Plugin\Block\TestBlock<br> */<br><br>namespace Drupal\test\Plugin\Block;<br><br>use Drupal\block\BlockBase;<br>use Drupal\block\Annotation\Block;<br>use Drupal\Core\Annotation\Translation;<br><br>/**<br> * Provides a test block.<br> *<br> * @Block(<br> * id = "test_block",<br> * admin_label = @Translation("Test Block")<br> * )<br> */<br>class TestBlock extends BlockBase {<br> /**<br> * {@inheritdoc}<br> */<br> public function build() {<br> $this->configuration['label'] = t('Test block');<br> return array(<br> '#children' => t('Here is the content'),<br> );<br> }<br>}
Again, this is a pretty standard php class. TestBlock extends the BlockBase class. BlockBase has a number of methods that can be overridden here to do all sorts of cool things with your custom blocks. I'm not going to go into those right now though. We'll just stick with the build method which lets you put content into your block. The most important thing to notice here is the @Block directive in the comments before the TestBlock class definition. This is Doctrine-style annotation. This is how Drupal 8 discovers and gets metadata about your plugins. This is all new for D8, and without going into it in too much detail, the old way of getting metadata was very memory-intensive so this new way was adopted and implemented. You can find out all about it by clicking here. Also note that the use lines are required.
Once you save this file, navigate to admin/structure/block, and you should see your custom block in the list on the right-hand side of the page. Click it to “place” the block, then find the block in the list in the main part of the block page and add it to the region that you want.
Well that does it for this post. Keep in mind that this is all new to me as well, and I'm just figuring it out as I go -- mostly by reading through the changelog. If you see anything here that could be done in a more efficient way, or if I've missed anything, please don't hesitate to make suggestions in the comments.
Topics