Doctrine - Extending Symfony2 Web Application Framework (2014)

Extending Symfony2 Web Application Framework (2014)

Chapter 5. Doctrine

Doctrine is the Object-relational Mapper (ORM) that ships with Symfony. It lets you work with PHP classes and objects, and handles their storage and retrieval to and from a data store. It can work with a variety of data stores such as traditional relational databases or document databases. The examples in this chapter will be either for the ORM or for the MongoDB ODM (Object-document Mapper).

Creating your own data types

Not all databases are created equal! MongoDB can store a collection of values or documents within a document, which is impossible in most relational databases. PostgreSQL can deal with geographical values, but MySQL can't.

For this reason, Doctrine only ships with a subset of standard supported types that are common across most of the databases. But, what if you want to use features specific to your database vendor or invent your own form of mapping type? You can define these types in exactly the same way Doctrine does.

User and meetup locations

We have already created a class named Coordinates to hold the latitude and longitude of a meetup. We have also created a query in our first controller to get a user only the events within a 50 km side square centered on them. There are a few problems with this; firstly, we can only use a square (or force the DB to do some calculation on each row), and secondly, there's no index on these queries, so it might slow down after some time.

MongoDB has support for geospatial indexes, but it requires the locations to be stored as [latitude, longitude]. If we had used MongoDB instead of a relational database in the first place, our meetup class would look as follows:

/**

* @ODM\Document

*/

class Meetup

{

/**

* @ODM\Id

*/

protected $id;

/**

* @ODM\String

*/

protected $name;

/**

* @ODM\???

*/

protected $location;

// Getters and Setters ...

}

The annotation for location is ??? as we don't know how to store this yet! So, we'll create our own Doctrine mapping type to be applied here. Let's say, we add a custom type named coordinates, and then our annotation will become as follows:

/**

* @ODM\Field(type="coordinates")

*/

For Doctrine to become aware of our custom type, we need to do the following two things:

· Create the Type class

· Tell Doctrine about it

The Type class is very simple to understand, but there's a catch since some of its behavior is not yet implemented in Doctrine's ODM! It has the following four possible methods:

· convertToPHPValue

· convertToDatabaseValue

· closureToPHP

· closureToDatabase

The names are immediately easy to understand. The two closureTo* methods actually return a string containing PHP code that will be used during Doctrine's code generation. Here's the catch: convertToPHPValue doesn't work. It is simply never called, so you must use the closureToPHP method instead, as follows:

namespace Khepin\BookBundle\Document;

use Doctrine\ODM\MongoDB\Types\Type;

use Doctrine\ODM\MongoDB\Types\DateType;

use Khepin\BookBundle\Geo\Coordinate;

use Symfony\Component\Validator\Exception\UnexpectedTypeException;

class CoordinatesType extends Type

{

public function convertToPHPValue($value)

{

return new Coordinate($value[0], $value[1]);

}

public function convertToDatabaseValue($value)

{

if (!$value instanceof Coordinate) {

throw new UnexpectedTypeException($value, 'Khepin\BookBundle\Geo\Coordinate');

}

return [$value->getLatitude(), $value->getLongitude()];

}

public function closureToPHP()

{

return '$return = new \Khepin\BookBundle\Geo\Coordinate($value[0], $value[1]);';

}

}

Also, be careful, because your closure's code will actually be written as code in a completely different context than the one of this class; therefore, it is important to use fully qualified namespaces.

In Doctrine's base type class, we find a list of all available types as a static array, as follows:

private static $typesMap = array(

self::STRING => 'Doctrine\ODM\MongoDB\Types\StringType',

self::DATE => 'Doctrine\ODM\MongoDB\Types\DateType',

// ...

);

This is where our type must be declared for Doctrine to know about it. It is registered as follows:

use Doctrine\ODM\MongoDB\Types\Type;

Type::addType('coordinates', 'Khepin\BookBundle\Document\CoordinatesType');

The Mongo ODM bundle doesn't offer a way (similar to forms) of tagging your types and letting Doctrine register them on its own. As the preceding two lines of code are only here to declare how to load a special type, we'll add them to app/autoload.php.

Testing

Let's test whether our mapping is working properly using the following code:

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

use Khepin\BookBundle\Document\Meetup;

use Khepin\BookBundle\Geo\Coordinate;

