Module Development

Controllers for Drupal 8, 9, and 10

When Drupal receives an incoming request the HTTP Kernel identifies the appropriate route for the requested path. The routing system then matches this route with a controller that has been registered to handle responses for these types of requests. Sections of code responsible for generating the response to these requests are called Controllers.

Example tasks

  • Write the code in a custom module that responds to particular incoming requests
  • Understand how to identify the controllers responsible for responding to a given url, path and route.


Drupal's system of mapping request routes to controllers is quite well established. Since it's based on Symfony components it is unlikely to change dramatically in the foreseeable future.

Drupalize.Me resources

More information

Do you know some PHP and want to learn how to create a custom page at a custom URL in Drupal? You're in the right place.

Every web framework has the same job: provide a way for developers to map user-accessible URLs with code that builds the page. Routes, controllers, and responses are what module developers use to create pages at custom URLs in a Drupal site.

In this tutorial, we'll:

  • Define what routes, controllers, and responses are.
  • Explain the routing workflow that Drupal uses to match a URL to a route.
  • Define routing system-related terms like parameter and upcasting.

By the end of this tutorial, you should be able to explain how a developer uses routes, controllers, and responses to create custom pages in a module.

Drupal 8, 9, and 10
More information

If you want to define a new URL that a user can navigate to, and custom PHP code that will generate the content of the page, you need a route and a controller. Most of the time you'll want to do something more complex than hard code the content of the page. This will require using services in your controller. This can be accomplished in different ways.

In this tutorial we'll:

  • Provide the definition for a new route which maps a path to the callback method of a controller class.
  • Create a controller that returns a hard coded string.
  • Look at examples of using both ControllerBase and dependency injection to access services from a controller, and discuss the benefits of both approaches.

By the end of this tutorial, you should be able to define a new route that maps to a controller and displays content on the page as a result of your custom logic.

More information

The routing system can get dynamic values from the URL and pass them as arguments to the controller. This means a single route with a path of /node/{node} can be used to display any node entity. Route parameters can be validated, and upcast to complex data types via parameter conversion. If you ever want to pass arguments to the controller for a route, you'll use parameters to do so.

In this tutorial we'll:

  • Define what parameters (slugs, placeholders) are and what they are used for in a route definition.
  • Explain how URL parameters are passed to a controller.
  • Define parameter upcasting.

By the end of this tutorial, you should be able to explain how to define a route that uses parameters to pass dynamic values to the route controller, and explain how parameter upcasting works.

Drupal 8, 9, and 10
More information

Let's write some code that will allow us to see route parameters in action. We'll define a new route with a path like /journey/42/full but where 42 can be any node ID, and full can be any view mode. When a user accesses the path we'll pass the dynamic parameters from the URL to the controller. The controller will then load the corresponding node and render it using the provided view mode, and return that to display on the page.

By the end of this tutorial you should be able to:

  • Use dynamic slugs in a route to pass parameters to the route controller.
  • See how Drupal will upcast a value like the node ID, 42, to a Node object automatically.
  • Explain what happens when you visit a URL that matches a route but the parameters don't pass validation.

By the end of this tutorial, you should be able to pass dynamic values from the URL to a route's controller.

More information

When defining a route and subsequently displaying the page, often we need to calculate the page title based on route parameters or other logic. In these cases, we can't just hard code the value into the _title configuration of the route. To set a dynamic title for a page, we'll use the route's _title_callback option, and point to a PHP callback that contains the logic that computes the title of the page.

In this tutorial we'll:

  • Learn how to use the _title_callback route configuration option to dynamically set a page title
  • Explain how arguments are provided to the title callback method
  • Update the route and controller from a previous tutorial to use a dynamic title callback

By the end of this tutorial, you should be able to configure a route so that its title can be set dynamically using route parameters, instead of hard-coding the title with a static string of text.

Drupal 8, 9, and 10
More information

Every route should define its access control parameters. When you define routes in a module, you can limit who has access to those routes via different access control options. Route-level access control applies to the path. If your route defines a path like /journey/example, the access control configuration will determine whether to show the current user the page at the path defined by the route, or to have Drupal serve an "HTTP 403 Access Denied" message instead.

In this tutorial we'll look at different ways of adding access control to a route including:

  • Access based on the current user's roles and permissions
  • Access based on custom logic in a callback method
  • Logic in an access checker service

By the end of this tutorial, you should be able to add access control logic to your custom routes that will meet any requirement.

More information

