By the end of this lesson, you’ll have restructured the project created in the core concepts tutorial in to a series of services which make your controller code shorter and site functionality more flexible. Along the way, we’ll be:
- Learning what a Symfony service is
- Creating a manager for handling the blog
- Creating a Doctrine repository service
Follow along using the code available from this GIT repository.
Start - a_better_form
End - micro_services
A service, at the basic level is nothing more than a PHP class with some configuration parameters which hook it in to Symfony pipeline.
Their primary usage allows you to abstract code away from the controller (or whatever else is handling the request) in to a series of smaller files. By building your project with services, you will steer your project towards SOLID design principles and improve maintainability of your app.
In front of any service sits the Service Container and this is where (aside from better design) power and flexiblity is introduced. With the service container, some of the immediate benefits for any project include:
- Not needing to manually instantiate classes. It is done for you when the framework bootstraps, although they can be lazily loaded
- Different services and settings can be used depending on the environment with only a few configuration changes
- Easily define constructor arguments and dependency injection whilst making use of other configuration options. A simple example: passing secure credentials to the Database handler
- The ability to hook extra functionality in to existing services with minimal effort through service tags
You're already using services
Before proceeding to create our first service, it is important to note that there is no single type of service and you aren't restricted at all.
Here are some examples you might already be aware of in order to give some context and help you begin to think about how they can be used:
- Admin – When creating the administrator area for blog handling in an earlier lesson, the class you created was a service that gets hooked in to Sonata Admin using tags.
- Repositories – These allow you to define data queries relating to a specific entity type in to a class of their own which allow you to build a library of useful queries.
- Event Listeners – When Symfony is running, there are many milestones which, when reached, allow extra code to be inserted in to the pipeline without editing the original code.
- Forms – When forms have extra dependencies, defining a form and it’s logic as a service makes not only the controller much cleaner but also reusing a form in other places possible
- Twig Extensions – It is possible to add new functionality to the templating engine in the form of functions and filters.
Creating a service
We’ll start with the simple part, defining the service. Create the following file
- namespace AppBundle\Service;
- class BlogManager
- public function test()
- return 'This comes from your Blog service';
This is just a simple class with a method named test; we'll use it to illustrate how we would connect it to the service container. This is done through configuration making use of the (you guessed it) services key.
- class: AppBundle\Service\BlogManager
Notice that, the service name has been prefixed by app and service. This is not necessary but, in the name of creating a maintainable project, I strongly recommend adopting a naming system otherwise as the project grows and you make use of multiple bundles, it will become difficult to know where each service is located. Personally, I use a scheme which is similar to the file layout.
With that in mind, if you’ve been following the tutorial so far you should also have a sonata admin definition, add the app prefix to it also.
Let’s test that everything is working properly by creating a simple Test controller which makes use of the service. In the following code, the service can be retrieved from within a controller so easily because a Symfony controller is “container aware” and for convenience has a shortcut method “get” to retrieve services.
- // src/AppBundle/Controller/TestController.php
- namespace AppBundle\Controller;
- use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
- use Symfony\Bundle\FrameworkBundle\Controller\Controller;
- class TestController extends Controller
- * @Route("/test", name="test")
- public function indexAction()
- /* Use the service container to retrieve the blog manager */
- $blogManager = $this->get('app.service.blog_manager');
- /* Return the output of test function as a response */
- return new Response($blogManager->test());
Beware that if you are trying to access the service container from somewhere other than a controller (such as unit tests or command line classes), this shortcut method will not work. Each of the mentioned situations do have simple ways of becoming container aware but, know that the container can be retrieved so long as you have access to the kernel using the following:
Creating a repository service
In the last section, we created a service which returned a simple string when the test function was called. Whilst this method of design may be a useful strategy for simple utilities, it is isolated and doesn’t provide any way to retrieve posts from the database.
We could inject Doctrine in to the service and then perform database operations but, this is bad design as it would tie the service to Doctrine making it difficult to swap out in the future. Instead, we will create a repository service for the post entity.
Repository services are smaller utility classes which extend a part of Doctrine allowing us to define custom methods, they allow for much simpler swapping in and out. Let’s define the repository class.
- namespace AppBundle\Repository;
- /* The base Doctrine repository class */
- use Doctrine\ORM\EntityRepository;
- class PostRepository extends EntityRepository
- public function findAll($sortOrder = 'DESC')
- return $this->createQueryBuilder('p')
- ->orderBy('p.createdAt', $sortOrder)
In this tutorial, we will be locking down access to the post repository by making it private; this means that it cannot be directly retrieved from the container (unless using an alias but, that is a subject for a different day).
You might not need to go to such an extreme and Doctrine has a facility to quickly retrieve repositories through the entity manager. In fact, it automatically creates repositories for you but, these have a limited set of methods and we want to add some custom methods. Adding these custom repository methods to Doctrine is defined within the entity file by chaning the initial ORM annotation to include the following:
- * @ORMEntity(repositoryClass="AppBundleRepositoryPostRepository")
Now, when getting the repository, you will also have access to the findAllOrderedByDate when running the following
- $em = $this->getDoctrine()->getManager();
- $posts = $em->getRepository("AppBundle:Post")
Just like before, we have the class prepared but the service container is unaware of its’ existence so, we want to define it in the services section of the configuration
- class: AppBundle\Repository\PostRepository
- factory: ['@doctrine.orm.entity_manager', getRepository]
- arguments: [ AppBundle:Post ]
- public: false
Notice in the above, that aside from the class definition, there are now three extra parts to the configuration. The first two are because Doctrine repositories are created using the factory design pattern.
- Factory – When creating the object, instead of instantiating a new class, the object is the result of calling a method (second parameter) on another service (first parameter).
- Arguments – When calling either the constructor function or (in this case) a factory method, the arguments key is an array of parameters to pass. These parameters can be flat strings (as it is in this case), substituted variables or even other services.
- Public – In the case where a service is created for the sole purpose of being injected in to others, services can be declared private which tells the service container it will never be requested from outside. It is also considered good practise to mark as many services as possible as private due to the performance optimizations. Without the declaration, services will be considered public
Making the service useful
Now you’ve got a post repository, we’re going to inject it in to the blog manager. This is only slightly different from passing arguments to a manually called class because, that’s exactly what we are doing. In order to reference a service with a config file, we prefix it with @. So far, when passing arguments we have used the simple array format ( brackets).
This is perfectly fine to repeat but, in a case where many arguments are passed, the line could get very long so to demonstrate a way around this, we’ll use an alternative syntax which means the same thing. Personally, whenever using more than a single argument, this is the approach I take.
- class: AppBundle\Service\BlogManager
- - '@app.repository.post'
No magic happens when passing the repository to the blog manager, we still need to modify the constructor function within the service in order to make it available. We'll also add some methods for retrieving blog posts using the repository at the same time
- namespace AppBundle\Service;
- /* Include namespaces for type casting */
- use Doctrine\Common\Persistence\ObjectRepository;
- class BlogManager
- protected $postRepository;
- /* Notice the type forcing of the argument */
- public function __construct(ObjectRepository $postRepository)
- $this->postRepository = $postRepository;
- public function fetchPosts()
- return $this->postRepository->findAllOrderedByDate();
- public function fetchPost($id)
- return $this->postRepository->find($id);
Notice that even though we are injecting the Post repository in to the Blog Manager, it is being cast as using ObjectRepository. This would allow us to swap out the database engine with minimal changes because we are relying on an abstraction instead of the low level class.
This is just one aspect of SOLID design principles which will make your application more maintainable.
This might seem a lot of effort for just fetching some post details but consider the following:
- By seperating the controller completely from the database and using the manager as a proxy, we have tighter control on what database queries can be run.
- From a reuse point of view, adding features around the posts retrieval becomes trivial as it can be handled through the blog manager leaving the controller to focus on routing requests
- Modify your Blog Controller to make use of the Blog Manager service