Categories
Symfony

Symfony – Bundles & Services

The purpose of bundles is to expose services to our application. It’s a bit like Dependency Injection coupled with PHP autoloading, and primarily allows for the loose coupling of parts of our code by using interfaces. A service is just a class that does some work.

Bundles are installed via composer. They add entries to the config/bundles.php via recipes. This exposes the classes within that bundle as services which can be used elsewhere.

Use the command

bin/console debug:autowiring

Services are configured using the services.yaml file. This typically configures the App namespace to be autowired (with a few exclusions such as the Entity & Test folders). This allows us to inject references to these classes into our other classes’ methods. In Magento, DI allows us to inject into class constructors, but Symfony’s system allows all Controller’s methods to receive autowired classes.

** Controllers are able to receive autowired services in any method, other classes use the DI constructor technique **

To show all registered services, including those in App\, use the --all flag.

bin/console debug:autowiring --all

This can also be used to search for services using

bin/console debug:autowiring searchTerm

When composer installs a bundle, recipes which are added are shown;

This will also change config/bundles.php. Do a git status and see the files changed by the bundle.

return [
    Symfony\Bundle\FrameworkBundle\FrameworkBundle::class => ['all' => true],
    Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
    Doctrine\Bundle\DoctrineBundle\DoctrineBundle::class => ['all' => true],
    Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle::class => ['all' => true],
    Symfony\Bundle\SecurityBundle\SecurityBundle::class => ['all' => true],
    Symfony\Bundle\TwigBundle\TwigBundle::class => ['all' => true],
    Twig\Extra\TwigExtraBundle\TwigExtraBundle::class => ['all' => true],
    Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],
    Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
    Symfony\WebpackEncoreBundle\WebpackEncoreBundle::class => ['all' => true],
    EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
    Knp\Bundle\MarkdownBundle\KnpMarkdownBundle::class => ['all' => true],
];

The last line is added and the modified bundles.php should be committed to the repository.

Each service registered has a unique ID. When running the bin/console debug:autowiring command, the unique ID appears alongside the service class name in blue.

Services are all PHP interfaces which define a set of methods for that utility. Including them in class methods automatically resolves them to a real object.

Displaying functions of Twig

Use the command

bin/console debug:twig

This shows filters, tests etc which can be used with Twig. E.g. the ‘raw’ filter which doesn’t escape HTML.

Bundles add services to other bundles.

Installing the knplabs/knp-markdown-bundle Will add the markdown filter to Twig, allowing for the following expression in Twig templates.

{{ my_data|markdown }}

When running the command bin/console debug:twig, * markdown(parser = null) is now in the filter list.

Cache Service & Configuring Service Behaviour

The cache interface is exposed as the following service;

Covers most simple to advanced caching needs.
 Symfony\Contracts\Cache\CacheInterface (cache.app)

Use this interface in methods to use this service. The cache interface is automatically instantiated from the Typehint. E.g.

use Symfony\Contracts\Cache\CacheInterface;
...

class Example {
    public function show(CacheInterface $cache) {
       ...
    }
}

Using the Cache Interface

$textContent = 'text item content'
$cache->get('text_content_cache_'.md5($textContent), function() use ($textContent) {
       // Transform the text in some way to be cached
        return "ITEM TO BE CACHED " . $textContent;
 });

Use the get method to check if a cached item exists. If it doesn’t, the callback method is executed to create the content to be cached.

Cached items are stored on the filesystem in var/cache/dev/pools/ and can be inspected in the Symfony Toolbar’s cache section, which shows cache hits and misses.

There’s also a Symfony\Contracts\Cache\TagAwareCacheInterface (cache.app.taggable) service which allows finer grained cache control with tagging of items, allowing groups to be set & cleared.

The dd (dump and die) method

Use this to inspect objects, rather than var_dump.

Configuring Bundles

Bundles provide configuration which can be passed to a service on instantiation. To show the available configuration for a service, find the bundle name in bundles.php. Use the bundle name in the following CLI command;

bin/console config:dump KnpMarkdownBundle

To change the service for the parser in the above image, we would create a new config file in config/packages. The name of the file doesn’t matter, but for convention it generally matches the first key of the YAML. In this instance, it would be;

knp_markdown:
    parser:
        service: markdown.parser.light

“markdown.parser.light” is the id of a service in the container.

The Service Container

The service container is essentially an associative array of services registered in the application. Each service’s key is its unique id. For a complete list of all services, run

bin/console debug:container

A lot of these services aren’t autowirable. For the shorter list of autowirable services (which can be used with typehints) stick to the shorter list offered by debug:autowiring

To find which class is assigned to a service’s id, use the console command to search for the string;

bin/console debug:container markdown.parser.light

Which shows the class behind the service is Knp\Bundle\MarkdownBundle\Parser\Preset\Light.

