Typically you will have a factory which creates a concrete instance depending on some options (dependencies).
Let's say My factory requires a configuration so you will implement the RequiresConfig
interface.
use Interop\Config\RequiresConfig;
class MyAwesomeFactory implements RequiresConfig
{
public function dimensions() : iterable
{
return ['vendor-package'];
}
public function canRetrieveOptions($config) : bool
{
// custom implementation depending on specifications
}
public function options($config)
{
// custom implementation depending on specifications
}
}
If you support more than one instance with different configuration then you simply use the RequiresConfigId
interface.
Don't use the dimensions() method for container id configuration
use Interop\Config\RequiresConfigId;
class MyAwesomeFactory implements RequiresConfigId
{
public function dimensions() : iterable
{
return ['vendor-package'];
}
public function canRetrieveOptions($config, string $configId = null) : bool
{
// custom implementation depending on specifications
}
public function options($config, string $configId = null)
{
// custom implementation depending on specifications
}
}
Ok you have now a factory which says that the factory supports a configuration and you have a PHP file which contains the configuration as a PHP array, but how is the configuration used?
Depending on the implemented interface RequiresConfigId
above our configuration PHP file looks like that:
// interop config example
return [
// vendor/package name
'vendor-package' => [
// container id
'container-id' => [
// some options ...
],
],
];
As you can see that you have to implement the functionality of canRetrieveOptions()
and options()
method. Good news,
this is not necessary. See ConfigurationTrait
.
The ConfigurationTrait
is a concrete implementation of the RequiresConfig
interface and has full support of
ProvidesDefaultOptions
, RequiresMandatoryOptions
and RequiresConfigId
interfaces. It's a
PHP Trait so you can extend your factory
from a class.
Your factory looks now like that:
use Interop\Config\RequiresConfigId;
use Interop\Config\ConfigurationTrait;
class MyAwesomeFactory implements RequiresConfigId
{
use ConfigurationTrait;
public function dimensions() : iterable
{
return ['vendor-package'];
}
}
Now you have all the ingredients to create multiple different instances depending on configuration.
Factories are often implemented as a callable.
This means that your factory instance can be called like a function. You can also use a create
method or something else.
The factory gets a ContainerInterface
(Container PSR)
provided to retrieve the configuration.
Note that the configuration above is injected as
$config
inoptions()
and psr-11 is used to retrieve the application configuration.
use Interop\Config\RequiresConfigId;
use Interop\Config\ConfigurationTrait;
use Psr\Container\ContainerInterface;
class MyAwesomeFactory implements RequiresConfigId
{
use ConfigurationTrait;
public function dimensions() : iterable
{
return ['vendor-package'];
}
public function __invoke(ContainerInterface $container)
{
// get options for vendor-package.container-id
// method options() is implemented in ConfigurationTrait
$options = $this->options($container->get('config'), 'orm_default');
return new Awesome($options);
}
}
The ConfigurationTrait
does the job to check and retrieve options depending on implemented interfaces. Nice, but what is
if I have mandatory options? See RequiresMandatoryOptions
interface.
The RequiresConfig::options()
interface specification says that it MUST support mandatory options check. Let's say that we need
params for a db connection. Our config should looks like that:
// interop config example
return [
// vendor/package name
'vendor-package' => [
// container id
'container-id' => [
'params' => [
'user' => 'username',
'password' => 'password',
'dbname' => 'database',
],
],
],
];
Remember our factory sentence. My factory requires a configuration and requires a container id along with mandatory options.
The ConfigurationTrait
ensures that these options are available, otherwise an exception is thrown. This is great, because
the developer gets an exact exception message with what is wrong. This is useful for developers who use your factory the first time.
use Interop\Config\RequiresConfigId;
use Interop\Config\RequiresMandatoryOptions;
use Interop\Config\ConfigurationTrait;
use Psr\Container\ContainerInterface;
class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions
{
use ConfigurationTrait;
public function dimensions() : iterable
{
return ['vendor-package'];
}
public function mandatoryOptions() : iterable
{
return ['params' => ['user', 'password', 'dbname']];
}
public function __invoke(ContainerInterface $container)
{
// get options for vendor-package.container-id
// method options() is implemented in ConfigurationTrait
// an exception is raised when a mandatory option is missing
$options = $this->options($container->get('config'), 'container-id');
return new Awesome($options);
}
}
Hey, the database port and host is missing. That's right, but the default value of the port is 3306 and the host is
localhost. It makes no sense to set it in the configuration. So I make the database port/host not configurable? No, you
use the ProvidesDefaultOptions
interface.
The ProvidesDefaultOptions
interface defines default options for your instance. These options are merged with the provided
options.
Remember: My factory requires configuration, requires a container id along with mandatory options and it provides default options.
use Interop\Config\RequiresConfigId;
use Interop\Config\RequiresMandatoryOptions;
use Interop\Config\ProvidesDefaultOptions;
use Interop\Config\ConfigurationTrait;
use Psr\Container\ContainerInterface;
class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions, ProvidesDefaultOptions
{
use ConfigurationTrait;
public function dimensions() : iterable
{
return ['vendor-package'];
}
public function mandatoryOptions() : iterable
{
return ['params' => ['user', 'password', 'dbname']];
}
public function defaultOptions() : iterable
{
return [
'params' => [
'host' => 'localhost',
'port' => '3306',
],
];
}
public function __invoke(ContainerInterface $container)
{
// get options for vendor-package.container-id
// method options() is implemented in ConfigurationTrait
// an exception is raised when a mandatory option is missing
// if host/port is missing, default options will be used
$options = $this->options($container->get('config'), 'container-id');
return new Awesome($options);
}
}
Now you have a bullet proof factory class which throws meaningful exceptions if something goes wrong. This is cool, but I don't want to use exceptions. No problem, see next.
The RequiresConfig
interface provides a method canRetrieveOptions()
. This method checks if options are available depending on
implemented interfaces and checks that the retrieved options are an array or have implemented \ArrayAccess
.
canRetrieveOptions()
returning true does not mean thatoptions($config)
will not throw an exception. It does however mean thatoptions()
will not throw anOptionNotFoundException
. Mandatory options are not checked.
You can call this function and if it returns false, you can use the default options.
use Interop\Config\RequiresConfigId;
use Interop\Config\RequiresMandatoryOptions;
use Interop\Config\ProvidesDefaultOptions;
use Interop\Config\ConfigurationTrait;
use Psr\Container\ContainerInterface;
class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions, ProvidesDefaultOptions
{
use ConfigurationTrait;
// other functions see above
public function __invoke(ContainerInterface $container)
{
$config = $container->get('config');
$options = [];
if ($this->canRetrieveOptions($config, 'container-id')) {
// get options for vendor-package.container-id
// method options() is implemented in ConfigurationTrait
// if host/port is missing, default options will be used
$options = $this->options($config, 'container-id');
} elseif ($this instanceof ProvidesDefaultOptions) {
$options = $this->defaultOptions();
}
return new Awesome($options);
}
}
Nice, is there a one-liner? Of course. You can use the optionsWithFallback()
method. This function is not a part
of the specification but is implemented in ConfigurationTrait
to reduce some boilerplate code.
use Interop\Config\RequiresConfigId;
use Interop\Config\RequiresMandatoryOptions;
use Interop\Config\ProvidesDefaultOptions;
use Interop\Config\ConfigurationTrait;
use Psr\Container\ContainerInterface;
class MyAwesomeFactory implements RequiresConfigId, RequiresMandatoryOptions, ProvidesDefaultOptions
{
use ConfigurationTrait;
// other functions see above
public function __invoke(ContainerInterface $container)
{
// get options for vendor-package.container-id
// method options() is implemented in ConfigurationTrait
// if configuration is not available, default options will be used
$options = $this->optionsWithFallback($container->get('config'), 'container-id');
return new Awesome($options);
}
}
Using optionsWithFallback()
method and the RequiresMandatoryOptions
is ambiguous or? Yes, so it's up to you to implement
the interfaces in a sense order.
Take a look at the examples section for more use cases. interop-config
is universally applicable.