Symfony 4 Workshop

The Service Container

Theory

Let's talk about services.

Let's do the following. We want to create an entry in our logs every time a new sale is closed.

We could do this as an event, but let's build instead a service and call it directly so we get more acquainted with services.

Services are just plain PHP objects. However, many objects need dependencies to work correctly, and setting up these dependencies every time can be painful.

Let's create a service that depends on a generic logger. First, install the logger with composer and Symfony Flex:

composer req logger

Now let's create our service in src/Logger/SaleLogger.php:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace App\Logger;

use Psr\Log\LoggerInterface;
use App\Entity\Sale;

class SaleLogger
{
    private $logger;

    public function __construct(LoggerInterface $logger)
    {
        $this->logger = $logger;
    }

    public function log(Sale $sale)
    {
        $this->logger->info('Sold ' . count($sale->getTickets()) . ' tickets to ' . $sale->getFullName());
    }
}

Since we are specifying in the constructor that it needs an object that implements LoggerInterface to be built, Symfony will try to find if there is a service that implements that interface.

We have just installed a logger that implements that interface, so from now on, Symfony knows how to build our SaleLogger when we ask for it.

Can we use the service now? If so, how?

Let's see.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
use App\Logger\SaleLogger;

//...

    /**
     * @Route("/movie/{movieId}/book", name="book")
     */
    public function newSale(SaleLogger $saleLogger, Request $request, $movieId)
    {
        $sale = new Sale();
        $movie = $this->getDoctrine()
            ->getRepository(Movie::class)
            ->find($movieId);

        $form = $this->createForm(SaleType::class, $sale);
    
        $form->handleRequest($request);
    
        if ($form->isSubmitted() && $form->isValid()) {
            $em = $this->getDoctrine()->getManager();
            $em->persist($sale);

            $numTickets = $form['numTickets']->getData();

            $this->get('event_dispatcher')
                ->dispatch(
                    SaleEvent::NAME,
                    new SaleEvent($sale, $movie, $numTickets)
                );

            $em->flush();

            $saleLogger->log($sale);
        
            $this->addFlash(
                'notice',
                'Thank you for your sale!'
            );
        
            return $this->redirectToRoute('index');
        }
    
        return $this->render('movies/newSale.html.twig', [
            'form' => $form->createView(),
            'movie' => $movie,
        ]);
    }

We are only changing three lines:

  • Line 1: Importing SaleLogger with a use statement.
  • Line 8: Saying that the action needs as parameter a SaleLogger.
  • Line 33: Using it $saleLogger->log($sale).

If you complete a sale, you should see this among the logs in var/log/dev.log:

1
2

[2017-10-20 11:03:57] app.INFO: Sold 3 tickets to Victoria [] []

We could use this pattern to do anything that requires dependencies: sending emails, generating PDFs, generating thumbnails when a new image is processed, and so on.

Still: If we try this in the controller, it won't work. Why?

1
$this->get(SaleLogger::class)->log($sale);

Let's see what is going on with our service.

If you want to know what services are available at a given time run:

Exercise

Examine the services available running: bin/console debug:container

You can filter the search, for instance doing this

bin/console debug:container 'App\Logger\SaleLogger'

We will see something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

Information for Service "App\Logger\SaleLogger"
===============================================

 ---------------- -----------------------
  Option           Value
 ---------------- -----------------------
  Service ID       App\Logger\SaleLogger
  Class            App\Logger\SaleLogger
  Tags             -
  Public           no
  Synthetic        no
  Lazy             no
  Shared           yes
  Abstract         no
  Autowired        yes
  Autoconfigured   yes
 ---------------- -----------------------

The problem here is that Public no means that our service can be used to build other dependencies, but we can not `get` it directly from the service container.

If we wanted to change this, we could add this into config/services.yaml

1
2
3
4
5

App\Logger\:
    resource: '../src/Logger'
    public: true
        

Doing that, we are configuring our service to be public.

Symfony will try its best to configure our services, but sometimes we will need to tweak the configuration.

Now that we know about services, can we discuss this question that was left open?

Discuss

How is that we can run $this->get( 'doctrine.orm.default_entity_manager' ) in our controller? Can we obtain information about it running bin/console debug:container event?