This is primarily how the autowiring system works. Bundles add services to the container and typically they use this snake-case naming scheme, which means the services can’t be autowired. Then, to add autowiring support for the most important services, they register an alias from the class or interface to that service.

Showing all configuration

The FrameworkBundle provides the main config for the symfony application. Show all of the config using

bin/console config:dump FrameworkBundle

More useful, we can dump specific config within the FramwworkBundle. To view the default cache services config, we can run

bin/console config:dump FrameworkBundle cache

This will output the config we’re able to change. Even though the cache service is offered by the FrameworkBundle, the config file for this service is in packages/cache.yaml. Generally, config will be in the bundle config file, but it’s fine to create separate config files for big parts of the same bundle.

To view the current config for cache, including our changes in YAML files, use

bin/console debug:config FrameworkBundle cache
Current configuration for "framework.cache"
===========================================

app: cache.adapter.array
prefix_seed: _%kernel.project_dir%.%kernel.container_class%
system: cache.adapter.system
directory: '%kernel.cache_dir%/pools'
default_redis_provider: 'redis://localhost'
default_memcached_provider: 'memcached://localhost'
default_pdo_provider: database_connection
pools: {  }

Environments

  • Environments are sets of configuration
  • There are two environments, dev and prod.
  • Dev shows a big exception page, prod hides errors.

Environment variables defined in .env are different to Symfony environments. To change the Symfony environment using the .env file, change APP_ENV=dev to APP_ENV=prod.

This explains how Symfony initialises via the src/Kernel.php file. This loads the config for bundles, and creates them. This file is part of the local application and is editable (although probably won’t be required).

  • registerBundles loads bundles from bundles.php (this is defined in MicroKernalTrait.php)
  • configureContainer loads all config from the config directory. This shows why config names don’t matter – they’re all globbed from the packages directory.
  • All config files are merged together in one giant array.
  • override specifig config in the environment’s directory. E.g. prod/.
  • config files are now YAML, but routes can be php too. This is defined in the configureRoutes method.
  • routes can also be defined for specific environments. The configureRoutes method loads routes from 'config/{routes}/. $this->environment . '.*.yaml'

A route which is added just to the dev environment is the web_profiler (config/routes/dev/web_profiler.yaml).

We can enable and disable bundles for particular Symfony environments. This can be achieved via the bundles.php file by defining the environment in the bundle’s config array. E.g. by default the DebugBundle is only enabled for test and dev

    Symfony\Bundle\DebugBundle\DebugBundle::class => ['dev' => true, 'test' => true],

Debugging Routes

Run the command

bin/console debug:router

This will show all defined routes. Several useful routes are configured by default for the dev environment, such as getting phpinfo() with _profiler/phpinfo.

Parameters

Use to configure services via yaml files. Typically, it’s convention to keep parameters in services.yaml, however any yaml config file can contain parameters. To show available parameters for all services, use the command

php bin/console debug:container --parameters

Parameters can be used as scalar values using the path of the config value, surrounded by percentage signs. To bind parameters to services, the arguments or bind key can be used in services.yaml. Bind is preferred and is apparently more powerful than arguments.

services:
    App\Service\Post
        bind:
            $isDebug: '%kernel.debug%

To override an existing parameter, match the key to the defined parameter’s path;

framework:
    cache:
        app: cache.adapter.filesystem

Global Config Binding

Global bindings can be defined and then used by any service using the defined key. In services.yaml use the bind key of the _defaults key;

services:
    _defaults:
        bind: 
            bool $isDebug: '%kernel.debug%'

This will now be available to any service constructor in App or any controller method. The type is optional, but it must be explicitly typed the same when passed into a method.

Defining a custom bind service

To create a service of a particular type, but with a different actual service class, use the following syntax;

services:
    # default configuration for services in *this* file
    _defaults:

        bind:

            Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'

Use @ to refer to a named service within a string. Classes can now be autowired with the service Psr\Log\LoggerInterface $mdLogger, which must be passed in exactly as specified with the typehint for default binding to work.

Alternatively to default binding, a service can also be aliased.

Aliasing Services

To create an alias of a service, define it in the services root key of services.yaml

services:
    Psr\Log\LoggerInterface $mdLogger: '@monolog.logger.markdown'

This will create a service with the alias Psr\Log\LoggerInterface $mdLogger which will be available when using that exact type & parameter name.

Named aliases

In the same way that bundle providers provide named aliases, we can also do the same. This allows us to create aliases of another service, E.g.

services:
    php.logger: '@monolog.logger.php'

    App\Controller\Scratch\MarkdownController:
        bind:
            $mdLogger: '@php.logger'

\Psr\Log\LoggerInterface $mdLogger will now be the defined PHP logger.