Symfony2 MVC - part 2: Controller

by Hrvoje Knežević -

In our first blog post in the Symfony2 MVC series we covered the concept of the Model part in Symfony2 MVC, using a simple example of a model implemented with Doctrine ORM. In this blog post, we will focus on the Controller part, where we will create a simple controller class implementing the basic create/update/delete operations on the model from the previous blog post. We will also cover the basics of mapping a specific controller action to a URL path using the Symfony2 routing mechanism.

So, what is a controller?

In short, a controller is a PHP function which processes HTTP request and returns an HTTP response. The controller can perform any kind of server-side logic you need, and return a response object. A response object can contain a JSON, HTML or XML document, an error message, a file or anything else you need. While handling an HTTP request, a controller can perform various other actions, such as reading the data from the request, setting a session variable, getting data from the database and much much more.

In Symfony2 MVC, controllers are usually called actions. By convention, actions performing similar operations or working with the same group of data are grouped together in so-called controller classes.  Controller classes should not be confused with controllers (actions), they are simply containers holding similar actions together. We will demonstrate this concept by creating a simple UserController class, containing several actions for handling various operations on User objects.

Example: UserController class

First, we will create a new PHP file inside the TestBundle/Controller folder. This file will contain the UserController class, which will contain several actions performing basic operations on User objects. The name of every controller class in Symfony2 must contain the “Controller” suffix:

/TestBundle/Controller/UserController.php:

<?php

namespace Netgen\TestBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;

class UserController extends Controller
{
}

?>

The name of each action in a controller class is suffixed with “Action” and is referenced by the actions name in the routing configuration (for example, a method named ‘listAction’ implements an action named ‘list’ ). We will get to this later, but first, let’s create an action which will return a list of all users from the database. We will call this method listAction:

public function listAction()
{
    $repository = $this->getDoctrine()
        ->getRepository('NetgenTestBundle:User');

    $userList = $repository->findAll();

    $data = array();

    foreach( $userList as $user )
    {
        if ($user instanceof \Netgen\TestBundle\Entity\User )
        {
            $data[] = array(
                'id' => $user->getId(),
                'firstName' => $user->getFirstName(),
                'lastName' => $user->getLastName()
            );
        }
    }
    return new JsonResponse( $data );
}

In the ListAction method, we fetch a list of all User objects from the database. Then we iterate through the list, get the ID, first name and last name for each user and place them in an associative array. Finally, we return an HTTP Response object, a list of associative arrays containing user data, formatted as JSON. For this we construct a JsonResponse object from a specified data array by invoking the JsonResponse class constructor with data array as an input parameter.

We can create another action, which displays data for a specific user:

 public function userAction( $userID )
{        
    $repository = $this->getDoctrine()
        ->getRepository('NetgenTestBundle:User');

    $user = $repository->find( $userID );

    if (!( $user instanceof User ) )
    {
         throw new NotFoundHttpException( "User not found" );
    }

    return new JsonResponse( 
        array( 
            'ID' => $user->getId(),
            'username' => $user->getUsername(),
            'firstName' => $user->getFirstName(),
            'lastName' => $user->getLastName(),
            'email' => $user->getEmailAddress(),
            'enabled' => $user->getIsEnabled() 
        ) 
    );
}

This action has an input parameter $userID. It checks if a user with the specified ID exists in the database. If a user doesn’t exist, we throw an exception stating that the user was not found. Otherwise, we return the data for the specified user.

We can also create another action, which will allow us to create new users:

 public function addAction()
{
    if ( $this->getRequest()->isMethod( 'POST' ) )
    {
        $user = new User();

        $userName = $this->getRequest()->request->get( 'username', false );

        if ( !$userName || strlen( $userName ) <= 0 )
        {
            throw new \Exception( "Parameter 'username' is required!" );
        }

        $user->setUsername( $userName );

        $email = $this->getRequest()->request->get( 'email', false );
        if ( !$email || strlen( $email ) <= 0 )
        {
            throw new \Exception( "Parameter 'email' is required!" );
        }

        $user->setEmailAddress( $email );

        $user-&gt;setFirstName(
            $this->getRequest() ->request->get( 'firstName', '' ) 
        );

        $user->setLastName(
            $this->getRequest()->request->get( 'lastName', '' )
        );

        $user->setPassword( "" );
        $user->setIsEnabled( true );

        /** @var \Doctrine\ORM\EntityManager $em */
        $em = $this->getDoctrine()->getManager();

        $em->persist( $user );
        $em->flush();

        return $this->redirect(
            $this->generateUrl(
                'netgen_test_user',
                array( 'userID' => $user->getId() )
            )
        );
    }

    return new Response( $this->renderView(
        'NetgenTestBundle:User:add.html.twig' ) );
}

