Search icon CANCEL
Subscription
0
Cart icon
Your Cart (0 item)
Close icon
You have no products in your basket yet
Save more on your purchases! discount-offer-chevron-icon
Savings automatically calculated. No voucher code required.
Arrow left icon
Explore Products
Best Sellers
New Releases
Books
Videos
Audiobooks
Learning Hub
Newsletter Hub
Free Learning
Arrow right icon
timer SALE ENDS IN
0 Days
:
00 Hours
:
00 Minutes
:
00 Seconds
Extending Symfony2 Web Application Framework
Extending Symfony2 Web Application Framework

Extending Symfony2 Web Application Framework: Symfony2 took the great features of the original framework to new levels of extensibility. With this practical guide you'll learn how to make the most of Symfony2 through controlling your code and sharing it more widely.

eBook
£19.99 £22.99
Paperback
£28.99
Subscription
Free Trial
Renews at $19.99p/m

What do you get with Print?

Product feature icon Instant access to your digital copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Redeem a companion digital copy on all Print orders
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Table of content icon View table of contents Preview book icon Preview Book

Extending Symfony2 Web Application Framework

Chapter 1. Services and Listeners

This chapter will explain the basis of services in the Symfony2 framework. A service is an essential and core concept in Symfony2. In fact, most of the framework itself is just a big set of predefined services that are ready to use. As an example, if you just set up a new installation of Symfony2, from your project root, you can type php app/console container:debug to see the full list of services currently defined in your application. As you can see, even before we start writing anything for our application, we already have almost 200 services defined. The php app/console container:debug <service_name> command will provide information about a specific service and will be a useful command to refer to throughout the book.

Services


A service is just a specific instance of a given class. For example, whenever you access doctrine such as $this->get('doctrine'); in a controller, it implies that you are accessing a service. This service is an instance of the Doctrine EntityManager class, but you never have to create this instance yourself. The code needed to create this entity manager is actually not that simple since it requires a connection to the database, some other configurations, and so on. Without this service already being defined, you would have to create this instance in your own code. Maybe you will have to repeat this initialization in each controller, thus making your application messier and harder to maintain.

Some of the default services present in Symfony2 are as follows:

  • The annotation reader

  • Assetic—the asset management library

  • The event dispatcher

  • The form widgets and form factory

  • The Symfony2 Kernel and HttpKernel

  • Monolog—the logging library

  • The router

  • Twig—the templating engine

It is very easy to create new services because of the Symfony2 framework. If we have a controller that has started to become quite messy with long code, a good way to refactor it and make it simpler will be to move some of the code to services. We have described all these services starting with "the" and a singular noun. This is because most of the time, services will be singleton objects where a single instance is needed.

A geolocation service

In this example, we imagine an application for listing events, which we will call "meetups". The controller makes it so that we can first retrieve the current user's IP address, use it as basic information to retrieve the user's location, and only display meetups within 50 kms of distance to the user's current location. Currently, the code is all set up in the controller. As it is, the controller is not actually that long yet, it has a single method and the whole class is around 50 lines of code. However, when you start to add more code, to only list the type of meetups that are the user's favorites or the ones they attended the most. When you want to mix that information and have complex calculations as to which meetups might be the most relevant to this specific user, the code could easily grow out of control!

There are many ways to refactor this simple example. The geocoding logic can just be put in a separate method for now, and this will be a good step, but let's plan for the future and move some of the logic to the services where it belongs. Our current code is as follows:

use Geocoder\HttpAdapter\CurlHttpAdapter;
use Geocoder\Geocoder;
use Geocoder\Provider\FreeGeoIpProvider;

