[Symfony]: Services

Let’s get some terminology straight.

Service is an object which performs specific function – sending email, caching, etc. Service container will control how services are constructured and are used in Dependency Injection.

Based on DI practices objects will be passed, which makes the code cleaner and writing tests easier.

# Displays full list of defined services
php bin/console debug:container

services.yaml

Contains service configuration. Here you will find paths defined to be used as services. In addition you can use services_[env].yaml for environment specific configuration.

Service is a class which represents the business logic. To utilize Dependency Injection, Interfaces classes are passed as arguments to the class constructors. In this  aspect symfony will choose appropriate Service which implements an interface and inject this dependency.

# Displays what service is associated with the interface
php bin/console debug:autowiring LoggerInterface

# Displays all services and corresponding interfaces
php bin/console debug:autowiring

# Displays information about the service
php bin/console debug:container monolog.logger

Auto-Wire

Autowiring is responsible for injecting dependencies in your services. By default you will find autowiring enabled (services.yaml). If autowiring is disabled you will have to inject dependencies manually through defining them in services.yaml as:

[Class name]: [Injected Dependency Name]
# Injects Greeting service to BlogController
App\Controller\BlogController: ['@App\Service\Greeting']

# Injects Logger service to Greeting service (constructor)
App\Service\Greeting: ['@monolog.logger']

Auto-Configure

When set to true symfony automatically adds tags to classes implementing certain interfaces. In addition symfony automatically registers your services as commands, event subscribers, etc.

Sample services.yaml file:

# This file is the entry point to configure your own services.
# Files in the packages/ subdirectory configure your dependencies.

# Put parameters here that don't need to change on each machine where the app is deployed
# https://symfony.com/doc/current/best_practices/configuration.html#application-related-configuration
parameters:
locale: 'en'

services:
# default configuration for services in *this* file
  _defaults:
    autowire: true       # Automatically injects dependencies in your services.
    autoconfigure: true  # Automatically registers your services as commands, event subscribers, etc.
    public: false        # Allows optimizing the container by removing unused services; this also means
                         # fetching services directly from the container via $container->get() won't work.
                         # The best practice is to be explicit about your dependencies anyway.

# makes classes in src/ available to be used as services
# this creates a service per class whose id is the fully-qualified class name
App\:
  resource: '../src/*'
  exclude: '../src/{Entity,Migrations,Tests,Kernel.php}'

# controllers are imported separately to make sure services can be injected
# as action arguments even if you don't extend any base controller class
App\Controller\:
  resource: '../src/Controller'
  tags: ['controller.service_arguments']

# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

Public / Private Services

By default, services are private (public: false).

If you try to fetch the service using the container, service should be defined as public. This example is demonstrated as:

/**
 * @required
 */
public function setContainer(ContainerInterface $container = null){
  $container->get(MyService::class);
}

In such example MyService should be a public service. The better way is just to use dependency injection which will make it possible to inject private services.

Tags

Service tags are a way to tell Symfony or other third-party bundles that your service should be registered in specific way. For example, if tagged in with specific way we can fetch all services which are responsible for sending mail, based on the common tag used. Tags can be explicitly defined in services.yaml file under services block.

services:
  Swift_SmtpTransport:
    tags: ['app.mail_transport']

  Swift_SendmailTransport:
    tags: ['app.mail_transport']
public function process(ContainerBuilder $container)
{
  $taggedServices = $container->findTaggedServiceIds('app.mail_transport');
}

Another way is when symfony defines the tags based on autoconfigure option being set to true. In such case, command extending from Console\Command will automatically be tagged with console.command tag, tag makes it available under php bin/console command.

<?php

namespace App\Command;

use Symfony\Component\Console\Command\Command;

class UtilsCommand extends Command
{
  protected function configure(){
    $this->setName('app:utils')
         ->setDescription('Utils to be run in console')
         ->addArgument('name', InputArgument::REQUIRED);
  }

  ..
}
# UtilsCommand is tagged with console.command
php bin/console debug:container 'App\Command\UtilsCommand'

# app:utils is displayed based on share console.command tag
php bin/console

Manual service wiring, parameter binding

Example below demonstrates wiring $message argument in the Greeting service.

<?php

namespace App\Service;

use Psr\Log\LoggerInterface;

class Greeting
{

  /**
   * @var LoggerInterface
   */
  private $logger;
  /**
   * @var string
   */
  private $message;

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

  public function greeting(string $name): string
  {
    $this->logger->info("{$this->message} $name");
    return "Hello $name";
  }
}

Wiring service:

# config/services.yaml
services:
  App\Service\Greeting:
    arguments:
      $message: 'hello from service'

Inject our service using parameter:

# config/services.yaml
parameters:
  locale: 'en'
  hello_message: 'hello from service'

services:
  App\Service\Greeting:
    arguments:
      $message: '%hello_message%'

Binding (in this case all the parameters in any service will be wired automatically):

# config/services.yaml
parameters:
  locale: 'en'
  hello_message: 'hello from service'

services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

    bind:
      $message: '%hello_message%'

Another great feature allow wiring based on type:

# config/services.yaml
services:
  _defaults:
    autowire: true
    autoconfigure: true
    public: false

    bind:
      App\Service\SomeInterface: '@some_service'

Majd Arbash

Leave a Reply