Skip to content

Service Container

A service is simply a PHP object with a name. For example:

Service Name PHP Class Description
psr_log CRM_Core_Error_Log
(LoggerInterface)
Provide logging functionality.
cache.js_strings CRM_Utils_Cache_*
(CacheInterface)
Store parsed data from Javascript files (temporarily).
crypto.jwt Civi\Crypto\CryptoJwt Create and verify JSON Web Tokens

The service container is the collection of all services in CiviCRM. It instantiates service objects (when needed).

In this chapter, we'll see how to use, discover, and define services.

Using services

The Civi facade provides access to services. For example:

Civi::service('psr_log')->info('I know how to request a service.');

Civi::service('crypto.jwt')->decode('ABCD.EFGH');

Civi::service('cache.js_strings')->set('key', 'value');

For convenience, common services have their own top-level lookup functions. These are easier to read, and the extra type-hints enable IDE auto-completion.

Civi::log()->info("I know the prettiest way to request a service");

Civi::cache('js_strings')->set('key', 'value');

If you are implementing a new service of your own, it may use dependency injection.

class MyService extends AutoService {
  /**
   * @var \Psr\Log\LoggerInterface
   * @inject psr_log
   */
  protected $log;
}

To learn more about writing new services and using dependency-injection, see Registering Services: Automatic.

Finally, the PSR-11 ContainerInterface provides an industry standard API for fetching services from a container. You can get a reference via Civi::container():

/** @var \Psr\Container\ContainerInterface $c */
$c = Civi::container();
$c->has('psr_log');
$c->get('psr_log');

Discovering services

To get a full list of all services, use the cv command. For example:

$ cv service
+---------------------+----------------------------------+
| service             | class                            |
+---------------------+----------------------------------+
| ...                 | ...                              |
| cache.default       | CRM_Utils_Cache                  |
| cache.js_strings    | CRM_Utils_Cache_Interface        |
| cache.session       | CRM_Utils_Cache_Interface        |
| ...                 | ...                              |
| crypto.jwt          | Civi\Crypto\CryptoJwt            |
| crypto.registry     | Civi\Crypto\CryptoRegistry       |
| crypto.token        | Civi\Crypto\CryptoToken          |
| ...                 | ...                              |
| psr_log             | CRM_Core_Error_Log               |
| ...                 | ...                              |
+---------------------+----------------------------------+

The list is long. You may find it easier to inspect with one of these commands:

## Show all services, page-by-page
$ cv service | less -R

## Show a specific service
$ cv service psr_log

## Show any services that include the word "cache"
$ cv service /cache/

## Show detailed info about all "crypto" services
$ cv service /crypto/ -v

Registering services

Automatic

Automatic service registration was added in CiviCRM v5.55.

Prior releases of CiviCRM only support manual service definition.

Extensions must enable the scan-classes mixin.

If you are developing an extension with auto-registered services, then you must enable the scan-classes mixin. This can be done with civix:

civix mixin --enable='scan-classes@1.0.0'

It can also be enabled by editing info.xml:

 <extension key="myextension" type="module">
   <compatibility>
+    <ver>5.55</ver>
   </compatibility>
   <mixins>
+    <mixin>scan-classes@1.0.0</mixin>
   </mixins>
   <classloader>
+    <psr0 prefix="CRM_" path=""/>
+    <psr4 prefix="Civi\" path="Civi"/>
   </classloader>
 </extension>

The class-scanner will load all PHP files in ./CRM and ./Civi. This is safe for most PHP class-files. However, if these folders have script-files or complex class-loading, then you may need to reorganize or customize.

Any class which builds on AutoService will be automatically registered as a service. We will need to use the @service annotation to identify the new service. This example registers a @service named greeter:

Register a named service
namespace Civi\Myextension;
use Civi\Core\Service\AutoService;

/**
 * @service greeter
 */
class MyGreeter extends AutoService {
  public function sayHello(string $name): void {
    echo "Hello {$name}!\n";
  }
}

If anyone requests Civi::service('greeter'), the container will make an instance of this class (new MyGreeter()).

Register a named service (factory-method)