public function indexAction()
  {

Initialize our geocoding tools (based on the excellent geocoding library at http://geocoder-php.org/) using the following code:

    $adapter = new CurlHttpAdapter();
    $geocoder = new Geocoder();
    $geocoder->registerProviders(array(
      new FreeGeoIpProvider($adapter),
    ));

Retrieve our user's IP address using the following code:

    $ip = $this->get('request')->getClientIp();
    // Or use a default one
    if ($ip == '127.0.0.1') {
      $ip = '114.247.144.250';
    }

Get the coordinates and adapt them using the following code so that they are roughly a square of 50 kms on each side:

    $result = $geocoder->geocode($ip);
    $lat = $result->getLatitude();
    $long = $result->getLongitude();
    $lat_max = $lat + 0.25; // (Roughly 25km)
    $lat_min = $lat - 0.25;
    $long_max = $long + 0.3; // (Roughly 25km)
    $long_min = $long - 0.3;

Create a query based on all this information using the following code:

    $em = $this->getDoctrine()->getManager();
    $qb = $em->createQueryBuilder();
    $qb->select('e')
        ->from('KhepinBookBundle:Meetup, 'e')
        ->where('e.latitude < :lat_max')
        ->andWhere('e.latitude > :lat_min')
        ->andWhere('e.longitude < :long_max')
        ->andWhere('e.longitude > :long_min')
        ->setParameters([
          'lat_max' => $lat_max,
          'lat_min' => $lat_min,
          'long_max' => $long_max,
          'long_min' => $long_min
        ]);

Retrieve the results and pass them to the template using the following code:

    $meetups = $qb->getQuery()->execute();
    return ['ip' => $ip, 'result' => $result,
      'meetups' => $meetups];
  }

The first thing we want to do is get rid of the geocoding initialization. It would be great to have all of this taken care of automatically and we would just access the geocoder with: $this->get('geocoder');.

Tip

Downloading the example code

You can download the example code files for all Packt books you have purchased from your account at http://www.packtpub.com. If you purchased this book elsewhere, you can visit http://www.packtpub.com/support and register to have the files e-mailed directly to you.

You can define your services directly in the config.yml file of Symfony under the services key, as follows:

    services:
      geocoder:
        class: Geocoder\Geocoder

That is it! We defined a service that can now be accessed in any of our controllers. Our code now looks as follows:

// Create the geocoding class
$adapter = new \Geocoder\HttpAdapter\CurlHttpAdapter();
$geocoder = $this->get('geocoder');
$geocoder->registerProviders(array(
    new \Geocoder\Provider\FreeGeoIpProvider($adapter),
));

Well, I can see you rolling your eyes, thinking that it is not really helping so far. That's because initializing the geocoder is a bit more complex than just using the new \Geocoder\Geocoder() code. It needs another class to be instantiated and then passed as a parameter to a method. The good news is that we can do all of this in our service definition by modifying it as follows:

services:
    # Defines the adapter class
    geocoder_adapter:
        class: Geocoder\HttpAdapter\CurlHttpAdapter
        public: false
    # Defines the provider class
    geocoder_provider:
        class: Geocoder\Provider\FreeGeoIpProvider
        public: false
        # The provider class is passed the adapter as an argument
        arguments: [@geocoder_adapter]
    geocoder:
        class: Geocoder\Geocoder
        # We call a method on the geocoder after initialization to set up the
        # right parameters
        calls:
            - [registerProviders, [[@geocoder_provider]]]

It's a bit longer than this, but it is the code that we never have to write anywhere else ever again. A few things to notice are as follows:

  • We actually defined three services, as our geocoder requires two other classes to be instantiated.

  • We used @+service_name to pass a reference to a service as an argument to another service.

  • We can do more than just defining new Class($argument); we can also call a method on the class after it is instantiated. It is even possible to set properties directly when they are declared as public.

  • We marked the first two services as private. This means that they won't be accessible in our controllers. They can, however, be used by the Dependency Injection Container (DIC) to be injected into other services.

Our code now looks as follows:

// Retrieve current user's IP address
$ip = $this->get('request')->getClientIp();

// Or use a default one
if ($ip == '127.0.0.1') {
    $ip = '114.247.144.250';
}
// Find the user's coordinates
$result = $this->get('geocoder')->geocode($ip);
$lat = $result->getLatitude();
// ... Remaining code is unchanged

Note

Here, our controllers are extending the BaseController class, which has access to DIC since it implements the ContainerAware interface. All calls to $this->get('service_name') are proxied to the container that constructs (if needed) and returns the service.

Let's go one step further and define our own class that will directly get the user's IP address and return an array of maximum and minimum longitude and latitudes. We will create the following class:

namespace Khepin\BookBundle\Geo;

use Geocoder\Geocoder;
use Symfony\Component\HttpFoundation\Request;

class UserLocator {

    protected $geocoder;

    protected $user_ip;

    public function __construct(Geocoder $geocoder, Request $request) {
        $this->geocoder = $geocoder;
        $this->user_ip = $request->getClientIp();
        if ($this->user_ip == '127.0.0.1') {
            $this->user_ip = '114.247.144.250';
        }
    }

    public function getUserGeoBoundaries($precision = 0.3) {
        // Find the user's coordinates
        $result = $this->geocoder->geocode($this->user_ip);
        $lat = $result->getLatitude();
        $long = $result->getLongitude();
        $lat_max = $lat + 0.25; // (Roughly 25km)
        $lat_min = $lat - 0.25;
        $long_max = $long + 0.3; // (Roughly 25km)
        $long_min = $long - 0.3;
        return ['lat_max' => $lat_max, 'lat_min' => $lat_min,
           'long_max' => $long_max, 'long_min' => $long_min];
    }
}

It takes our geocoder and request variables as arguments, and then does all the heavy work we were doing in the controller at the beginning of the chapter. Just as we did before, we will define this class as a service, as follows, so that it becomes very easy to access from within the controllers:

# config.yml
services:
    #...
    user_locator:
       class: Khepin\BookBundle\Geo\UserLocator
       scope: request
       arguments: [@geocoder, @request]

Notice that we have defined the scope here. The DIC has two scopes by default: container and prototype, to which the framework also adds a third one named request. The following table shows their differences:

Scope

Differences

Container

All calls to $this->get('service_name') return the same instance of the service.

Prototype

Each call to $this->get('service_name') returns a new instance of the service.

Request

Each call to $this->get('service_name') returns the same instance of the service within a request. Symfony can have subrequests (such as including a controller in Twig).

Now, the advantage is that the service knows everything it needs by itself, but it also becomes unusable in contexts where there are no requests. If we wanted to create a command that gets all users' last-connected IP address and sends them a newsletter of the meetups around them on the weekend, this design would prevent us from using the Khepin\BookBundle\Geo\UserLocator class to do so.

Note

As we see, by default, the services are in the container scope, which means they will only be instantiated once and then reused, therefore implementing the singleton pattern. It is also important to note that the DIC does not create all the services immediately, but only on demand. If your code in a different controller never tries to access the user_locator service, then that service and all the other ones it depends on (geocoder, geocoder_provider, and geocoder_adapter) will never be created.

Also, remember that the configuration from the config.yml is cached when on a production environment, so there is also little to no overhead in defining these services.

Our controller looks a lot simpler now and is as follows:

$boundaries = $this->get('user_locator')->getUserGeoBoundaries();
// Create our database query
$em = $this->getDoctrine()->getManager();
$qb = $em->createQueryBuilder();
$qb->select('e')
    ->from('KhepinBookBundle:Meetup', 'e')
    ->where('e.latitude < :lat_max')
    ->andWhere('e.latitude > :lat_min')
    ->andWhere('e.longitude < :long_max')
    ->andWhere('e.longitude > :long_min')
    ->setParameters($boundaries);
// Retrieve interesting meetups
$meetups = $qb->getQuery()->execute();
return ['meetups' => $meetups];

The longest part here is the doctrine query, which we could easily put on the repository class to further simplify our controller.

As we just saw, defining and creating services in Symfony2 is fairly easy and inexpensive. We created our own UserLocator class, made it a service, and saw that it can depend on our other services such as @geocoder service. We are not finished with services or the DIC as they are the underlying part of almost everything related to extending Symfony2. We will keep seeing them throughout this book; therefore, it is important to have a good understanding of them before continuing.

Testing services and testing with services

One of the great advantages of putting your code in a service is that a service is just a simple PHP class. This makes it very easy to unit test. You don't actually need the controller or the DIC. All you need is to create mocks of a geocoder and request class.

In the test folder of the bundle, we can add a Geo folder where we test our UserLocator class. Since we are only testing a simple PHP class, we don't need to use WebTestCase. The standard PHPUnit_Framework_TestCase will suffice. Our class has only one method that geocodes an IP address and returns a set of coordinates based on the required precision. We can mock the geocoder to return fixed numbers and therefore avoid a network call that would slow down our tests. A simple test case looks as follows:

    class UserLocatorTest extends PHPUnit_Framework_TestCase
    {
        public function testGetBoundaries()
        {
            $geocoder = $this->getMock('Geocoder\Geocoder');
            $result = $this->getMock('Geocoder\Result\Geocoded');

            $geocoder->expects($this->any())->method('geocode')->will($this->returnValue($result));
            $result->expects($this->any())->method('getLatitude')->will($this->returnValue(3));
            $result->expects($this->any())->method('getLongitude')->will($this->returnValue(7));

            $request = $this->getMock('Symfony\Component\HttpFoundation\Request', ['getUserIp']);
            $locator = new UserLocator($geocoder, $request);

            $boundaries = $locator->getUserGeoBoundaries(0);

            $this->assertTrue($boundaries['lat_min'] == 3);
        }
    }

We can now simply verify that our class itself is working, but what about the whole controller logic?

We can write a simple integration test for this controller and test for the presence and absence of some meetups on the rendered page. However, in some cases, for performance, convenience, or because it is simply not possible, we don't want to actually call the external services while testing. In that case, it is also possible to mock the services that will be used in the controller. In your tests, you will need to do the following:

public function testIndexMock()
{
    $client = static::createClient();
    $locator = $this->getMockBuilder('Khepin\BookBundle\Geo\UserLocator')->disableOriginalConstructor()->getMock();
    $boundaries = ["lat_max" => 40.2289, "lat_min" => 39.6289, "long_max" => 116.6883, "long_min" => 116.0883];
    $locator->expects($this->any())->method('getUserGeoBoundaries')->will($this->returnValue($boundaries));
    $client->getContainer()->set('user_locator', $locator);
    $crawler = $client->request('GET', '/');
  // Verify that the page contains the meetups we expect
    
}

Here, we mock the UserLocator class so that it will always return the same coordinates. This way, we can better control what we are testing and avoid waiting for a long call to the geolocation server.

Tagging services

You have most likely already encountered tagged services when using Symfony, for example, if you have defined custom form widgets or security voters. Event listeners, which we will talk about in the second part of this chapter, are also tagged services.

In our previous examples, we created a user_locator service that relies on a geocoder service. However, there are many possible ways to locate a user. We can have their address information in their profile, which will be faster and more accurate than getting it from a user's IP address. We can use different online providers such as FreeGeoIp as we did in the previous code, or have a local geoip database. We can even have all of these in our application at the same time, and try them one after the other from most to least accurate.

Let's define the interface for this new type of geocoder as follows:

namespace Khepin\BookBundle\Geo;

interface Geocoder
{
    public function getAccuracy();

    public function geocode($ip);
}

We will then define two geocoders using the following code; the first one just wraps our existing one in a new class that implements our Geocoder interface:

namespace Khepin\BookBundle\Geo;
use Geocoder\Geocoder as IpGeocoder;

class FreeGeoIpGeocoder implements Geocoder
{
    public function __construct(IpGeocoder $geocoder)
    {
        $this->geocoder = $geocoder;
    }

    public function geocode($ip)
    {
        return $this->geocoder->geocode($ip);
    }

    public function getAccuracy()
    {
        return 100;
    }
}

The first type of geocoder is configured as follows:

freegeoip_geocoder:
    class: Khepin\BookBundle\Geo\FreeGeoIpGeocoder
    arguments: [@geocoder]

The second geocoder returns a random location every time, as follows:

namespace Khepin\BookBundle\Geo;

class RandomLocationGeocoder implements Geocoder
{
    public function geocode($ip)
    {
        return new Result();
    }

    public function getAccuracy()
    {
        return 0;
    }
}

class Result
{
    public function getLatitude()
    {
        return rand(-85, 85);
    }

    public function getLongitude()
    {
        return rand(-180, 180);
    }

    public function getCountryCode()
    {
        return 'CN';
    }
}

The second geocoder is configured as follows:

random_geocoder:
    class: Khepin\BookBundle\Geo\RandomLocationGeocoder

Now, if we change the configuration of our user_locator service to use any of these geocoders, things will work correctly. However, what we really want is that it has access to all the available geolocation methods and then picks the most accurate one, even when we add new ones without changing the user_locator service.

Let's tag our services by modifying their configuration to add a tag as follows:

freegeoip_geocoder:
    class: Khepin\BookBundle\Geo\FreeGeoIpGeocoder
    arguments: [@geocoder]
    tags:
        - { name: khepin_book.geocoder }
random_geocoder:
    class: Khepin\BookBundle\Geo\RandomLocationGeocoder
    tags:
        - { name: khepin_book.geocoder }

We cannot pass all of these in the constructor of our class directly, so we'll modify our UserLocator class to have an addGeocoder method as follows:

class UserLocator
{    protected $geocoders = [];

    protected $user_ip;

    // Removed the geocoder from here
    public function __construct(Request $request)
    {
        $this->user_ip = $request->getClientIp();
    }

    public function addGeocoder(Geocoder $geocoder)
    {
        $this->geocoders[] = $geocoder;
    }

    // Picks the most accurate geocoder
    public function getBestGeocoder(){/* ... */}

    // ...
}

Informing the DIC that we want to add tagged services cannot be done only through configuration. This is instead done through a compiler pass when the DIC is being compiled.

Compiler passes allow you to dynamically modify service definitions. They can be used for tagged services and for creating bundles that enable extra functionalities whenever another bundle is also present and configured. The compiler pass can be used as follows:

namespace Khepin\BookBundle\DependencyInjection\Compiler;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Reference;

class UserLocatorPass implements CompilerPassInterface
{
    public function process(ContainerBuilder $container)
    {
      if (!$container->hasDefinition('khepin_book.user_locator'))
        {
            return;
        }

        $service_definition = $container->getDefinition('khepin_book.user_locator');
        $tagged = $container->findTaggedServiceIds('khepin_book.geocoder');

        foreach ($tagged as $id => $attrs) {
            $service_definition->addMethodCall(
              'addGeocoder', 
              [new Reference($id)]
            );
        }
    }
}

After we have confirmed that the user_locator (renamed here as khepin_book.user_locator) service exists, we find all the services with the corresponding tag and modify the service definition for khepin_book.user_locator so that it loads these services.

Note

You can define custom attributes on a tag. So, we could have moved the accuracy of each geocoder to the configuration as follows, and then used the compiler pass to only provide the most accurate geocoder to the user locator:

tags:
    - { name: khepin_book.geocoder, accuracy: 69 }

Whenever we define the YAML configuration for services, Symfony will internally create service definitions based on that information. By adding a compiler pass, we can modify these service definitions dynamically. The service definitions are then all cached so that we don't have to compile the container again.

Listeners


Listeners are a way of implementing the observer's design pattern. In this pattern, a particular piece of code does not try to start the execution of all the code that should happen at a given time. Instead, it notifies all of its observers that it has reached a given point in execution and lets these observers to take over the control flow if they have to.

In Symfony, we use the observer's pattern through events. Any class or function can trigger an event whenever it sees the event fit. The event itself can be defined in a class. This allows the passing of more information to the code observing this event. The framework itself will trigger events at different points in the process of handling the requests. These events are as follows:

  • kernel.request: This event happens before reaching a controller. It is used internally to populate the request object.

  • kernel.controller: This event happens immediately before executing the controller. It can be used to change the controller being executed.

  • kernel.view: This event happens after executing the controller and if the controller did not return a response object. For example, this will be used to let Twig handle the rendering of a view by default.

  • kernel.response: This event happens before the response is sent out. It can be used to modify the response before it is sent out.

  • kernel.terminate: This event happens after the response has been sent out. It can be used to perform any time-consuming operations that are not necessary to generate the response.

  • kernel.exception: This event happens whenever the framework catches an exception that was not handled.

    Note

    Doctrine will also trigger events during an object's lifecycle (such as before or after persisting it to the database), but they are a whole different topic. You can learn everything about Doctrine LifeCycle Events at http://docs.doctrine-project.org/en/latest/reference/events.html.

Events are very powerful and we will use them in many places throughout this book. When you begin sharing your Symfony extensions with others, it is always a good idea to define and trigger custom events as these can be used as your own extension points.

We will build on the example provided in the Services section to see what use we could make of the listeners.

In the first part, we made our site that only shows a user the meetups that are happening around him or her. We now want to show meetups also taking into account a user's preferences (most joined meetups).

We have updated the schema to have a many-to-many relationship between users and the meetups as follows:

// Entity/User.php
/**
 * @ORM\ManyToMany(targetEntity="Meetup", mappedBy="attendees")
 */
protected $meetups;

// Entity/Meetup.php
/**
 * @ORM\ManyToMany(targetEntity="User", inversedBy="meetups")
 */
protected $attendees;

In our controller, we have a simple action to join a meetup, which is as follows:

/**
 * @Route("/meetups/{meetup_id}/join")
 * @Template()
 */
public function joinAction($meetup_id) {
    $em = $this->getDoctrine()->getManager();
    $meetup = $em->getRepository('KhepinBookBundle:Meetup')->find($meetup_id);

    $form = $this->createForm(new JoinMeetupType(), 
      $meetup, 
      ['action' => '', 'method' => 'POST']
    );
    $form->add('submit', 'submit', array('label' => 'Join'));
    $form->handleRequest($this->get('request'));

    $user = $this->get('security.context')->getToken()->getUser();

    if ($form->isValid()) {
        $meetup->addAttendee($user);
        $em->flush();
    }

    $form = $form->createView();
    return ['meetup' => $meetup, 'user' => $user, 
      'form' => $form];
}

Tip

We use a form even for such a simple action because getting all our information from the URL in order to update the database and register this user as an attendee would enable many vulnerability issues such as CSRF attacks.

Updating user preferences using custom events

We want to add some code to generate the new list of favorite meetups of our user. This will allow us to change the logic for displaying the frontpage. Now, we can not only show users all the meetups happening around them, but also data will be filtered as to how likely they are to enjoy this kind of meetup. Our users will view the frontpage often, making the cost of calculating their favorite meetups on each page load very high. Therefore, we prefer to have a pre-calculated list of their favorite meetup types. We will update this list whenever a user joins or resigns from a meetup. In the future, we can also update it based on the pages they browse, even without actually joining the meetup.

The problem now is to decide where this code should live. The easy and immediate answer could be to add it right here in our controller. But, we can see that this logic doesn't really belong here. The controller makes sure that a user can join a meetup. It should limit its own logic to just doing that.

What is possible though is to let the controller call an event, warning all observers that a user has joined a meetup and letting these observers decide what is best to do with this information.

For this event to be useful, it needs to hold information about the user and the meetup. Let's create a simple class using the following code to hold that information:

// Bundle/Event/MeetupEvent.php
namespace Khepin\BookBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use Khepin\BookBundle\Entity\User;
use Khepin\BookBundle\Entity\Meetup;

class MeetupEvent extends Event
{
    protected $user;
    protected $event;

    public function __construct(User $user, Meetup $meetup) {
        $this->user = $user;
        $this->meetup= $meetup;
    }

    public function getUser() {
        return $this->user;
    }

    public function getMeetup() {
        return $this->meetup;
    }
}

This class is very simple and is only here to hold data about an event regarding a meetup and a user. Now let's trigger that event whenever a user joins a meetup. In our controller, use the following code after validating the form:

if ($form->isValid()) {
    $meetup->addAttendee($user);
    // This is the new line
    $this->get('event_dispatcher')->dispatch('meetup.join', new MeetupEvent($user, $meetup)
    );
    $em->flush();
}

All we did was find the event_dispatcher service and dispatch the meetup.join event associated with some data. Dispatching an event is nothing more than just sending a message under a name, meetup.join in our case, potentially with some data. Before the code keeps on executing to the next line, all the classes and objects that listen to that event will be given the opportunity to run some code as well.

Tip

It is a good practice to namespace your events to avoid event name collisions. The dot (.) notation is usually preferred to separate event namespaces. So, it's very common to find events such as acme.user.authentication.success, acme.user.authentication.fail, and so on.

Another good practice is to catalog and document your events. We can see that if we keep on adding many events, since they are so easy to trigger because it's only a name, we will have a hard time keeping track of what events we have and what their purpose is. It is even more important to catalog your events if you intend to share your code with other people at some point. To do that, we create a static events class as follows:

namespace Khepin\BookBundle\Event;

final class MeetupEvents
{
    /**
     * The meetup.join event is triggered every time a user
     * registers for a meetup.
     *
     * Listeners receive an instance of:
     * Khepin\BookBundle\Event\MeetupEvent
     */
    const MEETUP_JOIN = 'meetup.join';
}

As we said, this class is much more for documentation purposes than anything else. Your code can now be changed in the controller as follows:

$container->get('event_dispatcher')->dispatch(
    MeetupEvents::MEETUP_JOIN, 
    new MeetupEvent($user, $meetup)
);

We now know how to trigger an event, but we can't say that it has helped us to achieve anything interesting so far! Let's add a little bit of logic based on that. We will first create a listener class using the following code that will be responsible for generating the user's new list of preferred meetups:

namespace Khepin\BookBundle\Event\Listener;
use Khepin\BookBundle\Event\MeetupEvent;

class JoinMeetupListener
{
    public function generatePreferences(MeetupEvent $event) {
        $user = $event->getUser();
        $meetup = $event->getMeetup();
        // Logic to generate the user's new preferences
    }
}

Our class is a plain PHP class; it doesn't need to extend anything special. Therefore, it doesn't need to have any specific name. All it needs is to have at least one method that accepts a MeetupEvent argument. If we were to execute the code now, nothing would happen as we never said that this class should listen to a specific event. This is done by making this class a service again. This means that our listener could also be passed an instance of our geolocation service that we defined in the first part of this chapter, or any other existing Symfony service. The definition of our listener as a service, however, shows us some more advanced use of services:

join_meetup_listener:
    class: Khepin\BookBundle\Event\Listener\JoinMeetupListener
    tags:
        - { name: kernel.event_listener, event: meetup.join, method: generatePreferences }

What the tags section means is that when the event_dispatcher service is first created, it will also look for other services that were given a specific tag (kernel.event_listener in this case) and remember them. This is used by other Symfony components too, such as the form framework (which we'll see in Chapter 3, Forms).

Improving user performance

We have achieved something great by using events and listeners. All the logic related to calculating a user's meetup preferences is now isolated in its own listener class. We didn't detail the implementation of that logic, but we already know from this chapter that it would be a good idea to not keep it in the controller, but as an independent service that could be called from the listener. The more you use Symfony, the more this idea will seem clear and obvious; all the code that can be moved to a service should be moved to a service. Some Symfony core developers even advocate that controllers themselves should be services. Following this practice will make your code simpler and more testable.

Code that works after the response

Now, when our site grows in complexity and usage, our calculation of users' preferred event types could take quite a while. Maybe the users can now have friends on our site, and we want a user's choice to also affect his or her friend's preferences.

There are many cases in modern web applications where very long operations are not essential in order to return a response to the user. Some of the cases are as follows:

  • After uploading a video, a user shouldn't wait until the conversion of the video to another format is finished before seeing a page that tells him or her that the upload was successful

  • A few seconds could maybe be saved if we don't resize the user's profile picture before showing that the update went through

  • In our case, the user shouldn't wait until we have propagated to all his or her friends the news of him or her joining a meetup, to see that he or she is now accepted and taking part in the meetup

There are many ways to deal with such situations and to remove unnecessary work from the process of generating a response. You can use batch processes that will recalculate all user preferences every day, but this will cause a lag in response time as the updates will be only once a day, and can be a waste of resources. You can also use a setup with a message queue and workers, where the queue notifies the workers that they should do something. This is somewhat similar to what we just did with events, but the code taking care of the calculation will now run in a different process, or maybe even on a different machine. Also, we won't wait for it to complete in order to proceed.

Symfony offers a simple way to achieve this while keeping everything inside the framework. By listening to the kernel.terminate event, we can run our listener's method after the response has been sent to the client.

We will update our code to take advantage of this. Our new listener will now behave as explained in the following table:

Event

Listener

meetup.join

Remembers the user and meetup involved for later use. No calculation happens.

kernel.terminate

Actually generates the user preferences. The heavy calculation takes place.

Our code should then look as follows:

class JoinMeetupListener
{
    protected $event;

    public function onUserJoinsMeetup(MeetupEvent $event) {
        $this->event = $event;
    }

    public function generatePreferences() {
        if ($this->event) {
            // Generate the new preferences for the user
        }
    }
}

We then need to also update the configuration to call generatePreferences on the kernel.terminate event, as follows:

join_meetup_listener:
        class: Khepin\BookBundle\Event\Listener\JoinMeetupListener
        tags:
            - { name: kernel.event_listener, event: meetup.join, method: onUserJoinsMeetup }
            - { name: kernel.event_listener, event: kernel.terminate, method: generatePreferences }

This is done very simply by only adding a tag to our existing listener. If you were thinking about creating a new service of the same class but listening on a different event, you will have two different instances of the service. So, the service that remembered the event will never be called to generate the preferences, and the service called to generate the preferences will never have an event to work with. Through this new setup, our heavy calculation code is now out of the way for sending a response to the user, and he or she can now enjoy a faster browsing experience.

Summary


This chapter introduced two of the most important concepts in Symfony, especially when it comes to extending the framework. By creating our geocoding service, we saw how easy it is to add a service that is just like any of the other Symfony services. We also reviewed how to use events to keep your code logic where it belongs and avoid cluttering your controllers with unwanted code. Then finally, we used them to make your site faster and more responsive to your users.

Believe it or not, if you really understand services and events, you know almost everything about extending Symfony. You will see throughout this book that we will constantly keep referring to both of these concepts, so, it is important that you have a good understanding of them.

In the next chapter, we will augment Symfony by adding new commands to the console tool and customize the templating engine. We will see that the services can be really helpful there as well.

Left arrow icon Right arrow icon
Estimated delivery fee Deliver to Great Britain

Standard delivery 1 - 4 business days

£4.95

Premium delivery 1 - 4 business days

£7.95
(Includes tracking information)

Product Details

Country selected
Publication date, Length, Edition, Language, ISBN-13
Publication date : Mar 25, 2014
Length: 140 pages
Edition :
Language : English
ISBN-13 : 9781783287192
Languages :
Tools :

What do you get with Print?

Product feature icon Instant access to your digital copy whilst your Print order is Shipped
Product feature icon Paperback book shipped to your preferred address
Product feature icon Redeem a companion digital copy on all Print orders
Product feature icon Access this title in our online reader with advanced features
Product feature icon DRM FREE - Read whenever, wherever and however you want
OR
Modal Close icon
Payment Processing...
tick Completed

Shipping Address

Billing Address

Shipping Methods
Estimated delivery fee Deliver to Great Britain

Standard delivery 1 - 4 business days

£4.95

Premium delivery 1 - 4 business days

£7.95
(Includes tracking information)

Product Details

Publication date : Mar 25, 2014
Length: 140 pages
Edition :
Language : English
ISBN-13 : 9781783287192
Languages :
Tools :

Packt Subscriptions

See our plans and pricing
Modal Close icon
$19.99 billed monthly
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Simple pricing, no contract
$199.99 billed annually
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just £5 each
Feature tick icon Exclusive print discounts
$279.99 billed in 18 months
Feature tick icon Unlimited access to Packt's library of 7,000+ practical books and videos
Feature tick icon Constantly refreshed with 50+ new titles a month
Feature tick icon Exclusive Early access to books as they're written
Feature tick icon Solve problems while you work with advanced search and reference features
Feature tick icon Offline reading on the mobile app
Feature tick icon Choose a DRM-free eBook or Video every month to keep
Feature tick icon PLUS own as many other DRM-free eBooks or Videos as you like for just £5 each
Feature tick icon Exclusive print discounts

Frequently bought together


Stars icon
Total £ 78.97
Symfony2 Essentials
£24.99
Extending Symfony2 Web Application Framework
£28.99
Persistence in PHP with Doctrine ORM
£24.99
Total £ 78.97 Stars icon

Table of Contents

6 Chapters
Services and Listeners Chevron down icon Chevron up icon
Commands and Templates Chevron down icon Chevron up icon
Forms Chevron down icon Chevron up icon
Security Chevron down icon Chevron up icon
Doctrine Chevron down icon Chevron up icon
Sharing Your Extensions Chevron down icon Chevron up icon

Customer reviews

Top Reviews
Rating distribution
Full star icon Full star icon Full star icon Half star icon Empty star icon 3.8
(8 Ratings)
5 star 37.5%
4 star 25%
3 star 25%
2 star 0%
1 star 12.5%
Filter icon Filter
Top Reviews

Filter reviews by




Richard R Perez Dec 07, 2015
Full star icon Full star icon Full star icon Full star icon Full star icon 5
Is a short and concise book with clear goals. There are some concepts that even I heard before, I thought they was complicate, but with this book I realized that they are very simple.You need to know about Symfony2 is not a beginner book and also you won't learn everything about Symfony2 is a book that teach you how you can create your bundles in a way that they could be reused in other applications and in they way of it, it explains functionalities that can be helpful even if that is not your intention.
Amazon Verified review Amazon
xlthlx May 29, 2014
Full star icon Full star icon Full star icon Full star icon Full star icon 5
This book is not for beginners, to understand and appreciate it you must have made at least one project with Symphony2.The book presents code examples mainly, very clear, that can be safely reused for your own projects.The ultimate goal of the book is to create and distribute your bundle, but each chapter can be considered separately, and each can help a lot to improve the way to implement functionality in Symfony2.I especially enjoyed the chapter on Commands and how to set them as an interface to Services, because, as written by the author, opening a terminal is already a technical operation for many people, and having a web interface for starting the process that the site admins could use is very powerful.The other chapters that I liked were the one on Doctrine, on how to use with MongoDB and coordinates, and the one on Twig and how to implement a widget for a map.I recommend this book to anyone who wants to deepen and improve his knowledge of Symfony2, and who wants to better understand the logic.
Amazon Verified review Amazon
Johnny B Mar 28, 2014
Full star icon Full star icon Full star icon Full star icon Full star icon 5
I have been programming in PHP semi-professionally for 14 years now. I've done a few small and unpublished Symfony2 projects trying to learn it but never have felt like I was REALLY grasping it. I've found the Yii framework to be easier to use for someone who doesn't program every day as Yii is straight forward and you can dive right in with little refresher. Symfony has made me feel like I'm becoming a dinosaur in the programming world but at the same time I need to get caught up. I'm glad to say that this book is clear and concise (much more so than the Symfony book). It's very easy to follow along and has great examples that you can expand on. Within a few minutes of starting this book I could actually feel the cobwebs in my head clear and could grasp Symfony a whole lot easier. I can see myself jumping into Symfony projects just as easily as Yii and with the extra power and resources that Symfony has. If you are a seasoned PHP developer but having a difficult time grasping Symfony, this book will help you immensely. Thanks to everyone who made this book possible.
Amazon Verified review Amazon
Greg Freeman Jun 30, 2014
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
I've been using symfony2 since its release on numerous projects and I recently read through this book.If you are a beginner or just starting out with symfony2, I suggest you spend your time reading the documentation on symfony.com and working on some test projects first. This book is not beginner friendly and assumes you have a working knowledge of symfony2, some of the explanations are skipped over quickly which is great for the intended audience of the book, as you spend more time of more advanced uses of the framework.If you are an intermediate or more advanced user of symfony2, I am sure you will learn something by reading this book and it is well worth your time. Most books are very general, this book gives some real-world usage tips that you can use in your projects right now.This book has useful information that will
Amazon Verified review Amazon
Stan Jun 12, 2014
Full star icon Full star icon Full star icon Full star icon Empty star icon 4
Extending Symnfony by Sébastien Armand is a tutorial-style introduction to a variety of the ways that you can extend a Symfony 2 full stack installation. I’m a big fan of Symfony 2 and I’ve done a fair amount of app building with it, so I was interested in Armand’s book and seeing what new things I could discover about hooking into sf2.The book is filled with code samples, far more than you’ll find in most other technical books. Most of these code samples are also complete, which anyone who has traversed the official Symfony 2 cookbooks will greatly appreciate. Unfortunately, these code samples sometimes cross pages in inconvenient ways, and none of them include syntax highlighting which can make it hard to read at times.Armand tackles six (sort of eight) areas of Symfony 2 development where developers can tap in and extend existing functionality of Symfony 2. First and foremost he kicks off his tutorials with covering service definitions and listeners. These topics seem like they could have been separate chapters to me, but nonetheless he does a good job of giving real world examples of how to tie these things in. He especially does well with event listeners - the secret weapon of the Symfony 2 stack (in my opinion anyhow).Armand’s approach to extending symfony is project-based, meaning that through the book you’re working on building an app that handles some details for meet ups between users. You can think of it like the old Symfony 1 Askeet tutorial. This is a huge advantage of Armand’s book over other Symfony 2 texts you’ll find in the wild. Actual applications create context and drive home the concepts. As an added bonus, in this book you are NOT building yet another task manager!The Security chapter covers some of the more difficult areas of Symfony 2. Anyone who has dealt with Security in sf2 knows that, while extremely powerful, it can also be extremely challenging. Armand’s examples are helpful, especially as he tackles an OAuth implementation. Armand uses the Friends of Symfony UserBundle to get going, but unfortunately didn’t cover with too much depth getting started with this super handy bundle. The examples in the book are priceless, but I look forward to future revisions that cover the new SimpleAuth implementation in Symfony 2. The only other thing I wished Armand would have covered was securing an api with tokens and a custom user provider for doing this. He shows how a cookie can be used with an event listener, but truthfully there are better ways of tackling this problem in Symfony 2 that are more consistent with its security model.One of the most valuable chapters in this book is the Doctrine chapter. Doctrine 2’s official documentation lacks a lot of context. By being a project-based tutorial, Armand actually shows you how to write a custom data type, custom DQL function, and a custom filter, rather than stumble through the Doctrine 2 docs and hope you got close. This chapter in and of itself is a valuable resource for those times when you need to do these things.The final chapter discusses bundles briefly. This is one area of the book I felt could have been fleshed out a bit more. Armand covers the basics, but part of me felt like this chapter almost belonged at the beginning of the book instead of the tail end. The other thing that was missing from this chapter was bundle inheritance which, while a tricky subject, is a huge part of extending a Symfony 2 application.All in all I think this is a solid book on tapping into some of the more powerful features of Symfony 2 and it’s counterpart Doctrine 2. The book is at times a little oddly organized, but the code samples and tip are worthy any web developers time. If you’re looking to dive into some of the things in the book’s table of contents get yourself a copy and profit from Armand’s tutorials and extensive code samples.
Amazon Verified review Amazon
Get free access to Packt library with over 7500+ books and video courses for 7 days!
Start Free Trial

FAQs

What is the digital copy I get with my Print order? Chevron down icon Chevron up icon

When you buy any Print edition of our Books, you can redeem (for free) the eBook edition of the Print Book you’ve purchased. This gives you instant access to your book when you make an order via PDF, EPUB or our online Reader experience.

What is the delivery time and cost of print book? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela
What is custom duty/charge? Chevron down icon Chevron up icon

Customs duty are charges levied on goods when they cross international borders. It is a tax that is imposed on imported goods. These duties are charged by special authorities and bodies created by local governments and are meant to protect local industries, economies, and businesses.

Do I have to pay customs charges for the print book order? Chevron down icon Chevron up icon

The orders shipped to the countries that are listed under EU27 will not bear custom charges. They are paid by Packt as part of the order.

List of EU27 countries: www.gov.uk/eu-eea:

A custom duty or localized taxes may be applicable on the shipment and would be charged by the recipient country outside of the EU27 which should be paid by the customer and these duties are not included in the shipping charges been charged on the order.

How do I know my custom duty charges? Chevron down icon Chevron up icon

The amount of duty payable varies greatly depending on the imported goods, the country of origin and several other factors like the total invoice amount or dimensions like weight, and other such criteria applicable in your country.

For example:

  • If you live in Mexico, and the declared value of your ordered items is over $ 50, for you to receive a package, you will have to pay additional import tax of 19% which will be $ 9.50 to the courier service.
  • Whereas if you live in Turkey, and the declared value of your ordered items is over € 22, for you to receive a package, you will have to pay additional import tax of 18% which will be € 3.96 to the courier service.
How can I cancel my order? Chevron down icon Chevron up icon

Cancellation Policy for Published Printed Books:

You can cancel any order within 1 hour of placing the order. Simply contact customercare@packt.com with your order details or payment transaction id. If your order has already started the shipment process, we will do our best to stop it. However, if it is already on the way to you then when you receive it, you can contact us at customercare@packt.com using the returns and refund process.

Please understand that Packt Publishing cannot provide refunds or cancel any order except for the cases described in our Return Policy (i.e. Packt Publishing agrees to replace your printed book because it arrives damaged or material defect in book), Packt Publishing will not accept returns.

What is your returns and refunds policy? Chevron down icon Chevron up icon

Return Policy:

We want you to be happy with your purchase from Packtpub.com. We will not hassle you with returning print books to us. If the print book you receive from us is incorrect, damaged, doesn't work or is unacceptably late, please contact Customer Relations Team on customercare@packt.com with the order number and issue details as explained below:

  1. If you ordered (eBook, Video or Print Book) incorrectly or accidentally, please contact Customer Relations Team on customercare@packt.com within one hour of placing the order and we will replace/refund you the item cost.
  2. Sadly, if your eBook or Video file is faulty or a fault occurs during the eBook or Video being made available to you, i.e. during download then you should contact Customer Relations Team within 14 days of purchase on customercare@packt.com who will be able to resolve this issue for you.
  3. You will have a choice of replacement or refund of the problem items.(damaged, defective or incorrect)
  4. Once Customer Care Team confirms that you will be refunded, you should receive the refund within 10 to 12 working days.
  5. If you are only requesting a refund of one book from a multiple order, then we will refund you the appropriate single item.
  6. Where the items were shipped under a free shipping offer, there will be no shipping costs to refund.

On the off chance your printed book arrives damaged, with book material defect, contact our Customer Relation Team on customercare@packt.com within 14 days of receipt of the book with appropriate evidence of damage and we will work with you to secure a replacement copy, if necessary. Please note that each printed book you order from us is individually made by Packt's professional book-printing partner which is on a print-on-demand basis.

What tax is charged? Chevron down icon Chevron up icon

Currently, no tax is charged on the purchase of any print book (subject to change based on the laws and regulations). A localized VAT fee is charged only to our European and UK customers on eBooks, Video and subscriptions that they buy. GST is charged to Indian customers for eBooks and video purchases.

What payment methods can I use? Chevron down icon Chevron up icon

You can pay with the following card types:

  1. Visa Debit
  2. Visa Credit
  3. MasterCard
  4. PayPal
What is the delivery time and cost of print books? Chevron down icon Chevron up icon

Shipping Details

USA:

'

Economy: Delivery to most addresses in the US within 10-15 business days

Premium: Trackable Delivery to most addresses in the US within 3-8 business days

UK:

Economy: Delivery to most addresses in the U.K. within 7-9 business days.
Shipments are not trackable

Premium: Trackable delivery to most addresses in the U.K. within 3-4 business days!
Add one extra business day for deliveries to Northern Ireland and Scottish Highlands and islands

EU:

Premium: Trackable delivery to most EU destinations within 4-9 business days.

Australia:

Economy: Can deliver to P. O. Boxes and private residences.
Trackable service with delivery to addresses in Australia only.
Delivery time ranges from 7-9 business days for VIC and 8-10 business days for Interstate metro
Delivery time is up to 15 business days for remote areas of WA, NT & QLD.

Premium: Delivery to addresses in Australia only
Trackable delivery to most P. O. Boxes and private residences in Australia within 4-5 days based on the distance to a destination following dispatch.

India:

Premium: Delivery to most Indian addresses within 5-6 business days

Rest of the World:

Premium: Countries in the American continent: Trackable delivery to most countries within 4-7 business days

Asia:

Premium: Delivery to most Asian addresses within 5-9 business days

Disclaimer:
All orders received before 5 PM U.K time would start printing from the next business day. So the estimated delivery times start from the next day as well. Orders received after 5 PM U.K time (in our internal systems) on a business day or anytime on the weekend will begin printing the second to next business day. For example, an order placed at 11 AM today will begin printing tomorrow, whereas an order placed at 9 PM tonight will begin printing the day after tomorrow.


Unfortunately, due to several restrictions, we are unable to ship to the following countries:

  1. Afghanistan
  2. American Samoa
  3. Belarus
  4. Brunei Darussalam
  5. Central African Republic
  6. The Democratic Republic of Congo
  7. Eritrea
  8. Guinea-bissau
  9. Iran
  10. Lebanon
  11. Libiya Arab Jamahriya
  12. Somalia
  13. Sudan
  14. Russian Federation
  15. Syrian Arab Republic
  16. Ukraine
  17. Venezuela