For those who understand what it is and strive for simplicity.
Zero dependencies and PHP 7.1+.
- Simple API: Container API is as simple as regular array.
- Tags: Organize services using tags and fetch them in groups (consider them aliases).
- Shared Instances: Easily create singletons with a built-in helper.
- Service Extenders: Modify/Extend services after creation and before they are fully resolved.
- Circular Dependency Detection: Automatically prevents and reports circular dependencies.
- Autowiring: Container will automatically resolve dependencies.
- Rewrite Protection: Reports about accidental service rewrite.
- Service Providers: Package your services into a provider for better organization.
- Graph: Generate DOT script to visualize dependencies (Graphviz).
Consider Container
as a regular array.
$container = new Container();
$container['cache-file-path'] = '/tmp/app.cache';
$container[CacheStorage::class] = fn (Container $c) => new CacheStorage($c['cache-file-path']);
$container[Cache::class] = fn(Container $c) => new Cache($c[CacheStorage::class]);
$cache = $container[Cache::class];
$cache->get('currency-rates');
Easier than ever for simple script/app.
Things get a bit more complicated when you need tags. Use the Container::add()
method to setup tags.
$container = new Container();
$container['log-file-path'] = 'your.log';
$container->add('file-handler', fn (Container $c) => new StreamHandler($c['log-file-path']), ['handler']);
$container->add('stdout-handler', fn (Container $c) => new StreamHandler('php://output'), ['handler']);
$container[Logger::class] = function (Container $c) {
$logger = new Logger('app');
$logger->setHandlers($c->getByTag('handler'));
return $logger;
};
$logger = $container[Logger::class];
$logger->warning('Oops!');
Use the Container::getByTag()
method to retrieve services by tags.
This separate method exists because regular get
should return a specific service or throw a NotFoundException
exception.
Clear and strict separation: Container::get()
returns a service or throws NotFoundException
,
and the Container::getByTag()
returns an array of services of throws NotFoundException
.
This container has no separate feature like alias
for two main reasons.
- Container has a
tags
feature, basicallyalias
. - To keep container code as simple as possible.
Use tagging functionality to achieve the same $service = $container->getByTag('tag')[0];
.
Don't worry about Warning: Undefined array key 0
because container does not return an empty array. It will return
an array of services or throw NotFoundException
like regular get
.
Most likely, you want to use alias
to avoid class name exposure. For example when you want to get service
by requests param.
class GameController extends Controller {
public function getPotion(string $potionType)
{
try {
$potionService = $this->container->getByTag($potionType)[0];
return new Response($potionService->getPotion());
} catch (NotFoundException) {
return new Response('Unknown potion type: ' . $potionType, 400);
} catch (Exception $exception) {
$this->logger->error($exception->getMessage());
return new Response('Sorry, internal error', 500);
}
}
}
Use Container::addShared()
method to create a shareable service.
Container will return the same instance each time.
$container = new Container();
$container->addShared(MySQLConnection::class, fn (Container $c) => new MySQLConnection());
$conn1 = $container->get(MySQLConnection::class);
$conn2 = $container->get(MySQLConnection::class);
assert(spl_object_hash($conn1) === spl_object_hash($conn2));
Use Container::extend()
to modify services by a common parent.
$container = new Container();
$container[\Logger::class] = fn () => new \Logger();
$container[\SomeRepository::class] = fn () => new \SomeRepository();
$container->extend(\BaseRepository::class, function (\BaseRepository $repository, Container $c) {
$repository->setLogger($c[\Logger::class]);
return $repository;
});
$repository = $container[\SomeRepository::class];
// From now on, every repository returned by the container and which `extends` BaseRepository has a Logger inside.
Container will let you know if you have a circular dependency.
$container = new Container();
$container[A::class] = fn (Container $c) => new \A($c[B::class]);
$container[B::class] = fn (Container $c) => new \B($c[A::class]);
$a = $container[\A::class];
// CircularDependencyException: Circular dependency detected: A -> B -> A
Use AutowireContainer
for automatic dependency resolving.
class FilesystemAdapter {
// some logic inside
}
class Filesystem {
/**
* @var \FilesystemAdapter
*/
private $adapter;
public function __construct(FilesystemAdapter $adapter)
{
$this->adapter = $adapter;
}
// use $this->adapter as you need
}
$container = new AutowireContainer();
$filesystem = $container[Filesystem::class]
// $filesystem will be an instance of Filesystem that contains FilesystemAdapter inside
Container will throw an exception if you accidentally rewrite the service definition.
$container = new Container();
$container['k'] = 'v1';
$container['k'] = 'v2'; // RewriteAttemptException: The resource 'k' already defined.
But what if I need to overwrite definition!
Shift your mindset from "overwrite" to "setup based on X condition" and do it in the user-land code.
$container = new Container();
$container['mysql-dsn'] = getenv('ENVIRONMENT') === 'prod' ? 'mysql://production-dsn' : 'mysql://development-dsn';
This approach, together with Rewrite Protection,
will keep your code strict and more understandable.
There will be no hidden overwrites.
Providers give you the benefit of organizing your definitions.
class LoggerServiceProvider implements ProviderInterface {
public function register(Container $container): void
{
// Define your handlers, formatters, processors, and logger itself.
// All Logger-related things are in one place.
}
}
Then, add the provider to the container.
$container->addProvider(new LoggerServiceProvider());
Using the DOT language, build a graph (as in nodes and edges, not as in bar charts).
$container = new Container();
// Add services and define dependencies
$graph = new Graphviz($container);
$graph->build();
For example, you can visualize the output of Graphviz::build()
with https://dreampuf.github.io/GraphvizOnline.
Graph element explanation:
- Oval — parameter
- Solid Rectangle — service
- Dashed Rectangle — shared service
- Parallelogram — non-existent service or param
Note: Graph not working with AutowireContainer
.
Install psr/container
package first.
composer require psr/container
Then wrap Container
in PsrContainer
and pass it to whoever expected Psr\Container\ContainerInterface
.
// Define services, dependencies, etc.
$container = new Container();
// Wrap it.
$psrContainer = new PsrContainer($container);
$service = $psrContainer->get(Service::class);