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:
SaleLogger
with a use
statement.SaleLogger
.$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:
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?
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
?