Categories


Archives


Recent Posts


Categories


Symfony’s Service Container

astorm

Frustrated by Magento? Then you’ll love Commerce Bug, the must have debugging extension for anyone using Magento. Whether you’re just starting out or you’re a seasoned pro, Commerce Bug will save you and your team hours everyday. Grab a copy and start working with Magento instead of against it.

Updated for Magento 2! No Frills Magento Layout is the only Magento front end book you'll ever need. Get your copy today!

Symfony is a configuration based framework, and a framework that’s transitioning towards “configuration-but-with-defaults”. This means that every Symfony application is going to take a slightly different approach to how they use Symfony. This includes Sylius. Before we can talk about Sylius’s approach to Symfony, there’s some core Symfony concepts and vocabulary we’ll want to cover.

Today’s topic? Services and Service Containers

What is a Service?

So what exactly is a service? On one level, services are pretty simple — a service is an object that does something useful. Of course, most objects in your program are objects that do something useful — so the better question might be “what makes a service different from other objects?”.

To understand that, you need to understand an opinion that many (but not all) programmers come to after working with languages where you can instantiate an object — and that’s that instantiating an object and initializing its values can create a lot of problems for the long term maintainability of a program.

To solve this, (and a slew of other problems) systems engineers came up with a number of concepts for best practices in programming. Design By Contract, Service Oriented Architecture, Patterns of Enterprise Application Architecture — the list goes on. These ideas have trickled down to PHP, with many systems developers using the metaphor of services and service containers.

A service is a way to categorize the objects that exist in your system in terms that are outside your programming language’s implementation. A service container is the single object that manages your services for you.

//non-service container
$service = new Foo\Baz\Bar;

$service = Foo\Baz\Bar::getInstance();


//with a service container
$container = $this->getContainer();

$object = $container->get('foo.baz.bar');

Service containers are popular with systems developers because they allow them to centralize object instantiation. A service container allows the system developer to ensure that objects created in their system always have certain properties or characteristics. A service container allows systems developers to write code that monitors how their systems behave without altering the client/application code.

Symfony is built around a service container, and in order to reason about the system and understand a lot of its documentation, it’s important to understand the basics of how this container works.

The following code was developed against a Sylius Standard application, but should be applicable to generic Symfony based systems and applications as well.

Hello Routing

We’re going to walk through the configuration of the world’s simplest Symfony service. In order to do that, we need to find a way to run PHP code with the Symfony system code already bootstrapped. The simplest way to do this is configure a route in your Symfony or Sylius application. We won’t be going deep into routing this time — that’s an article for a later day.

If you found the above intimidating, don’t worry. We’ll walk you through step-by-step.

So — you’ve installed a Symfony application, maybe even the Sylius Standard Edition. If you’re familiar with Symfony, we’ll be running the following code in the dev environment. If you’re not familiar with Symfony, don’t worry, dev is currently the default environment.

Our first task is to create a route. A route lets us tell the framework

When a user loads URL X, you should run PHP code Y

Let’s try loading a URL in our system without doing anything. Try loading the following URL, replacing sylius.example.com with your own server name

http://sylius.example.com/pulsestorm_helloworld

If you do this, you’ll see an error page with the following message.

No route found for “GET /pulsestorm_helloworld”

This is Symfony’s way of telling us it doesn’t know what to do with the /pulsestorm_helloworld URL. We need to tell it what to do.

Add the following text to the following file.

#File: config/routes.yaml
a_unique_identifier:
    path: /pulsestorm_helloworld
    methods: [GET]
    controller: App\MyControllers\Pulsestorm\HelloworldController::doTheThing

A Note on YAML Files: This route configuration is done via a yaml file. The yaml format is a human readable data serialization format, and is worth reading up on if you’re not familiar with it. The short version: YAML lets you create data structures of nested key/value pairs and lists of data using simple indentation and markdown-style lists. Symfony tends to encourage use of yaml for its configuration files, although XML and PHP are also supported.

Once you’ve added this configuration, try loading the page again — this time you’ll get a different message

Controller “App\MyControllers\Pulsestorm\HelloworldController” does neither exist as service nor as class

The above configuration tells Symfony that when a user loads the path /pulsestorm_helloworld, that it should instantiate an object with the class App\MyControllers\Pulsestorm\HelloworldController, and then call the method named doTheThing on the controller object. The error message is Symfony telling us it couldn’t find this class. Let’s create it.

#File: src/MyControllers/Pulsestorm/HelloworldController.php
<?php
namespace App\MyControllers\Pulsestorm;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
class HelloworldController extends AbstractController {
    public function doTheThing() {
        echo "Hello World";
        exit;
    }
}

With the above file in place reload the page, you should see the Hello World text displayed. That’s it! We now have a place to write some basic Symfony code.

Before we continue — the above is NOT a great example of best practices around creating Symfony routes and controllers. We used weird naming conventions, and exited early from the controller. Like we said, we’ll be covering proper Symfony and Sylius routing in a future article. This is just a quick and dirty way to run some Symfony code.

Hello Service

Now that we can run code in a Symfony bootstrapped environment, we’re going to create a service. Earlier we said a service was an object that did something useful — so we’ll start by creating our object’s class, instantiating the object, and then making it do the useful thing.

First, create the following class file at the following location

#File: src/OurUsefulThing.php
<?php
namespace App;
class OurUsefulThing {
    public function sayHello() {
        echo "Hello Service";
    }
}

Then, instantiate an object from this class and call its sayHello method

#File: src/MyControllers/Pulsestorm/HelloworldController.php
/* ... */
    public function doTheThing() {
        $service = new \App\OurUsefulThing;
        $service->sayHello();
        exit;
    }
/* ... */

