Configuring a Service by Hand
Write a definition yourself when a service needs explicit arguments, two different configurations, or a chosen implementation behind an interface.
app.service_id: { class: App\Service\Thing, arguments: { $name: '@other.service' } }Autowiring covers the common case: one instance per class, every argument matched by type. A hand-written service definition covers the rest. A definition is an entry under services: whose key is the service id and whose body says which class to build and what to pass it.
Arguments
Two forms exist. A list passes arguments positionally, in constructor order. A map keyed by $name sets arguments by parameter name, leaving the others to their defaults or to autowiring. The map is almost always the better choice: it survives a new argument being added in the middle.
Inside an argument value, '@some.id' means the service with that id, '%app.param%' means a container parameter, and '@@literal' is how you pass a string that really starts with an at sign.
Two configurations of one class
Give each one its own id, then add an alias so the class name still resolves to a sensible default. Other classes keep type-hinting the class; the ones that need the other instance say so with #[Autowire(service: 'app.other.id')].
Shorthand and longhand
App\Image\Thumbnailer: '@app.thumbnailer.hero' is shorthand for a definition whose only key is alias: app.thumbnailer.hero. App\Image\Thumbnailer: ~ is shorthand for an empty definition: build this class, autowire everything.
What trips people up
_defaults.bind sets a value for every matching argument in the file, which is quieter than repeating it in ten definitions.
Example
// src/Image/Thumbnailer.php
declare(strict_types=1);
namespace App\Image;
final class Thumbnailer
{
public function __construct(
// none of these can be autowired: they are the configuration
private int $maxWidth,
private int $maxHeight,
private int $quality = 82,
) {
}
/**
* @return array{width: int, height: int}
*/
public function fit(int $width, int $height): array
{
// never enlarge: the 1 caps the scale factor
$scale = min($this->maxWidth / $width, $this->maxHeight / $height, 1);
return [
'width' => (int) round($width * $scale),
'height' => (int) round($height * $scale),
];
}
public function quality(): int
{
return $this->quality;
}
}
// (new Thumbnailer(400, 400))->fit(1600, 900)
// Output: ['width' => 400, 'height' => 225]
// (new Thumbnailer(400, 400))->fit(120, 80)
// Output: ['width' => 120, 'height' => 80]When to use it
- Register the same class twice with different sizes, accounts or timeouts.
- Pass a directory, a retry count or an API base URL that the container cannot infer.
- Choose which implementation an interface resolves to for the whole application.
More examples
Two instances of one class, plus a default
The exclude entry stops the resource block from also registering the class, which would make the two definitions compete for the same id. Use this shape whenever one class is really two pieces of configuration: image sizes, retry budgets, two accounts on the same API.
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
App\:
resource: '../src/'
# this class is configured by hand below
exclude:
- '../src/Image/Thumbnailer.php'
# arguments by name: clear, and safe if a constructor argument is added
app.thumbnailer.hero:
class: App\Image\Thumbnailer
arguments:
$maxWidth: 1600
$maxHeight: 900
$quality: 90
# positional arguments: constructor order, defaults kept for the rest
app.thumbnailer.avatar:
class: App\Image\Thumbnailer
arguments: [128, 128]
# shorthand alias: type-hinting the class injects the hero instance.
# Longhand: { alias: app.thumbnailer.hero }
App\Image\Thumbnailer: '@app.thumbnailer.hero'Asking for the non-default instance
One class needs the avatar sizes while everything else wants the default, so the exception is stated on the argument that needs it rather than in a configuration file. Reach for this whenever exactly one consumer differs; if several do, a named alias reads better.
// src/Image/AvatarUploader.php
declare(strict_types=1);
namespace App\Image;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
final class AvatarUploader
{
public function __construct(
// the alias would give the hero instance, so name the id instead.
// Longhand: arguments: { $thumbnailer: '@app.thumbnailer.avatar' }
#[Autowire(service: 'app.thumbnailer.avatar')]
private Thumbnailer $thumbnailer,
#[Autowire('%kernel.project_dir%/public/uploads')]
private string $uploadDir,
) {
}
public function targetPath(string $filename, int $width, int $height): string
{
$size = $this->thumbnailer->fit($width, $height);
return sprintf('%s/%dx%d/%s', $this->uploadDir, $size['width'], $size['height'], $filename);
}
}
// With kernel.project_dir = /srv/app:
// $uploader->targetPath('ada.jpg', 512, 512)
// Output: /srv/app/public/uploads/128x128/ada.jpgBinding a value once for the whole file
bind removes the repetition when several services need the same scalar value. It applies only to services defined in the same file, and an explicit arguments entry on a definition still wins over it.
# config/services.yaml
services:
_defaults:
autowire: true
autoconfigure: true
bind:
# every argument named $uploadDir in this file receives this value
$uploadDir: '%kernel.project_dir%/public/uploads'
# bound by type and name together: only this type with this name
App\Image\Thumbnailer $avatarThumbnailer: '@app.thumbnailer.avatar'
App\:
resource: '../src/'
Discussion