You may wish to define initialization steps specifically for this service. One simple way is to define a factory-method.

In this example, we move the @service annotation to a static function:

namespace Civi\Myextension;
use Civi\Core\Service\AutoService;

class MyGreeter extends AutoService {

  /**
   * @service greeter
   */
  public static function factory(): MyGreeter {
    $greeter = new MyGreeter();
    // ... Perform extra initialization ...
    return $greeter;
  }

  public function sayHello(string $name): void {
    echo "Hello {$name}!\n";
  }

}

If anyone requests Civi::service('greeter'), it will be instantiated by calling MyGreeter::factory().

This requires more boilerplate, but it allows more sophisticated initialization:

  • The factory can execute additional steps (with conditions, loops, lookups, etc).
  • The factory can dynamically decide which class to instantiate (eg based on settings or environment).
  • To generate additional services (with slightly different data), you can create additional factories.

You can call the service from PHP:

Civi::service('greeter')->sayHello('world');

Or you can call it from the CLI:

cv ev 'Civi::service("greeter")->sayHello("world");'

This greeter is pretty basic. To create useful software, you may call other services -- such as the logger (psr_log) or the JWT handler (crypto.jwt) from earlier.

AutoService supports dependency injection with the @inject annotation. This allows the greeter to receive references to other services. It works with fields, methods, and constructors.

Use @inject with a field

In this example, we use @inject to fill in the fields $this->jwt and $this->psr_log.

/**
 * @service greeter
 */
class MyGreeter extends AutoService {

  /**
   * @var \Civi\Crypto\CryptoJwt
   * @inject crypto.jwt
   */
  protected $jwt;

  /**
   * @var \Psr\Log\LoggerInterface
   * @inject
   */
  protected $psr_log;

  public function sayHello(string $authToken): void {
    $claims = $this->jwt->decode($authToken);
    printf("Hello %s\n!", $claims['name']);
    $this->psr_log->info('Showed greeting');
  }
}

In general, it is necessary to give a name for the injected service (as in @inject crypto.jwt for $jwt). However, there is a special case - if the field-name is an exact match to the service-name, then you can omit it (as in the case of $psr_log).

Use @inject with a factory-method

Alternatively, we can use @inject with a factory-method. With this example, the greeter service will be instantiated by calling MyGreeter::factory($jwt, $log) with the requested services.

class MyGreeter extends AutoService {

  protected $jwt, $log;

  /**
   * @service greeter
   * @inject crypto.jwt, psr_log
   */
  public static function factory(\Civi\Crypto\CryptoJwt $jwt, \Psr\Log\LoggerInterface $log) {
    $log->info('Initializing the greeter');
    $greeter = new MyGreeter();
    $greeter->jwt = $jwt;
    $greeter->log = $log;
    return $greeter;
  }

  public function sayHello(string $authToken): void {
    $claims = $this->jwt->decode($authToken);
    printf("Hello %s\n!", $claims['name']);
    $this->log->info('Showed greeting');
  }
}

Note that the function parameters ($jwt, $log) are in the same order as the injection parameters (@inject crypto.jwt, psr_log).

@inject simply lists the services to pass to the function. The same mechanism works with other functions.

Use @inject with a setter-method

Again, we inject two services, crypto.jwt and psr_log. These are injected indirectly -- through a setter method.

/**
 * @service greeter
 */
class MyGreeter extends AutoService {

  protected $jwt, $log;

  /**
   * @inject crypto.jwt
   */
  public function setJwt(\Civi\Crypto\CryptoJwt $jwt) {
    $this->jwt = $jwt;
  }

  /**
   * @inject psr_log
   */
  public function setLog(\Psr\Log\LoggerInterface $log) {
    $this->log = $log;
  }

  public function sayHello(string $authToken): void {
    $claims = $this->jwt->decode($authToken);
    printf("Hello %s\n!", $claims['name']);
    $this->log->info('Showed greeting');
  }
}
Use @inject with a constructor

Again, we inject two services, crypto.jwt and psr_log. These are injected through the constructor.

/**
 * @service greeter
 */
class MyGreeter extends AutoService {

