lexide / syringe
Lexide Syringe, configuration utility for Pimple
Installs: 8 327
Dependents: 2
Suggesters: 0
Security: 0
Stars: 0
Watchers: 3
Forks: 1
Open Issues: 6
Requires
- php: >=5.6
- pimple/pimple: ~3.0
- symfony/yaml: ~2.6|~3.0
Requires (Dev)
- mikey179/vfsstream: ~1.6.7
- phpunit/phpunit: ~4
Suggests
- ext-yaml: Allows much faster parsing of Yaml files
- dev-develop
- v3.x-dev
- 2.2.6
- 2.2.5
- 2.2.4
- 2.2.3
- 2.2.2
- 2.2.1
- 2.2.0
- 2.1.1
- 2.1.0
- 2.0.0
- 1.4.2
- 1.4.1
- 1.4.0
- 1.3.2
- 1.3.1
- 1.3.0
- 1.2.0
- 1.1.13
- 1.1.12
- 1.1.11
- 1.1.10
- 1.1.9
- 1.1.8
- 1.1.7
- 1.1.6
- 1.1.5
- 1.1.4
- 1.1.3
- 1.1.2
- 1.1.1
- 1.1.0
- dev-master / 1.0.x-dev
- 1.0.0
- 0.4.1
- 0.4.0
- 0.3.0
- 0.2.4
- 0.2.3
- 0.2.2
- 0.2.1
- 0.2.0
- 0.1.8
- 0.1.7
- 0.1.6
- 0.1.5
- 0.1.4
- 0.1.3
- 0.1.2
- 0.1.1
- 0.1.0
- dev-story/8
- dev-argument-array
- dev-argument-key-resolving
This package is auto-updated.
Last update: 2024-11-29 05:38:37 UTC
README
Syringe allows a Pimple DI container to be created and populated with services defined in configuration files, in the same fashion as Symfony's DI module.
Installation
composer require lexide/syringe
Getting Started
The simplest method to create and set up a new Container is to use the Lexide\Syringe\Syringe
class. It requires the path to the application directory and a list of filepaths that are relative to that directory
use Lexide\Syringe\Syringe; $appDir = __DIR__; $configFiles = [ "config/syringe.yml" // add paths to your configuration files here ]; Syringe::init($appDir, $configFiles); $container = Syringe::createContainer();
Configuration Files
By default, Syringe allows config files to be in JSON or YAML format. Each file can define parameters, services and tags to inject into the container, and these entities can be referenced in other areas of configuration.
Parameters
A Parameter is a named, static value, that can be accessed directly from the Container, or injected into other parameters or services.
For a config file to define a parameter, it uses the parameters
key and then states the parameters name and value.
parameters: myParam: "value"
Once defined, a parameter can be referenced inside a string value by surrounding its name with the %
symbol and the parameters value will the be inserted when the the string value is resolved. This can be done in service arguments or in other parameters, like so:
parameters: firstName: "Joe" lastName: "Bloggs" fullName: "%firstName% %lastName%"
Parameters can have any scalar or array value. Arrays are resolved recursively; you can set an array of strings to a parameter, each of which contain references to other parameters. This works for both values and array keys.
parameters: myFirstValue: "first" mySecondValue: "second" myList: - "The first value is %myFirstValue%" - "The second value is %mySecondValue% myHash: "%myFirstValue%": "%mySecondValue%"
Constants
Quite often, a value set in a PHP constant is required to be injected. Hard coding these value directly into DI config is brittle and requires maintenance to keep in sync, which should be avoided where possible.
Syringe solves this problem by allowing PHP constants to be referenced directly in config, by surrounding the constant name with ^
characters:
parameters: maxIntValue: "^PHP_INT_MAX^" custom: "^MY_CUSTOM_CONSTANT^" classConstant: "^MyModule\\MyService::CLASS_CONSTANT^"
Where class constants are used, you are required to provide the fully qualified class name. As this has to be enclosed inside a string, all forward slashes must be escaped, as in the example.
Services
Services are instances of a class that can have other services, parameters or values injected into them. A config file defines services inside the services
key and gives each entry a class
key, containing the fully qualified class name to instantiate.
For classes which have constructor arguments, these can be specified by setting the arguments
key to a list of values, parameters or other services, as required by the constructor
services: myService: class: MyModule\MyService arguments: - "first constructor argument" - 12345 - false
Service injection
Services can have parameters or other services injected into them as method arguments, by referencing a service name prefixed with the @
character. This is done in one of two ways:
Constructor injection
Injection can be done when a service is instantiated, by setting references in arguments
key of a service definition. This is typically done for dependencies which are required.
services: injectable: class: MyModule\MyDependency myService: class: MyModule\MyService arguments: - "@injectable" - "%myParam%"
Setter injection
Services can also be injected by calling a method after the service has been instantiated, passing the dependant service in as an argument. This form is useful for optional dependencies.
services: injectable: class: MyModule\MyDependency myService: class: MyModule\MyService calls: - method: "setInjectable" arguments: - "@injectable"
The calls
key can be used to run any method on a service, not necessarily one to inject a dependency. They are executed in the order they are defined.
services: myService: class: MyModule\MyService calls: - method: "warmCache" - method: "setTimeout" arguments: ["%myTimeout%"] - method: "setLogger" arguments: ["@myLogger"]
Tags
In some cases, you may want to inject all the services of a given type as a method argument. This can be done manually, by building a list of service references in config, but maintaining such a list is cumbersome and time consuming.
The solution is tags; allowing you to tag a service as being part of a collection and then to inject the whole collection of services in one reference.
A tag is referenced by prefixing its name with the #
character.
services: logHandler1: ... tags: - "logHandlers" logHandler2: ... tags: - "logHandlers" loggerService: ... arguments: - "#logHandlers"
When the tag is resolved, the collection is passed through as a simple numeric array. The parent service will have no knowledge that a tag was used to generate this list.
Factories
If you have a number of services to be available that use the same class or interface, it can be advantageous to abstract the creation of these services into a factory class, to aid maintenance and reusability. Syringe provides two methods of using factories in this way; via a call to a static method on the factory class, or by calling a method on a separate factory service.
services: newService1: class: MyModule\MyService factoryClass: MyModule\MyServiceFactory factoryMethod: "createdWithStatic" newService2: class: MyModule\MyService factoryService: "@myServiceFactory" factoryMethod: "createdWithService" myServiceFactory: class: MyModule\MyServiceFactory
If the factory methods require arguments, you can pass them through using the arguments
key, in the same way you would for a normal service or a method call.
Service Aliases
Syringe allows you to alias a service name to point to another definition, using the aliasOf
key.
This is useful if you deal with other modules and need to use your own version of a service instead of the module's default one.
# [foo.yml] services: default: class: MyModule\DefaultService ... # [bar.yml] services: default: aliasOf: "@custom" custom: class: MyModule\MyService ...
Abstract Services
Services can often have definitions that are very similar or contain portions that will always be the same.
As a method to reduce duplicated config, a service's definition can "extend" a base definition. This has the effect of merging the two definitions together. Any key conflicts take the service's value rather than the one from the base, however the list of calls is merged rather than overwritten. There is no restriction on what keys you can define in the base definition.
Base definitions have to be marked as abstract
and cannot be used directly as a service. These abstract definitions can extend other definitions in the same way, similar to how inheritence works in OOP.
services: loggable: abstract: true calls: - method: "setLogger" - arguments: "@logger" myService: class: MyModule\MyService extends: "@loggable" # this will import the "setLogger" call into this service definition factoriedService: abstract: true extends: "@loggable" factoryClass: MyModule\MyServiceFactory factoryMethod: "create" myFactoriedService: class: MyModule\MyService extends: "@factoriedService" # imports both the factory config and the "setLogger" call
Private Services
For the vast majority of cases, there is no issue with services being accessed from outside of the current module. In fact this is advantageous as it promotes modular design, reuse of services and code discovery. However, there can be times when data security requires that a service be locked down and to not be available to anything outside of the control of the current module.
Services can be marked as private by adding the private
key to their definition:
services: myService: ... private: true
Private services will only be available to other services that are defined with the same config alias, usually within the same module.
Stubbed Services
In some cases, you may require an application or external library to inject a service that you don't have information on, such as a plugin or adapter that has functionality that doesn't belong in your library.
In order for syringe to handle these situations, you should create a stub service to act as a placeholder which can be aliased later. These serve as a hook or API for other libraries to interact with your code through Syringe.
# library A services: # This service uses the "adapterService" stub aService: ... arguments: - "@adapterService" adapterService: stub: true # library B services: myAdapter: ... # alias "myAdapter" to be the service injected into "library_a.aService" library_a.adapterService: aliasOf: "@myAdapter"
By themselves, stub services cannot be accessed or injected; they must have been aliased before the service that uses them can be created.
Imports
When your object graph becomes large enough, it is often useful to split your configuration into separate files; keeping related parameters and services together. This can be done by using the imports
key:
imports: - "loggers.yml" - "users.yml" - "report/orders.yml" - "report/products.yml" services: ...
If any imported files contain duplicated keys, the file that is further down the list wins. As the parent file is always processed last, its services and parameters always take precedence over the imported config.
# [foo.yml] parameters: baz: "from foo" # [bar.yml] imports: - "foo.yml" parameters: baz: "from bar" # when bar.yml is loaded into Syringe, the "baz" parameter will have a value of "from bar"
Environment Variables
If required, Syringe allows you to set environment variables on the server that will be imported at runtime. This can be used to set different parameter values for local development machines and production servers, for example.
Any environment variable prefixed with SYRINGE__
will be imported as a parameter:
Config Aliases and Namespacing
When dealing with a large object graph, conflicting service names can become an issue. To avoid this, Syringe allows you to set an "alias" or namespace for a config file. Within the file, services can be referenced as normal, but files which use different aliases or no alias need to prefix the service name with the alias. This allows you to compartmentalise your DI config for better organisation and to promote modular coding.
For example, the two config files, foo.yml
and bar.yml
can be given aliases when setting up the config files to create a Container from:
$configFiles = [ "foo_alias" => "foo.yml", "bar_alias" => "bar.yml" ];
foo.yml
could defined a service, fooOne
, which injected another service in the same file, fooTwo
, as normal.
However, if a service in bar.yml
wanted to inject fooTwo
, it would have to use its full service reference @foo_alias.fooTwo
. Likewise if fooOne
wanted to inject barOne
from bar.yml
it would have to use @bar_alias.barOne
as the service reference.
Extensions
There can be times where you need to call setters on a dependent module's services, in order to inject your own dependent service as a replacement for the module's default one.
In order to do this, you need to use the extensions
key. This allows you to specify the service and provide a list of calls to make on it, essentially appending them to the service's own calls
key
# [foo.yml, aliased with "foo_alias"] services: myService: class: MyModule\MyService ... # [bar.yml] services: myCustomLogger: ... extensions: foo_alias.myService: - method: "addLogger" arguments: "@myCustomLogger"
Reference characters
In order to identify references, the following characters are used:
@
- Services%
- Parameters#
- Tags^
- Constants
Conventions
Syringe does not enforce naming or style conventions, with one exception. A service's name can be any you like, as long as it does not start with one of the reference characters, but a config alias is always seperated from a service name with a .
, e.g. myAlias.serviceName
. For this reason it can be useful to use .
as a separator in your own service names, to "namespace" related services and parameters:
parameters: database.host: "..." database.username: "..." database.password: "..." services: database.client: ...
Advanced Usage
The ContainerBuilder
The ContainerBuilder
class is the main component of Syringe. It has several configuration options that allow you to customise the containers it builds.
Base paths for config files
In order to use configuration in a particular file, its filepath must be passed to the ContainerBuilder
, which will use the loading system to convert a file into a PHP array. Syringe uses absolute paths when loading files, but this is obviously not ideal when you're passing config filepaths to the ContainerBuilder
.
In order to get around this, the ContainerBuilder
allows you to set a path or collection of paths to use as a base, so you can use relative filepaths when setting it up. For example, for a config file with absolute path of /var/www/app/config/syringe.yml
, you could set a base path of /var/www/app
and use config/syringe.yml
as the relative filepath.
$basePath = "/var/www/app"; $resolver = new Lexide\Syringe\ReferenceResolver(); $builder = new Lexide\Syringe\ContainerBuilder($resolver, [$basePath]); $builder->addConfigfile("config/syringe.yml"); ...
If you use several base paths, Syringe will look for a config file in each base path in turn, so the order is important.
$basePaths = [ "my-dir/config", // both these paths contain a file called "foo.yml" "my-dir/app" ]; $resolver = new Lexide\Syringe\ReferenceResolver(); $builder = new Lexide\Syringe\ContainerBuilder($resolver, $basePaths); $builder->addConfigfile("foo.yml"); // will load my-dir/config/foo.yml, as that is the first base path in the list
Application root directory
If you have services that deal with files, it can be very useful to have the base directory of the application as a parameter in DI config, so you can be sure any relative paths you use are correct.
The ContainerBuilder
allows you to set the base directory and the parameter name at runtime:
$builder->setApplicationRootDirectory("my/application/directory", "myParameterName");
If no key is passed, the default parameter name is app.dir
.
Container class
Some projects that use Pimple, such a Silex, extend the Container
class to add functionality to their API. Syringe can create custom containers in this way by allowing you to set the container class it instantiates:
$builder->setContainerClass(Silex\Application::class); $app = $builder->createContainer(); // returns a new Silex Application
Loaders
Syringe can support any data format that can be translated into a nested PHP array. Each config file is processed by the loader system, which is comprised of a series of Loader
objects, each handling a single data format, that take a file's contents and decode it into an array of configuration.
By default the ContainerBuilder
has no loaders, so you need to add at least one before a container can be built:
$builder->addLoader(new Lexide\Syringe\Loader\YamlLoader());
Custom loaders
By default Syringe supports YAML and JSON data formats for the configurations files, but it is possible to use any format that can be translated into a nested PHP array.
The translation is done by a Loader
; a class which takes a filepath, reads the file and decodes the data.
To create a Loader
for your chosen data format, the class needs to implement the LoaderInterface
and state what its name is and what file extensions it supports. For example, a hypothetical XML Loader
would look something like this:
use Lexide\Syringe\Loader\LoaderInterface; class XmlLoader implements LoaderInterface { public function getName() { return "XML Loader"; } public function supports($file) { return pathinfo($file, PATHINFO_EXTENSION) == "xml"; } public function loadFile($file) { // load and decode the file, returning the configuration array } }
Once created, such a loader can be used by adding it to the ContainerBuilder
in the normal way.
Populating a Container
In addition to creating a new container, the ContainerBuilder
can also populate an existing container that has been created elsewhere, with the populateContainer
method:
$container = new Pimple\Container(); $builder->populateContainer($contianer);
Method reference
The ContainerBuilder
class has the following methods available:
Constructor
-
__construct(Lexide\Syringe\ReferenceResolver $resolver, array $configPaths = [])
Constructs a new
ContainerBuilder
instance, with each $configPath set using theaddConfigPath
method
Container
-
createContainer()
Create a brand new container populated with all services defined in the configuration files that have been loaded into the
ContainerBuilder
-
populateContainer(Pimple\Container $container)
Populate an existing container with services as per
createContainer
-
setContainerClass($className)
Sets the class which will be instantiated when using
createContainer
Config Files
-
addConfigFile($file, $alias = "")
Adds a new file path to load configuration from, optionally with an alias to prefix its keys with
-
addConfigFiles(array $files)
Adds several config files in one go. Elements with numeric keys are added without an alias, otherwise the key is used as the alias for that file:
$files = [ "file1.yml", "alias_two" => "file2.yml", "file3.yml", "alias_four" => "file4.yml" ]
-
addConfigPath($path)
Register a path to use as a base for relative config filepaths
Loaders
-
addLoader(Lexide\Syringe\Loader\LoaderInterface $loader)
Registers a loader to add support for a specific data format
-
removeLoader($name)
Remove a loader based on its name
-
removeLoaderByFile($file)
Remove any loader that supports this file
Misc
-
setApplicationRootDirectory($path, $key = "")
Sets the directory to use as the root for this application, useful when processing relative file paths. The parameter name will be the $key, or
app.dir
if $key is empty
Credits
Written by Danny Smart (dannysmart@lexide.com).