This action behaves differently depending on the HTTP request type. For example, if we call this action using a GET request, it will render some HTML code (in this example, a twig template containing a simple HTML form).

However, if we make a POST request ( by submitting the form ), then we check the parameters passed in the request, make sure that all the required parameters are posted in the request, validate data, and if everything is OK, a new User object is created and stored in the database. If the new user was created successfully, we will display data for the new user by redirecting to the /user/{userID} route. The redirect URL is generated by calling the generateUrl() method, where we pass the route name and required parameters for the route. All actions we want to access through URL contain a route entry in the routing.yml file. To explain this, let’s describe the meaning of the term route and have a look at the routing.yml file.

Routing

To put it simple, a route is a map from a URL path to a controller. All routes in Symfony2 are defined in a configuration file called routing.yml. When you want to create a new action, you need to define a URL path which you will use to make a request to that action. In your project bundle, the routing.yml file is located inside the /Resources/config/ folder.

Here is a small portion of a routing.yml file where the routes for previously described actions are defined:

netgen_test_user_list:
    pattern: /user/list
    defaults: { _controller: NetgenTestBundle:User:list }

netgen_test_user:
    pattern: /user/{userID}
    defaults: { _controller: NetgenTestBundle:User:user }
    requirements:
        userID: \d+

netgen_test_user_add:
    pattern: /user/add
    defaults: { _controller: NetgenTestBundle:User:add }

Each route entry follows this general pattern:

route_name:
    pattern: url_path
    defaults: { _controller: bundle:controller:action }
    ...

Each route is defined by a unique identifier (route_name). A route name can be used for referencing the defined routes in code. For instance, when you want to dynamically generate links in templates or generate a redirect URL in a controller, you can reference a route by its name, instead of using full URL paths.

Next, you need to define a URL pattern matching your route (url_path). A URL path can contain parameters, which are enclosed in curly brackets. Check the netgen_test_user route in the routing.yml example portion: /user/{userID}. The path defined in this route acts like /user/*, where the parameter name is userID.

Finally, you need to tell the router which action is used when the URL path is matched. The _controller variable has a specific structure: bundle:controller:action. As we mentioned before, each controller class name needs to contain the “Controller” suffix, and each action name needs to contain the “Action” suffix. When we define the _controller variable, we reference the desired controller class and action without these suffixes. For example:

- bundle: NetgenTestBundle
- controller: UserController
- action: listAction
_controller: NetgenTestBundle:User:list

In the ‘defaults’ section, it is also possible to define default values for the parameters. We can do something like this:

netgen_test_user:
    pattern: /user/{userID}
    defaults: { _controller: NetgenTestBundle:User:user, userID: 1 }

It means that, if we omit the userID parameter in the URL, it will be set to a default value of 1, so the URL patterns ‘/user/1’ and ‘/user’ will have identical behavior.

Finally, you can specify some optional parameters for each route. For instance, you can define some extra constraints for parameters, so the parameters can contain only specific values or a specific subset of values. Or, you can restrict the route to match only specific request types. Let’s say that in the netgen_test_user route we want to specify that the userID parameter can only contain positive numeric values, and that this route is matched only for a POST request:

netgen_test_user:
    pattern: /user/{userID}
    defaults: { _controller: NetgenTestBundle:User:user, userID: 1 }
    requirements:
        userID: \d+
    methods: [POST]

Each action can contain one or more routes. For example, we can define two different routes for the same action, where one of them is used to execute the action with a POST request, and the other with a GET request.

As you can see, the Symfony2 routing mechanism allows us to quickly and easily map our actions to URL paths.

Full source code is free for use and available at: https://github.com/hknezevic/TestBundle

Comments

This site uses cookies. Some of these cookies are essential, while others help us improve your experience by providing insights into how the site is being used.

For more detailed information on the cookies we use, please check our Privacy Policy.

  • Necessary cookies enable core functionality. The website cannot function properly without these cookies, and can only be disabled by changing your browser preferences.