  protected $jwt, $log;

  /**
   * @inject crypto.jwt, psr_log
   */
  public function __construct(\Civi\Crypto\CryptoJwt $jwt, \Psr\Log\LoggerInterface $log) {
    $this->jwt = $jwt;
    $this->log = $log;
  }

  public function sayHello(string $authToken): void {
    $claims = $this->jwt->decode($authToken);
    printf("Hello %s\n!", $claims['name']);
    $this->log->info('Showed greeting');
  }
}

Note that the function parameters ($jwt, $log) match the injection parameters (@inject crypto.jwt, psr_log).

@inject simply lists the services to pass to the function. The same mechanism can be applied to any other function.

How do the field, setter-method, and constructor techniques differ?
  • Boilerplate code: Fields have shorter, more cohesive declarations. Setters and constructors require more boilerplate.
  • Assignment logic: Setters and constructors can customize the assignment logic (filtering or reshaping injected data). Fields are assigned verbatim.
  • Initialization order: The constructor receives all injections at once, and you decide how to sequence any steps within the constructor. For fields and setters, the order of initialization steps is arbitrary. (The order is deterministic but subject-to-change.)
  • Duplicate invocation: If you mix __construct()-injection with any other form of injection, then the container may invoke __construct() multiple times. (Observed in Symfony 4.x.)

In addition to consuming services, you may want to listen for events. For example, the greeter might listen to hook_civicrm_alterContent and add its greeting to every web-page. Do this with a suitable interface:

Listen to events via HookInterface
use Civi\Core\HookInterface;

/**
 * @service greeter
 */
class MyGreeter extends AutoService implements HookInterface {
  public function hook_civicrm_alterContent(&$content, $context, $tplName, &$object) {
    $content = 'Hello world' . $content;
  }
}

For more details on HookInterface, see Hooks in Symfony: HookInterface;

Listen to events via EventSubscriberInterface
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * @service greeter
 */
class MyGreeter extends AutoService implements EventSubscriberInterface {
  public static function getSubscribedEvents() {
    return ['hook_civicrm_alterContent' => 'onAlterContent'];
  }

  public function onAlterContent(GenericHookEvent $e) {
    $e->content = 'Hello world' . $content;
  }
}

For more details on EventSubscriberInterface, see Hooks in Symfony: EventSubscriberInterface;

All examples so far have been pretty similar: the class MyGreeter extends the class AutoService, and it has the public service-name greeter. This is good basic pattern, but you may need to break out of it (from time to time). Here are a few more advaned examples:

Register an internal service

An internal service is not advertised in cv service list, and it is not required to have an explicit name. If nobody will use the service directly (eg nobody calls Civi::service('greeter')), then you can mark it as @internal.

namespace Civi\Myextension;
use Civi\Core\Service\AutoService;
use Civi\Core\HookInterface;

/**
 * @service
 * @internal
 */