class MongoCoordinateTypeTest extends WebTestCase

{

public function testMapping()

{

$client = static::createClient();

$dm = $client->getContainer()->get('doctrine.odm');

Create a new meetup with a unique name and persist it, as follows:

$meetup = new Meetup();

$name = uniqid();

$meetup->setName($name);

$meetup->setLocation(new Coordinate(33, 75));

$dm->persist($meetup);

$dm->flush();

We will retrieve our meetup through PHP's native Mongo extension, using the following code, to verify that the value was indeed stored as an array:

$m = new \MongoClient();

$db = $m->extending;

$collection = $db->Meetup;

$met = $collection->findOne(['name' => $name]);

$this->assertTrue(is_array($met['location']));

$this->assertTrue($met['location'][0] === 33);

We set a new value without Doctrine, directly by setting an array in the database as follows:

$newName = uniqid();

$collection->insert([

'name' => $newName,

'location' => [11, 22]

]);

Now, retrieve our meetup through Doctrine and verify that we get a coordinate, as follows:

$dbmeetup = $dm->getRepository('KhepinBookBundle:Meetup')->findOneBy(['name' => $newName]);

$this->assertTrue($dbmeetup->getLocation() instanceof Coordinate);

}

Finally, test that the correct exception is thrown if we pass something that is not a coordinate, using the following code:

/**

* @expectedException \Symfony\Component\Validator\Exception\UnexpectedTypeException

*/

public function testTypeException()

{

$client = static::createClient();

$dm = $client->getContainer()->get('doctrine.odm');

$name = uniqid();

$meetup = new Meetup();

$meetup->setName($name);

$meetup->setLocation([1,2]);

$dm->persist($meetup);

$dm->flush();

}

Custom DQL functions

Doctrine can be adapted to many different database vendors such as MySQL, PostgreSQL, and others. To achieve this and still be able to take advantage of the specifics of each underlying platform, Doctrine is designed in such a way that it is easy to define your own custom SQL functions.

We will take advantage of this for our geolocation. In the first chapter, we decided that the home page would only display events within 25 kilometers (which roughly translates to 0.3 in terms of latitude and longitude). To do so, we defined a box of coordinates around a given point and then used it in the SQL code.

However, an actual distance between two points (in a Cartesian plan) is calculated by the following formula:

Custom DQL functions

The preceding formula can be translated to the following SQL query: SQRT (POW(lat_1 - lat_2, 2) + POW(long_1 - long_2, 2) ).

This is correct; however, it is a bit tedious to write, so we'll take advantage of Doctrine's ability to define your own SQL functions and define a DISTANCE function that will be used as DISTANCE( (lat_1, long_1), (lat_2, long_2) ).

Let's go ahead and register it immediately in our config.yml file as follows:

orm:

# ...

dql:

numeric_functions:

distance: Khepin\BookBundle\Doctrine\DistanceFunction

The name we chose here, distance, is important. Doctrine will register it as an identifier so that whenever it encounters the word DISTANCE in our DQL, it will call our DistanceFunction to take over.

We will also update our controller code so that it uses this new DQL function as follows:

/**

* @Route("/")

* @Template()

*/

public function indexAction()

{

$position = $this->get('user_locator')->getUserCoordinates();

$position = [

'latitude' => $position->getLatitude(),

'longitude' => $position->getLongitude()

];

// Create our database query

$em = $this->getDoctrine()->getManager();

$qb = $em->createQueryBuilder();

$qb->select('e')

->from('KhepinBookBundle:Event', 'e')

->where('DISTANCE((e.latitude, e.longitude), (:latitude, :longitude)) < 0.3')

->setParameters($position)

;

// Retrieve interesting events

$events = $qb->getQuery()->execute();

return compact('events');

}

We can now define our new SQL function as follows:

namespace Khepin\BookBundle\Doctrine;

use Doctrine\ORM\Query\AST\Functions\FunctionNode;

use Doctrine\ORM\Query\SqlWalker;

use Doctrine\ORM\Query\Parser;

use Doctrine\ORM\Query\Lexer;

class DistanceFunction extends FunctionNode

{

protected $from = [];

protected $to = [];

public function parse(Parser $parser)

{

// ...

}

public function getSql(SqlWalker $sqlWalker)

{

// ...

}

}

We have already stated that Doctrine should hand over the parsing to us whenever it encounters the DISTANCE token. Our function then needs to do the following two things:

· Parse the following DQL by consuming the DQL string until the final parenthesis of our DISTANCE function

· Generate some SQL, which will be the Cartesian distance calculation: SQRT(...)

Parsing the DQL is done by using the parser (which consumes the string), and the lexer, which knows how to read it.

When the parser consumes a part of the string, that part of the string is no longer available to be parsed by default. The parser advances its position in the string until the end, so we always need to be careful until which point we should parse the DQL.

The lexer knows about special DQL tokens such as parenthesis, commas, DQL function identifiers, and much more. By using these two, we tell the parser about our distance function in a way that can be described as follows:

Start with the **DISTANCE** identifier.

Find a **(**

Find another **(**

Find some expression (this could be a value, or a full SQL select statement)

Find a **,**

Find some expression

Find a **)**

Find a **,**

Find a **(**

Find some expression

Find a **,**

Find some expression

Find a **)**

Find a **)**

Our parse function actually looks very similar to the following few lines of code:

public function parse(Parser $parser)

{

// Match: DISTANCE( (lat, long), (lat, long))

$parser->match(Lexer::T_IDENTIFIER);

$parser->match(Lexer::T_OPEN_PARENTHESIS);

// First (lat, long)

$parser->match(Lexer::T_OPEN_PARENTHESIS);

$this->from['latitude'] = $parser

->ArithmeticPrimary();

$parser->match(Lexer::T_COMMA);

$this->from['longitude'] = $parser

->ArithmeticPrimary();

$parser->match(Lexer::T_CLOSE_PARENTHESIS);

$parser->match(Lexer::T_COMMA);

// Second (lat, long)

$parser->match(Lexer::T_OPEN_PARENTHESIS);

$this->to['latitude'] = $parser

->ArithmeticPrimary();

$parser->match(Lexer::T_COMMA);

$this->to['longitude'] = $parser

->ArithmeticPrimary();

$parser->match(Lexer::T_CLOSE_PARENTHESIS);

$parser->match(Lexer::T_CLOSE_PARENTHESIS);

}

We will save the matched expressions as a from and to variable so that we can use them to generate the SQL later.

Note

It is possible to check what the next token will be without consuming it to provide different possible syntax of your DQL function through on of:

· $parser->getLexer()->peek();

· $parser->getLexer()->glimpse();

· $parser->getLexer()->isNextToken(<token_type>);

So, we can, for example, use both DISTANCE( (lat, long), (lat, long) ) and DISTANCE ( e, (lat, long) ) functions if we know that the selected element e has a latitude and longitude property.

If the parser does not find what it is supposed to, it will throw a syntax error. The code to generate the SQL statement then looks as follows:

public function getSql(SqlWalker $sqlWalker)

{

$db = $sqlWalker->getConnection()->getDatabasePlatform();

$sql = 'POW(%s - %s, 2) + POW(%s - %s, 2)';

$sql = sprintf(

$sql,

$this->from['latitude']->dispatch($sqlWalker),

$this->to['latitude']->dispatch($sqlWalker),

$this->from['longitude']->dispatch($sqlWalker),

$this->to['longitude']->dispatch($sqlWalker)

);

$sql = $db->getSqrtExpression($sql);

return $sql;

}

Note

We could have written our SQL as SQRT ( POW(%s - %s, 2) + POW(%s - %s, 2) ) as the SQRT function is the same across all the major SQL database vendors. However, it is safer to rely on Doctrines Database Abstraction Layer to take care of these differences for us. As the POW function is not being included as an abstracted function, we can directly output its SQL statement.

What we stored in our from and to variables were not the results of SQL statements but the pieces of yet unparsed DQL. Since these could be anything from a literal value to a full SELECT statement, we can use the SQL Walker to keep generating the correct SQL for these expressions.

All Doctrine functions that you are currently using are also built this way, so you can find a lot of examples on how to write these functions within the Doctrine source code itself.

Versioning

A common issue when a lot of users have access to modifying the same resources is to make sure that they are not overwriting each other's changes. One technique to prevent this from happening is to version the resources. In Doctrine, we can set a version number to any entity when we first persist it, and then increment it whenever there is a request to change the information.

This will allow us to check if the version number of the incoming request is at least equal to the current one in the database. If not, refuse the change and force the user to refresh before updating the content.

Doctrine also uses events that we can listen to. These are as follows:

· prePersist: This event is triggered before the entity is persisted to the database for the first time.

· preRemove: This event occurs before deleting an object.

· preUpdate: This event occurs before a new version of the entity is saved to the database.

· post*: All the preceding events also have a post version that occurs after the action has been completed.

· postLoad: This event is triggered after loading data from the database.

· pre / on / postFlush: These events are not tied to a single entity, but occur when the entity manager is performing actions on the database.

· onClear: This event occurs when the entity manager has no more work to do on the entities.

· loadClassMetadata: This event is triggered when Doctrine has loaded metadata such as the mapping information about a class. This can be useful if you need to create a service that knows about different entity relations in your application.

Using these events, it is possible to add behavior to your entities and share this behavior among them. Some famous use cases include creating a soft delete behavior where a delete flag is set to true instead of actually removing the information from the database, dynamically creating a URL-friendly version of an article's title, saving the time of creation and last update, and so on.

To make it easy to share our Versionable behavior, we'll add the required fields and methods in a Trait method, as follows:

namespace Khepin\BookBundle\Doctrine;

use Doctrine\ORM\Mapping as ORM;

Trait Versionable

{

/**

* @ORM\Column(name="version", type="integer", length=255)

* @ORM\Version

*/

private $version;

public function getVersion()

{

return $this->version;

}

public function setVersion($version)

{

$this->version = $version;

}

}

This way, all we have to do for an entity, such as our meetups, to become versionable is to add the trait as follows:

class Event

{

use Versionable;

// ...

}

Note

The @ORM\Version annotation indicates to Doctrine that this field is to be used to compare versions. Doctrine doesn't provide a versionable trait but gives you the tools to create your own so that your version property can be an integer, a timestamp, a hashed value of the entity, and so on.

We identified two important steps in our process; first, we set a version number of 1 whenever the entity is created, and secondly, we used it to verify the validity of an operation and incremented it.

Setting a version on all entities

Since we are going to use listeners and events, we will again define a service as follows:

khepin.doctrine.versionable:

class: Khepin\BookBundle\Doctrine\VersionableListener

tags:

- { name: doctrine.event_listener, event: prePersist }

- { name: doctrine.event_listener, event: preUpdate }

We have already set our service to listen to both the prePersist and preUpdate methods. In this case, we don't have to define a method to be called on the listener whenever the event is triggered. Doctrine will just call the prePersist method or the preUpdate method of the class.

Our listener is quite simple this time, so the service doesn't rely on any other service, but for each entity, if you wanted to add the name of the last person who updated it, then your service could depend on the security context in order to get the current connected user.

Note

Although it is tempting to define Doctrine extensions that integrate with Symfony services in such a way, especially for adding the user, you should use this with caution and make sure that your code is flexible enough. Whenever you want to manipulate your objects from the command line, your listener might be called, but the user session or the security context would not exist, and this will prevent you from performing useful database operations from a command line.

In order to just set the version on any entity, it's quite easy. We only do it through the listener to show how it is working; otherwise, simply setting a default value of 1, as follows, would have been perfectly fine:

<?php

namespace Khepin\BookBundle\Doctrine;

use Doctrine\ORM\Event\LifecycleEventArgs;

class VersionableListener {

public function prePersist(LifecycleEventArgs $args)

{

$entity = $args->getEntity();

$versionable = in_array(

'Khepin\BookBundle\Doctrine\Versionable',

(new \ReflectionClass($entity))->getTraitNames()

);

if ($versionable) {

$entity->setVersion(1);

}

}

}

The listener will be called for absolutely all entities before it is persisted, no matter if we added the Versionable trait or not. So, the first thing to do is check that we are dealing with an object that we actually want to version. We do this by verifying that our class usesthe Versionable trait.

If we need to update the version, then we set it to 1.

Using and updating versions

Now, when we are about to save an updated object, we must check whether it is versionable, check whether the current version is compared to the database value, decide to allow the update or not, and increment the version number, by using the following code:

public function preUpdate(LifecycleEventArgs $args)

{

$entity = $args->getEntity();

$em = $args->getEntityManager();

$versionable = in_array(

'Khepin\BookBundle\Doctrine\Versionable',

(new \ReflectionClass($entity))->getTraitNames()

);

if ($versionable) {

$em->lock(

$entity,

LockMode::OPTIMISTIC,

$entity->getVersion()

);

$version = $entity->getVersion();

$uow = $em->getUnitOfWork();

$uow->propertyChanged(

$entity,

'version',

$version,

$version + 1

);

}

}

Since we added the @ORM\Version annotation to our Trait earlier, we can take advantage of Doctrine's entity locking. The OPTIMISTIC lock is one of the defaults that come with Doctrine. When we try to lock the entity, if the version number present in the database is not the same as the one present in the entity (someone else modified it in the meantime), Doctrine will throw an exception and the entity cannot be updated.

Notice that we have to then explicitly tell the unit of work that the version was updated. A unit of work is a small set of all the changes that the entity manager has to perform when $em->flush() is called. It already contains the newly computed values ready to be saved to the database. Here, since it has already been computed, we need to explicitly let it know that there is a new value.

Testing

Testing anything directly related to Doctrine like this is usually easier and is better done by directly interacting with the database. Therefore, your tests will modify the data included in the database. This might not be what you want if you cannot set up a clean test environment. One way to do it is to redefine the Doctrine connection for the test environment and use sqlite. This can be done in the config_test.yml file, as follows:

doctrine:

dbal:

driver: pdo_sqlite

host: localhost

port: null

dbname: test_db

user: root

password: null

charset: UTF8

path: %kernel.root_dir%/…/ BookBundle/Tests/db.sqlite

As long as you are running tests, you will be in the test environment, and any call to $container->get('doctrine') will return a connection to the test database. If you wish to execute any command in that environment (to first create the DB and schema, for example), just execute your normal command and add --env test.

Other than that, our tests are pretty simple and straightforward:

class VersionableTest extends WebTestCase

{

public function testVersionAdded()

{

$client = static::createClient();

$meetup = new Event();

$em = $client->getContainer()->get('doctrine')->getManager();

$this->assertTrue($meetup->getVersion() === null);

$em->persist($meetup);

$em->flush();

$em->refresh($meetup);

$this->assertTrue($meetup->getVersion() === 1);

}

/**

* @expectedException \Exception

*/

public function testRefuseOutdated()

{

$client = static::createClient();

$meetup = new Event();

$em = $client->getContainer()->get('doctrine')

->getManager();

$em->persist($meetup);

$em->flush();

$meetup->setName('myEvent');

$meetup->setVersion(0);

$em->flush();

}

public function testIncrementedVersion()

{

$client = static::createClient();

$meetup = new Event();

$em = $client->getContainer()->get('doctrine')

->getManager();

$em->persist($meetup);

$em->flush();

$this->assertTrue($meetup->getVersion() === 1);

$em->refresh($meetup);

$meetup->setName('test event');

$em->flush();

$this->assertTrue($meetup->getVersion() == 2);

}

}

Creating a Doctrine filter

With the two types of extensions we already saw, a lot can be done. We could create an extension that notifies us whenever an entity has been updated, by whom, or create URL-friendly names for entities. We know how to deal with entity versions; we could even extend that behavior to save all the previous versions of an entity and maintain a record history. Some behaviors, though, can still not be achieved with what we have seen.

If we want, we can create a soft delete, or ensure automatically that all database queries include user_id so that a user can see only data that belongs to them. In the latter case, we will be able to easily add a value to a user_id field on any entity before it is persisted, but while retrieving entities through a SQL query, we still need to remember to add the user_id = "123" value every time we write a query. This is likely to be forgotten, and that can cause some big issues, because your app will start to leak data from one user to another.

A better version will be that all queries have this bit of logic added automatically. In Doctrine, before version 2.2, you would have had to create a custom AST Walker. The AST (Abstract Syntax Tree) Walker is the class that generates the actual SQL statement based on the query you have defined in the query builder. The query builder receives chunks of SQL statements when you use the following code:

$qb->select('u')

->from('User', 'u')

->where('u.id = ?1')

->orderBy('u.name', 'ASC');

This was a bit complex and inconvenient.

Doctrine 2.2 and higher versions came with the concept of filters that allow you to do exactly this. Filters also come with the advantage that they can easily be enabled and disabled, so whenever you are writing commands for doing administrative work on your database, you can completely bypass the filter and perform your normal work.

We will first configure and add a very simple filter class by adding a filters entry to the ORM configuration, as follows:

orm:

auto_generate_proxy_classes: %kernel.debug%

auto_mapping: true

filters:

- { name: owner_filter, class: Khepin\BookBundle\Doctrine\OwnerFilter, enabled: true }

Sadly, it is not possible to use a service as a filter. Doctrine registers filters by class name. So, we won't be able to inject the current user inside of the filter. We also need to remember that the filter will be applied to all queries, even the one that retrieves our user from the database. So, we need a way to differentiate between entities for which this filter should be applied and those for which it is not needed.

We will define a simple PHP interface (an empty one) that will allow us to make the distinction between entities to which the filter should be applied and others. As with security, the secure option should always be the default one. It is better to pull your hair for an hour because you don't understand where this user_id = 123 constraint is coming from in your SQL statement, rather than having the user data exposed wrongly because you forgot to add a UserOwnedEntity interface to a specific entity.

To be on the safe side, we use the opposite of the UserOwnedEntity interface, as follows:

// Interface

namespace Khepin\BookBundle\Doctrine;

interface NonUserOwnedEntity

{

}

// Filter

namespace Khepin\BookBundle\Doctrine;

use Doctrine\ORM\Mapping\ClassMetaData,

Doctrine\ORM\Query\Filter\SQLFilter;

class OwnerFilter extends SQLFilter

{

public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)

{

if ($targetEntity->reflClass->implementsInterface('Khepin\BookBundle\Doctrine\NonUserOwnedEntity')) {

return "";

}

return $targetTableAlias.'.user_id = ' . $this->getParameter('user_id');

}

}

