Search by

lekoala / kaly-di

lekoala

Package info

github.com/lekoala/kaly-di

pkg:composer/lekoala/kaly-di

Statistics

Installs: 97

Dependents: 1

Suggesters: 0

Stars: 0

Open Issues: 0

0.3.0 2026-09-21 23:11 UTC

This package is auto-updated.

Last update: 2026-09-23 07:47:07 UTC


README

Latest Version Total Downloads License PHP Version Require

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 is get()/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 use assert().
  • 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 in get().
  • 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 with make().

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, a merge() collision, rebinding an unknown id, a rebind() 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.