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-21 23:12:43 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().
Documentation
Detailed guides are available in the docs/ directory:
- Definitions: bindings, parameters, callbacks and merging.
- Injector: building fresh instances and invoking callables.
- 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.