Every web framework, including Drupal, has basically the same job: provide a way for developers to map URLs to the code that builds the corresponding pages. Drupal uses Symfony's HTTPKernel component. Kernel events are dispatched to coordinate the following tasks:

  • Process the incoming request
  • Figure out what to put on the page
  • Create a response
  • Deliver that response to the user's browser

Knowing a bit more about how Drupal handles the request-to-response workflow will help you better understand how to use routes and controllers to create your own custom pages or deal with authentication, access checking, and error handling in a Drupal module.

In this tutorial we'll:

  • Walk through the process that Drupal uses to convert an incoming request into HTML that a browser can read
  • See how the Symfony HTTPKernel helps orchestrate this process
  • Learn about how the output from a custom controller gets incorporated into the final page

By the end of this tutorial, you should be able to describe the process that Drupal goes through to convert an incoming request for a URL into an HTML response displayed by the browser.

More information

Sometimes we need the response returned from a controller to be something other than HTML content wrapped with the rest of a Drupal theme. Maybe we need to return plain text, or JSON structured data for an application to consume. Perhaps we need greater control over the HTTP headers sent in the response. This is possible by building on the fact that controllers can return generic Response objects instead of renderable arrays, allowing you to gain complete control over what is sent to the requesting agent.

In this tutorial we'll:

  • Look at how to return a plain text response, and JSON data
  • Show how to make your responses cacheable by adding cacheability metadata
  • Learn about how to use a generic Symfony Response to gain greater control over what gets returned

By the end of this tutorial, you should be able to return responses from a controller in a Drupal module that are not HTML content wrapped in a Drupal theme.

Controllers and Form API

More information

Each form is defined by a controller, a class that implements the \Drupal\Core\Form\FormInterface. Form controllers declare the unique ID of the form, the $form array that describes the content of the form, how to validate the form, and what to do with the data collected.

In this tutorial we'll:

  • Define a new form controller class
  • Implement the required methods to describe a form
  • Add a route that can be used to access our form

By the end of this tutorial you should be able to define a form that adheres to the FormInterface requirements and know where to find more information about how to further customize your form controller.

More information

When your module defines the form and the form controller, you can add your validation logic as part of the form controller. This is done via the implementation of a validateForm() method. The FormBuilder service will automatically invoke this method at the appropriate time during the process of submitting a form. While the validateForm() method is required by \Drupal\Core\Form\FormInterface, an empty method will fulfill that requirement. It's up to you to provide appropriate validation code inside the method.

In this tutorial we'll:

  • Use the validateForm() method of a form controller to verify user input
  • Demonstrate how to raise errors on a form element when it doesn't pass validation

By the end of this tutorial you should know how to validate your custom forms.

More information

When you create a custom form for Drupal and your module defines the form controller, the best way to handle processing of submitted data is via the submitForm() method of your controller. This method is called automatically by the Form API during the process of handling a user-submitted form. It can be used to save incoming data to the database, trigger a workflow based on user input, and instruct the Form API where to send the user after form processing has completed.

In this tutorial we'll:

  • Demonstrate how to add a submitForm() method to a form controller class
  • Access the value(s) of form input elements via the $form_state object
  • Set a redirect after performing processing in a form submission handler

By the end of this tutorial you should know how to access the values of a submitted form, and how to write custom processing code inside of the submitForm() method of a form controller.

More information

Eventually you'll want to do something with the information your form collects beyond just printing it to the screen. It's generally considered a best practice to keep business logic out of your form controller so that it can be reused. In order to accomplish that you'll generally define your business logic in a service, and then call out to that service from your form controller. Or, you can make use of one of the existing services provided by Drupal core to save data.

In this tutorial we'll:

  • Use dependency injection to inject a service into a form controller
  • Make use of a service injected into a form controller from within the buildForm() and submitForm() methods

By the end of this tutorial you'll understand how to inject one or more services into your form controller and then make use of them.

Related topics


Drupal 8, 9, and 10
More information

In Drupal, routes help map the URL of an incoming request to the code responsible for generating the content that is rendered in response.


Drupal 8, 9, and 10
More information

Symfony is a set of reusable PHP components, and a framework for building PHP applications. Drupal makes use of various Symfony components.


Not sure where to start? Our guides provide useful learning tracks for all skill levels.

Navigate guides

External resources

  • Adding a basic controller (
    • This tutorial covers creating a very basic controller.
  • Examples for Developers project's page_example module (
    • The Examples project contains many sub-modules that demonstrate various Drupal sub-systems through well-documented code. To learn more about how routing and controllers work, take a look at the page_example module here.