We also need to remember that before any user is logged in, it is impossible to have the proper parameter value.

Doctrine allows us to retrieve the filters later, so we can still use an event listener that will be triggered on each request once the user information is available, and pass the correct parameter at that point. We will also disable our filter before this happens since the database will be queried at least once to get the current user information. This will prevent some future mistakes.

Our OwnerFilter class doesn't need to change except in the configuration where we will now set it to enable: false by default. We'll need to create an event listener that:

· Knows the user (inject @security.context)

· Knows about Doctrine (inject @doctrine)

· Is triggered very early on each request (listen to kernel.request)

kernel.request is the first event that gets triggered for any request, and it is called on every request. If the user previously logged in, the user information is already present when the kernel.request event is triggered. The following code shows the use of thekernel.request event:

khepin.doctrine.owned_entity.listener:

class: Khepin\BookBundle\Doctrine\OwnerListener

arguments: [@doctrine, @security.context]

tags:

- { name: kernel.event_listener, event: kernel.request , method: updateFilter }

The event listener class itself isn't very complex, as shown in the following code:

namespace Khepin\BookBundle\Doctrine;

class OwnerListener

{

private $em;

private $security_context;

public function __construct($doctrine, $security_context)

{

$this->em = $doctrine->getManager();

$this->security_context = $security_context;

}

public function updateFilter()

{

$id = $this->security_context->getToken()->getUser()->getUserId();

$this->em->getFilters()->enable('owner_filter')->setParameter('user_id', $id);

}

}

Summary

This chapter covered a great deal of what you could want to do in using and extending Doctrine. Combining events and filters, you can create very solid extensions. Do you want to create a new CMS where articles can only be seen after they are "published"? Events and filters will come along nicely to provide a publishable behavior to your entities. Do you need to keep versions of all changes and know who made what change and when? Here again, the events will allow you to have this taken care of on all entities without worrying about manually doing it.

As an exercise, you can try to implement a soft delete behavior. Soft delete indicates that whenever an entity is about to be deleted, you instead update a deleted field to true, or to the timestamp at which it was deleted. Creating a SoftDeleteable behavior for your entities should involve both listening to events and using a filter.

I mentioned earlier about Doctrine's Abstract Syntax Tree and how it used to be necessary before Doctrine added the concept of filters. There are still cases where you might want to use these, for example to augment the DQL syntax or to tailor it to your specific database vendor.

With all this, you are now fully equipped to create any type of extension in Symfony. But, the whole point of creating an extension rather than just coding something that works in one place is to be able to reuse and share it. In the last chapter, we will take a look at the possibilities to do so in Symfony.