lekoala / kaly-di
Requires
- php: ^8.3
- psr/container: ^1.1.2|^2.0.2
Requires (Dev)
- carthage-software/mago: ^1
- composer/ca-bundle: ^1.5
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.5
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-23 07:47:07 UTC
README
Small PSR-11 autowiring container for PHP 8.3+
Kaly DI is a lightweight dependency injection container built around a strict separation of concerns:
Definitions— Kaly-specific configuration, used at the composition root.Container— a PSR-11 container. At runtime, the only API isget()/has().Injector— an independent utility for building fresh instances and invoking callables.
Application code should normally receive its dependencies directly rather than the container itself.
Key Features
- PSR-11 Compliance: interoperable with PHP standards.
- No Attributes, No Magic: plain PHP configuration, no attributes or compilation.
- Strongly Typed Definitions: define dependencies in PHP for full IDE support.
- Fail-fast composition: duplicate service definitions are rejected; intentional overrides use
rebind(). - Predictable errors: configuration invariants throw
DefinitionException; development-only checks useassert(). - Autowiring: concrete classes are resolved automatically; bind interfaces when needed.
- Predictable
has(): true for explicit definitions/bindings or instantiable concrete classes; constructor resolution may still fail inget(). - Explicit Lifecycle:
Container::get()returns shared services,Injector::make()instantiates fresh concrete classes. - Developer Friendly: typed error reporting and development-only assertions.
Installation
composer require lekoala/kaly-di
Quick Start
use Kaly\Di\Container; use Kaly\Di\Definitions; use Kaly\Di\Injector; // 1. Configure the graph at the composition root $definitions = Definitions::create() ->set(\PDO::class, fn () => new \PDO('sqlite::memory:')) ->bind(LoggerInterface::class, FileLogger::class); // 2. Create the container $container = new Container($definitions); // 3. Resolve services (shared) $pdo = $container->get(\PDO::class); $logger = $container->get(LoggerInterface::class); // 4. Build fresh instances with the Injector $injector = new Injector($container); $fresh = $injector->make(MyService::class);
get() resolves services, make() instantiates classes
$container->get(Foo::class); // shared instance, configured by Definitions $injector->make(Foo::class); // fresh concrete instance, independent of Definitions
Container::get()resolves configured container entries (definitions, bindings, objects, factories). Entries are shared: two calls with the same id return the same object.Injector::make()instantiates a concrete class independently of container definitions. It uses PSR-11 only to resolve the object dependencies of that class. An interface or abstract class cannot be built withmake().
Autowire objects; configure values
Object dependencies are resolved by type. Scalar configuration is provided explicitly at the composition root:
$definitions = Definitions::create() ->bind(LoggerInterface::class, FileLogger::class) ->parameters(LogWriter::class, path: '/var/log/app.log', retries: 3); $container = new Container($definitions); $writer = $container->get(LogWriter::class);
Read the environment at boot, convert it to plain PHP values, then forget it came from the environment. See Configuration for the full migration guide.
Documentation
Detailed guides are available in the docs/ directory:
- Definitions: bindings, parameters, callbacks and merging.
- Injector: building fresh instances and invoking callables.
- Configuration: scalar values, environment and migration without attributes.
- Async: synchronous resolution, shared services and long-lived processes.
- Architecture: design decisions and the PSR-11 boundary.
Reflection Helpers
Pure, dependency-free utilities live in Kaly\Di\Reflection:
use Kaly\Di\Reflection; Reflection::getShortClassName($object); // e.g. "MyService" Reflection::getClassNamespace(MyService::class); // e.g. "App\Service" Reflection::getParameterClass($reflectionParameter); // ?ReflectionClass
Parameter resolution (Parameters::resolveParameters())
is the internal engine of Container/Injector and is deliberately not part
of the public API: unlike the legacy permissive resolver, it never invents
''/0/false/[] defaults and throws UnresolvableParameterException for
required parameters that cannot be satisfied. resolveParameters() returns
the final positional list ready for a Reflection call (variadic spread
included). Argument lists themselves are validated first: unknown named
arguments, double assignments, surplus positionals and positional-after-named
are rejected with an InvalidArgumentException before anything is resolved.
When a union parameter has several available candidates, resolution fails with
an UnresolvableParameterException naming them instead of picking one: pass the
dependency explicitly.
Configuration Errors, Assertions and Composition Tests
Kaly DI puts each check where its cost is reasonable:
- Unconditional configuration invariants always throw a
DefinitionException, whether or not assertions are enabled: mutating locked definitions, defining the same id twice, amerge()collision, rebinding an unknown id, arebind()precondition mismatch, or a factory returning an illegal value once it has run. These facts are already known and cheap to check. - Checks that may autoload or reflect code the runtime might never use (class
existence, binding compatibility, argument types) use PHP
assert(). They run in development (zend.assertions = 1) and are disabled in production (zend.assertions = -1), so production never visits services it does not use. - The composition is validated by its tests. There is no ahead-of-time graph audit: Kaly resolves only what is actually used. Build each real configuration and resolve its real entry points instead:
public function testWebApplicationComposition(): void { $container = webDefinitions()->createContainer(); $container->get(HttpKernel::class); } public function testWorkerComposition(): void { $container = workerDefinitions()->createContainer(); $container->get(Worker::class); }
This covers the compositions and entry points actually exercised. A factory with a runtime-dependent branch, or a dynamically computed id, can still introduce a path your tests did not take; the goal is to cover real configurations, not to prove the whole graph.
DefinitionException extends LogicException and also implements the PSR-11
ContainerExceptionInterface, because it can surface from Container::get() — for
instance when a factory returns something other than an object or a class-string.
Examples and Testing
Check the unit tests for comprehensive usage examples covering all features.