class MyGreeter extends AutoService implements HookInterface {
  public function hook_civicrm_alterContent(&$content, $context, $tplName, &$object) {
    $content = 'Hello world' . $content;
  }
}
```

Extend an alternative base-class

Suppose you found a pre-existing greeter class (\Upstream\SuperGreeter) which you would like to extend and register.

In lieu of the AutoService base-class, you may use the interface and trait:

  • AutoServiceInterface: This flags the class as something that Civi will auto-load.
  • AutoServiceTrait: This implements support for major annotations (@service, @inject) and interfaces (HookInterface, et al).
namespace Civi\Myextension;
use Civi\Core\Service\AutoServiceInterface;
use Civi\Core\Service\AutoServiceTrait;
use Civi\Core\Settings;
use Upstream\SuperGreeter;

/**
 * @service greeter
 */
class MyGreeter extends SuperGreeter implements AutoServiceInterface {
  use AutoServiceTrait;

  /**
   * @inject settings
   */
  public function __construct(Settings $settings) {
    parent::__construct($settings->get('my_greeter_settings'));
  }
}
Apply customized definitions

Suppose you need multiple greeters -- perhaps an optimistic greeter ("Hello! Today will be the best day of your life!") and a pessimistic greeter ("Hello! Today will be awful, and there's nothing you can do about it!"). The optimist and pessimist have different catch-phrases, but they are similar services with similar structure.

Service Name Class Catch Phrase
greeter.optimist MyGreeter "Hello! Today will be the best day of your life!"
greeter.pessimist MyGreeter "Hello! Today will be awful, and there's nothing you can do about it!"

To define two services with one class, we make a couple changes:

  • The class MyGreeter adds another property ($catchPhrase).
  • The class MyGreeter implements AutoServiceInterface::buildContainer() with help from AutoDefinition.

For this example, we again create two services (greeter.optimist and greeter.pessimist). However, this example uses custom Definitions

namespace Civi\Myextension;
use Civi\Core\Service\AutoServiceInterface;
use Civi\Core\Service\AutoDefinition;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class MyGreeter implements AutoServiceInterface {

  // ...Use whatever injections, hooks, etc seemed useful in the previous examples... and then...

  /**
   * @var string
   */
  public $catchPhrase;

  public static function buildContainer(ContainerBuilder $container): void {
    $optimist = AutoDefinition::create(static::class)
      ->setProperty('catchPhrase', 'Hello! Today will be the best day of your life!');
    $container->setDefinition('greeter.optimist', $optimist);

    $pessimist = AutoDefinition::create(static::class)
      ->setProperty('catchPhrase', 'Hello! Today will be awful, and there\'s nothing you can do about it!');
    $container->setDefinition('greeter.pessimist', $pessimist);

    $container->addResource(new \Symfony\Component\Config\Resource\FileResource(__FILE__));
  }

}

This technique allows you to fully manipulate the service definition with Symfony Service Container API. This is powerful, exposing more options and allowing dynamic services. However, the power comes with caveats:

  • Although buildContainer() allows dynamic services, it is constrained by its placement -- it runs early, before the system is fully bootstrapped. Be conservative about the use of high-level services (such as hooks, caches, and entity APIs).
  • The Symfony API occasionally changes with new versions, and it may be difficult to control the Symfony version. The Symfony version can depend on the Civi version, the CMS type (eg Drupal vs WordPress), and/or the CMS version (eg D9 vs D10). Stick to conservative options that have better interoperability.

Manual

CiviCRM's container is built with the Symfony Service Container API. This API allows one to programmatically register services. A typical registration may look like this:

/** @var Symfony\Component\DependencyInjection\ContainerBuilder $container */

$container->register('crypto.jwt', 'Civi\Crypto\CryptoJwt')
      ->setPublic(TRUE);
$container->register('psr_log', 'CRM_Core_Error_Log')
      ->setPublic(TRUE);

For more detailed information about using the PHP $container API, see Symfony Service Container.

There are two major areas where you will find manual service registrations:

  • Within civicrm-core, the class Civi\Core\Container includes a number of manual service definitions.
  • Within extensions, hook_civicrm_container may be used to add service definitions.

Appendix: Annotations

Services built on AutoService or AutoDefinition support the following annotations:

Annotation Valid For Description
@service <name> Classes,
Static-Methods
Specify the name of the service
@internal Classes,
Static-Methods
Mark the service as internal. (Internal services do not require a name, and they do not appear by default in cv services.)
@inject [<name1,...>] Fields,
Constructors,
Object-Methods,
Static-Methods,
During initialization, lookup services (by name) and assign them.

For fields, the <name> may be omitted if the field-name and service-name match.

For all functions, the injections must match the function parameters.

Appendix: Service Tags

Services in the container may be tagged to enable additional functionality.

Tag Handler Description
event_subscriber Civi EventScannerPass Scan for HookInterface or EventSubscriberInterface. Register whatever is found with the dispatcher.
spec_provider Civi SpecProviderPass Register SpecProviderInterfaces with the spec_gatherer.
internal N/A The cv CLI should hide this service (by default).
kernel.event_subscriber Symfony RegisterListenersPass Register EventSubscriberInterfaces with the dispatcher. (Details)
kernel.event_listener Symfony RegisterListenersPass Register one listener method with the dispatcher. (Details)