Reload the page and you should see your Hello Service message displayed in the browser. If you grant us that printing this message is a “useful thing”, then this class and object are great candidates to be turned into Symfony services.

Frequently Asked Questions

Wait, why did you name the class App\OurUsefulThing?

When it comes to the service system, you can name your service classes whatever you like. The App prefix is a Symfony standard for creating classes that are part of a specific application. Sylius core code is NOT a part of your application. When you download Sylius Standard Edition, Sylius’s code is managed by composer in vendor, and you build your app of these core-sylius bundles.

How did PHP know to find the App\OurUsefulThing in the src/OurUsefulThing.php file?

That’s the magic of PSR-4 autoloading. If you take a look in your project’s main composer.json (created when you installed either Symfony or Sylius Standard Edition)

#File: composer.json
"autoload": {
    "psr-4": {
        "App\\": "src/"
    }
},

you’ll see a configuration that tells PHP (or, more specifically, tells Composer’s autoloader) to look for classes prefixed with App\ in the src folder. The rest of the class name is turned into a file path, per PSR-4 rules.

These App\ classes are a default Symfony convention. You don’t need to use them and, in fact, if you plan on distributing your code to others you should probably pick a different prefix and add it to your psr-4 autoloader configuration.

Configuring the Service

OK! We’re ready to turn App\OurUsefulClass into a service. To do that, we’ll need to

  1. Come up with a unique service name
  2. Configure the service
  3. Use the service container to instantiate/fetch the service instead of fetching it directly

We’ll name our service app.our.useful.thing. A service name doesn’t need to be a direct transformation of a classname, but it often is.

To configure our service, we’ll add some lines to the config/services.yaml file. This is where we can add service configurations that are specific to our application. This file will likely already have some stock configurations in it, so make sure you’re only adding the following configuration under the existing services.yaml file

#File: config/services.yaml

# ... #

services:
    # ... #
    app.our.useful.thing:
        class: App\OurUsefulThing
        public: true
    # ... #

then, change our controller method so it looks like the following

#File: src/MyControllers/Pulsestorm/HelloworldController.php
public function doTheThing() {
    global $kernel;
    $container = $kernel->getContainer();
    $service = $container->get('app.our.useful.thing');
    $service->sayHello();
    exit;
}

Reload that page and — you should still see the Hello Service message, even though our code no longer references the App/OurUsefulThing class.

What Just Happened

So — Symfony 4 does a lot to discourage direct use of the service container object. Because of that, we had to do some global shenanigans to get at the container.

#File: src/MyControllers/Pulsestorm/HelloworldController.php

global $kernel;
$container = $kernel->getContainer();

The global $kernel variable holds the Symfony App Kernel object. This is the first thing that’s instantiated in Symfony’s index.php file, and is what kicks off execution of a Symfony based application. It also holds a reference to the service container, which we’ve fetched with the getContainer method. You probably don’t want to do this in a regular program/application, but it’s a useful mechanism for exploring the service container.

Next, we used the container’s get method to fetch our service by name, and then called our service’s sayHello method.

#File: src/MyControllers/Pulsestorm/HelloworldController.php

$service = $container->get('app.our.useful.thing');
$service->sayHello();

How did Symfony know to instantiate an App\OurUsefulThing object when we asked for the app.our.useful.thing service? That’s because of our service configuration

#File: config/services.yaml

# ... #

services:
    # ... #
    app.our.useful.thing:
        class: App\OurUsefulThing
        public: true
    # ... #

Each entry under the services key creates a named Symfony service. Because we used app.our.useful.thing:, our service is named app.our.useful.thing. Under the service’s key (app.our.useful.thing:), we set the PHP class that Symfony should use to instantiate this service (class: App\OurUsefulThing), and the public: true tells Symfony that this is a service we want to access directly from the container.

Controlling Services via Configuration

Once your object is a service, there’s a bunch of different things you can do with it. We can’t cover them all in this tutorial, but we will cover configurable constructor parameters.

Let’s change our service class so it looks like this.

#File: src/OurUsefulThing.php
<?php
namespace App;
class OurUsefulThing {
    protected $message;
    public function __construct($message='Hello Default Message') {
        $this->message = $message;
    }

    public function sayHello() {
        echo $this->message;
    }
}

We’ve added a constructor parameter with a default value, and changed the service so it echos this value. Reload the page, and you should see the Hello Default Message string displayed.

Now — try adding the following to your service configuration

#File: config/services.yaml

app.our.useful.thing:
    class: App\OurUsefulThing
    public: true
    arguments:
        - Hello From the Service Configuration

Reload the page and you’ll see the service message has changed to Hello From the Service Configuration. The arguments key configured is the arguments Symfony’s service container will use when instantiating the service object. This means you can change the behavior of your service without changing the service’s core code. This gets especially powerful once you learn these arguments can, themselves, be other services. The following configuration

arguments:
    - Hello From the Service Configuration
    - '@some.other.service'

would pass an instance of the services named some.other.service as the second parameter to the constructor (if such a service existed).

Wrap Up

Service containers have been a part of Symfony since version 2.0, first released back in 2011. In that time developer fashion has moved away from using the service container directly, and towards automatic “dependency injection” systems. You saw the edges of this in the above Symfony 4 code/configuration — needing to mark a service as public, or needing to resort to global shenanigans to get at the service container.

In our next tutorial, we’ll talk about how Symfony has used their service container system to offer an automatic constructor dependency injection system, or as its known in Symfony, autowiring.

Series Navigation<< Five First Impressions of the Sylius eCommerce SystemSymfony: Autowiring Services >>

Copyright © Alan Storm 1975 – 2019 All Rights Reserved

Originally Posted: 31st January 2019