Los Servicios: La columna vertebral de todo
Hablemos de los servicios. Son el concepto más importante en Symfony. Y una vez que los entiendas, sinceramente, serás capaz de hacer cualquier cosa.
¿Qué es un Servicio?
En primer lugar, un servicio es un objeto que hace un trabajo. Eso es todo. Por ejemplo, si instancias un objeto Logger que tiene un método log(), ¡eso es un servicio! Funciona: ¡registra cosas! O si creaste un objeto de conexión a la base de datos que hace consultas a la base de datos, entonces... ¡sí! Eso también es un servicio.
Entonces... si un servicio es sólo un objeto que funciona... ¿qué objetos perezosos no son servicios? Nuestra clase Starship es un ejemplo perfecto de no-servicio. Su función principal no es hacer trabajo: es guardar datos. Claro, tiene unos cuantos métodos públicos... e incluso podrías poner algo de lógica dentro de estos métodos para hacer algo. Pero en última instancia, no es un trabajador, es un poseedor de datos.
¿Y las clases controladoras? Sí, también son servicios. Su trabajo consiste en crear objetos de respuesta.
De todas formas, todo el trabajo que se hace en Symfony lo hace en realidad un servicio. ¿Escribir mensajes de registro en este archivo? Sí, hay un servicio para eso. ¿Descubrir qué ruta coincide con la URL actual? ¡Ese es el servicio router! ¿Y la representación de una plantilla Twig? Sí, resulta que el método render() es un atajo para encontrar el objeto de servicio correcto y llamar a un método en él.
El contenedor y debug:container
A veces también oirás que estos servicios están organizados en un gran objeto llamado "contenedor de servicios". Puedes pensar en el contenedor como en una gigantesca matriz asociativa de objetos de servicio, cada uno con un identificador único. ¿Quieres ver una lista de todos los servicios de nuestra aplicación ahora mismo? Yo también
Busca tu terminal y ejecuta:
bin/console debug:container
¡Son muchos servicios! Déjame hacerlo más pequeño para que cada uno quepa en su propia línea... mejor.
A la izquierda, vemos el ID de cada servicio. Y a la derecha, la clase del objeto al que corresponde el ID. Genial, ¿verdad?
Vuelve a nuestro controlador y mantén pulsado control o comando para abrir de nuevo el método json(). ¡Ahora tiene más sentido! Está comprobando si el contenedor tiene un servicio cuyo ID es serializer. Si es así, coge ese servicio del contenedor y llama al método serialize() sobre él.
Cuando trabajemos con servicios, no será exactamente así. Pero lo superimportante es que ahora entendemos lo que está pasando.
Los bundles proporcionan servicios
Mi siguiente pregunta es: ¿de dónde vienen estos servicios? Por ejemplo, ¿quién dice que hay un servicio cuyo ID es twig... y que cuando se lo pedimos al contenedor, éste debe devolver un objeto Twig Environment? La respuesta es: totalmente de bundles. De hecho, ése es el objetivo principal de instalar un nuevo bundle. Los bundles nos proporcionan servicios.
¿Recuerdas cuando instalamos twig? Añadió un bundle a nuestra aplicación. ¿Y adivinas qué hizo ese bundle? Sí: nos proporcionó nuevos servicios, incluido el serviciotwig. Los bundles nos dan servicios... y los servicios son herramientas.
Autocableado
Y aunque hay muchos servicios en esta lista, la gran mayoría son objetos de servicio de bajo nivel que nunca utilizaremos ni nos interesarán. Tampoco nos importará el ID de los servicios la mayoría de las veces.
En su lugar, ejecuta un comando relacionado llamado:
php bin/console debug:autowiring
Esto nos muestra todos los servicios que son autocableables, que es la técnica que utilizaremos para obtener servicios. Básicamente, es una lista curada de los servicios que es más probable que necesitemos.
Autoconexión del servicio Logger
Hagamos un reto: registremos algo desde nuestro controlador. He aquí un vistazo a cómo enfoco este problema en mi cerebro:
Vale, ¡necesito registrar algo! Y... registrar es trabajo. Y... ¡los servicios funcionan! Por tanto, ¡tiene que haber un servicio de registro que pueda utilizar! ¡Quod erat demonstrandum!
Perdonadme, frikis del latín. La cuestión es: si queremos registrar algo, sólo tenemos que encontrar el servicio que hace ese trabajo. ¡De acuerdo! Vuelve a ejecutar el comando pero busca log:
php bin/console debug:autowiring log
¡Boom! Ha encontrado unos 10 servicios, todos ellos empiezan por Psr\Log\LoggerInterface. Hablaremos de cuáles son esos otros servicios en el próximo tutorial. Por ahora, céntrate en el principal. Esto me dice que hay un servicio en el contenedor para un registrador. Y para obtenerlo, podemos autoconectarlo utilizando esta interfaz.
¿Qué significa esto? En el método del controlador donde queremos el logger, añade un argumento de tipo LoggerInterface - pulsa tabulador - y luego di $logger.
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| // ... lines 7 - 10 | |
| class StarshipApiController extends AbstractController | |
| { | |
| // ... line 13 | |
| public function getCollection(LoggerInterface $logger): Response | |
| { | |
| // ... lines 16 - 41 | |
| } | |
| } |
En este caso, el nombre del argumento no es importante: podría ser cualquier cosa. Lo que importa es que el LoggerInterface -que corresponde a esta declaración use - coincida con el Psr\Log\LoggerInterface de debug:autowiring.
¡Así de sencillo! Symfony verá esta sugerencia de tipo y dirá:
¡Oh! Como ese type-hint coincide con el tipo de autocableado de este servicio, deben querer que les pase ese objeto de servicio.
No sé por qué Symfony suena como una rana en mi cabeza. En fin, veamos si esto funciona. Añade dd($logger): dd() significa "volcar y morir" y viene de Symfony.
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| // ... lines 7 - 10 | |
| class StarshipApiController extends AbstractController | |
| { | |
| // ... line 13 | |
| public function getCollection(LoggerInterface $logger): Response | |
| { | |
| dd($logger); | |
| // ... lines 17 - 41 | |
| } | |
| } |
¡Actualiza! ¡Sí! Imprimió el objeto maravillosamente y luego detuvo la ejecución. ¡Funciona! Symfony nos pasa un objeto Monolog\Logger, que implementa eseLoggerInterface.
El truco que acabamos de hacer -llamado autocableado- funciona exactamente en dos lugares: los métodos de nuestro controlador y el método __construct() de cualquier servicio. Veremos esta segunda situación en el próximo capítulo.
Controlar el comportamiento de los servicios
Y si te estás preguntando de dónde salió este servicio Logger en primer lugar... ¡ya sabemos la respuesta! De un bundle. En este caso, MonologBundle. Y... ¿cómo podríamos configurar ese servicio... para que, no sé, se registre en un archivo diferente? La respuesta es: config/packages/monolog.yaml.
Esta configuración -incluida esta línea- configura MonologBundle... lo que en realidad significa que configura cómo funcionan los servicios que nos proporciona MonologBundle. Aprenderemos sobre esta sintaxis porcentual en el próximo tutorial, pero esto le dice al servicio Loggerque registre en este archivo dev.log.
Utilizar el Logger
Bien, ahora que tenemos el servicio Logger, ¡vamos a utilizarlo! ¿Cómo? Bueno, por supuesto, puedes leer la documentación. Pero gracias a la sugerencia de tipo, ¡nuestro editor nos ayudará!LoggerInterface tiene un montón de métodos. Utilicemos ->info() y digamos
| // ... lines 1 - 5 | |
| use Psr\Log\LoggerInterface; | |
| // ... lines 7 - 10 | |
| class StarshipApiController extends AbstractController | |
| { | |
| // ... line 13 | |
| public function getCollection(LoggerInterface $logger): Response | |
| { | |
| $logger->info('Starship collection retrieved'); | |
| // ... lines 17 - 41 | |
| } | |
| } |
Colección de naves recuperada.
Pruébalo: actualizar. La página funcionó... ¿pero registró algo? Podríamos comprobar el archivo dev.log. O podemos utilizar la sección Registro del perfilador para esta petición.
Ver el Perfilador de una petición API
Pero... ¡espera! Esto es una petición API... ¡así que no tenemos esa genial barra de herramientas de depuración web en la parte inferior! Es cierto... ¡pero Symfony sigue recopilando toda esa información! Para acceder al perfilador de esta petición, cambia la URL a /_profiler. Esto muestra las peticiones más recientes a nuestra aplicación, con la más reciente en la parte superior. ¿Ves ésta? ¡Es nuestra petición a la API de hace un minuto! Si haces clic en este token... ¡bum! Estamos viendo el perfilador de esa llamada a la API en todo su esplendor... incluyendo una sección de Registro... con nuestro mensaje.
Bien, ahora que hemos visto cómo utilizar un servicio, ¡vamos a crear nuestro propio servicio! Somos imparables!
21 Comments
Why is $logger used as an object but it's named as LoggerInterface ?I think quite a few other objects are autowired via "Xinterface $xobject" dependence injection.
Hey John,
Yeah, most of core objects you type hint with an interface that the real object extends. Mostly, that's done for flexibility, as you can pass any object that extends that interface (Symfony has many objects which are wrapped with another object). And finally, that's just a good OOP concept when you typhint with interfaces instead of end objects if you have them. For your own objects it's redundant to add interfaces if you don't have benefits from it, but from some third-party services it has more sense as most of them already implement some interfaces, extend some base cases, etc.
But for you, to know how to typehint - you can leverage Symfony's
bin/console debug:autowiringcommand that will show you what exactly type-hint and variable name you need to use to inject a proper service/object.I hope it helps!
Cheers!
Hey @John-S,
Great question!
In practice, I typically always use controller method injection but it really comes down to personal/project/team preference. I find controllers easier to read when all their requirements are injected into the method.
sorry. deleted.(not sure if needed because you mention this in next chapter. pls delete if not needed). Deleted post:
Two different ways of injecting logger service.
1.
class TestController
{
}
2.
class TestController
{
}
I intend to use logger in several methods but not all of them. Should i use constructor injection or inject it in every method ?
Hello,
In api controller, there is no black bar at bottom for debug. Is it possible to enable for this page also? What is actual reason that this bar is seen on MainController pages, but not ApiController pages?
thank you
Hey @Mahmut-A!
Great question! There's no web debug toolbar because this is a json response. The toolbar can only be shown on html responses.
There's still a way you can see the profile for this request though! All requests, regardless of the response type, are saved. After making a request to
/api/starshipsvisit/_profiler. You should see a list of recent requests - you can even search them! Find the one to/api/starshipsand click the token. That will be the full profile forthat request.
Hope that help!
--Kevin
Hi,
I was following the tutorial but when i enter the log section of _profiler I have a deprecation with the message "Please install the "intl" PHP extension for best performance." I wish if someone could help me with a possible solution.
I tried to require symfony/intl but it couldn't fix the error.
Thanks
Hey @Jaime-Garcia
It depends on your local PHP installation and the system you use for development. It asks you to install a
php-intlextension to PHPIf you provide more details on you system and PHP installataion then I can provide you more detail help.
Cheers
Hi, sorry for the time spent on answering, I have windows 11 and php on 8.3.12 (cli) (NTS Visual C++ 2019 x64)
as I see, it's a windows php version, not a WSL one, so you have to find php.ini and enable php-intl module there
How can we change the logger config to log into the datadog log server for example or another external log server?
Hey Ahmad,
Not sure about datadog but for example you can log into Loggly logs server. You can see the required config in the bundle's configuration file: https://github.com/symfony/monolog-bundle/blob/06ddd76fd24dcf325a61e826b5c799e4b929422e/DependencyInjection/Configuration.php#L318-L322
There you can search for other log platforms there like rollbar, etc.
Cheers!
Hello,
I wonder what exactly means autowiring? because, we need to log service. but lets say X service. how can we know, we will search it by ./php/console debug:autowiring X.. Why do we need autowiring to search some services to find?
could you please help me to make it clear?
thank you
Hey Mahmut,
Autowiring helps you to inject services with type-hints, i.e. in your action you can literally write the typehint of the class or interface from the
debug:autowiringcommand output and the system will inject that service into your action, e.g.:That's so simple. As long as a service is listed in the
debug:autowiringoutput - it will be able to typehint this way in your controller actions or__construct()methods of our services.How can you know the service name? That's a good question! Well, first of all, you probably should base on the documentation, either official Symfony docs or bundle's docs in case you're going to use a service from a bundle. Or, you can just "guess" it, like if you need a cache search, just search something like "cache" in the output and see if you have any matches. But mostly, I would recommend you to base on docs in the beginning, unless you get used to already known services and will know exactly what to look for.
I hope it's clearer for you now!
Cheers!
thank you Victor. I will add something:
for example serializer is a service. I am doing. ./bin/console debug:autowiring serialize and there are aliases in output.
I also saw this service id in debug:container. However, I could not find related class information in bundless.php.
for example, below is outpu of autowiring serialize. what is class to be seen in bundles.php? how can ı detect it?
thank you
The following classes & interfaces can be used as type-hints when autowiring:
(only showing classes/interfaces matching serialize)
Symfony\Component\Messenger\Transport\Serialization\SerializerInterface - alias:messenger.transport.native_php_serializer
Symfony\Component\Serializer\Encoder\DecoderInterface - alias:serializer
Symfony\Component\Serializer\Encoder\EncoderInterface - alias:serializer
Hey Mahmut,
Don't forget that extra services may come not only from bundles but only from pure PHP packages called Symfony components. Components are not bundles, they do not contain any configuration. For example that Serializer component's configuration is come from the core FrameworkBundle that is installed but default in our Symfony app, but the actual classes are available only after you install the component with Composer.
But as I already said, if you're not sure - please, better check the docs instead of guessing, that's the easiest and fastest way probably :)
Cheers!
How are you getting the nice icons on your project folders in PHP Storm? I assume it's from a particular theme?
Thanks for the great videos. I'm a long-time fan! Even when I already know most of what you are talking about, I always pick up helpful tips to make things just that much better.
Hey @Ernest-H
These nice icons are from https://plugins.jetbrains.com/plugin/10044-atom-material-icons IIRC
Thanks for the feedback. Cheers and happy coding!
Hi. Thank you so much for you helpful videos. A i added a point. The bundles file have the environment activation for the bundle. For example: dev => true, the bundle will be active only on dev environment.
Hi @Inforob,
Yeah that's a good point. This file was mentioned in the chapter 5 of this course, and as it was said previously in most cases you will not edit it manually, every installed bundle will be registered there with proper env activation param depending on how you installed it and recipes system configuration. Of course on custom bundles you will need to choose how to activate them.
Cheers!
"Houston: no signs of life"
Start the conversation!