Search by

migears / wiring

samxxu

Config-driven container wiring — load a PHP array of factories and register it into a PSR-11 container

2.0.0 2026-10-03 03:35 UTC

This package is auto-updated.

Last update: 2026-10-03 03:47:09 UTC


README

Version

Config-driven container wiring: load a PHP array of factories and register it into a PSR-11 container.

Background: miGears is the open-source successor of TinyGears, a self-developed PHP framework. It was renamed and open-sourced recently because the name TinyGears is already taken in the open-source community.

Contents

Why

The composition root is the one place that knows every concrete class, and writing it by hand is repetitive — the same set($id, $factory) line, once per entry. This package moves that repetition into a plain PHP array, so the wiring reads as a table of id => factory:

  • Config is data, factories stay PHP. The config file returns an array of closures, so anything a closure can express still works — DSNs, credentials from getenv(), decorators, conditional wiring. There is no config DSL to learn and no format that cannot hold construction logic.
  • Explicit, never magic. No reflection, no constructor inspection, no @id references, no guessing. Every entry is listed by hand, so what gets built stays readable in one place.
  • Lazy and all-or-nothing. Factories run on the container's first get(), never at load time; and the whole file is validated before anything is registered, so one malformed entry registers nothing at all.
  • Only psr/container. The loader speaks the standard PSR-11 ContainerInterface and does not depend on any particular container. Registration needs a write side PSR-11 deliberately leaves out, so the container must also expose set() — checked at load time, never assumed.

Installation

composer require migears/wiring

Requires PHP 8.1+. The only dependency is psr/container.

Quick Start

Write the wiring as data — config/wiring-list.php, beside src/ and tests/:

<?php

use Psr\Container\ContainerInterface;

return [
    // factories may ignore the container …
    'app.name' => static fn (): string => 'demo',

    // … or reach sibling entries through it
    PDO::class => static fn (): PDO => new PDO('sqlite:' . __DIR__ . '/../var/app.db'),
    LoggerInterface::class => static fn (): LoggerInterface => new NullLogger(),
    UserDao::class => static fn (ContainerInterface $c): UserDao
        => new UserDao($c->get(PDO::class), $c->get(LoggerInterface::class)),
];

Register it once, at bootstrap:

use MiGears\Wiring\Wiring;

Wiring::load(__DIR__ . '/config/wiring-list.php', $container);

$container is any PSR-11 container that also exposes set(); the loader checks for it at load time. A hand-written one is under thirty lines:

use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use RuntimeException;

final class Container implements ContainerInterface
{
    /** @var array<string, callable> */ private array $factories = [];
    /** @var array<string, mixed> */    private array $instances = [];

    public function set(string $id, callable $factory): static
    {
        $this->factories[$id] = $factory;
        unset($this->instances[$id]);
        return $this;
    }

    public function has(string $id): bool
    {
        return isset($this->factories[$id]) || array_key_exists($id, $this->instances);
    }

    public function get(string $id): mixed
    {
        if (array_key_exists($id, $this->instances)) {
            return $this->instances[$id];
        }
        if (!isset($this->factories[$id])) {
            throw new class extends RuntimeException implements NotFoundExceptionInterface {};
        }
        return $this->instances[$id] = ($this->factories[$id])();
    }
}

API Reference

MiGears\Wiring\Wiring

public static function load(string $file, ContainerInterface $container): void

Loads $file and registers every entry into $container. The container is typed as the standard Psr\Container\ContainerInterface; because PSR-11 has no registration side, load() also requires a set(string $id, callable $factory) method and checks for it up front. The file must return array<string, callable>:

  • ids are non-empty strings — a class name (PDO::class) or any custom string ('app.name')
  • factories are callables; fn (): mixed and fn (ContainerInterface $c): mixed both work

The file is read and validated in full first; only then is anything registered. Factories are registered lazily, so they run on the container's first get() for that id, and the container's result is cached as usual. The loader stays independent of any particular container: it only needs PSR-11 plus set().

MiGears\Wiring\WiringException

Thrown by load() when the file is missing or unreadable, does not return an array, holds an entry whose id is not a non-empty string or whose factory is not callable, or the container exposes no set(). It extends RuntimeException.

Errors

Every failure is an assembly mistake, so it fails loudly at initialization instead of surfacing later as a missing entry inside a request:

Condition Message
file missing or unreadable Wiring config not found or not readable: <file>
file did not return an array Wiring config must return an array, got <type>: <file>
id is not a non-empty string Wiring entry ids must be non-empty strings, got <type>: <file>
factory is not callable Wiring entry '<id>' must be a callable, got <type>: <file>
container has no set() Wiring target must be a writable PSR-11 container (no set() method), got <type>

Testing

composer test      # phpunit
composer analyse   # phpstan level 6

License

MIT. See LICENSE.

migears/wiring

Version

配置驱动的容器装配:读取一份 PHP 工厂数组,登记进一个 PSR-11 容器。

背景:miGears 是自研 PHP 框架 TinyGears 的开源后继。更名并开源,是因为 TinyGears 这个名字在开源社区已被占用。

目录

为什么

组合根是唯一知道每个具体类的地方,而手写它很重复——同样的 set($id, $factory),一个条目一行。 本包把这份重复挪进一份普通 PHP 数组,于是装配读起来就是一张 id => factory 表:

  • 配置是数据,工厂仍是 PHP。 配置文件返回一组闭包,因此闭包能表达的东西都仍然可用——DSN、来自 getenv() 的凭据、装饰器、条件装配。没有要学的配置 DSL,也没有装不下构造逻辑的格式。
  • 显式,绝不魔法。 没有反射、不检查构造函数、没有 @id 引用、不猜。每个条目都手写列出,因此 构建了什么始终在一处可读。
  • 懒加载、全有或全无。 工厂在容器首次 get() 时才运行,绝不在加载时运行;整份文件先校验完再 登记,因此一个畸形条目会让整份配置什么都不登记。
  • 只依赖 psr/container。 加载器说标准的 PSR-11 ContainerInterface,不依赖任何特定容器。登记需 要 PSR-11 刻意不提供的写侧,因此容器还须暴露 set()——在加载时检查,绝不假设。

安装

composer require migears/wiring

需要 PHP 8.1+。唯一依赖是 psr/container。

快速开始

把装配写成数据——config/wiring-list.php,与 src/、tests/ 同级:

<?php

use Psr\Container\ContainerInterface;

return [
    // 工厂可以忽略容器 …
    'app.name' => static fn (): string => 'demo',

    // … 也可以通过它取到兄弟条目
    PDO::class => static fn (): PDO => new PDO('sqlite:' . __DIR__ . '/../var/app.db'),
    LoggerInterface::class => static fn (): LoggerInterface => new NullLogger(),
    UserDao::class => static fn (ContainerInterface $c): UserDao
        => new UserDao($c->get(PDO::class), $c->get(LoggerInterface::class)),
];

在启动时登记一次:

use MiGears\Wiring\Wiring;

Wiring::load(__DIR__ . '/config/wiring-list.php', $container);

$container 是任何还暴露了 set() 的 PSR-11 容器;加载器会在加载时检查它是否存在。自己写一个不到三十行:

use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
use RuntimeException;

final class Container implements ContainerInterface
{
    /** @var array<string, callable> */ private array $factories = [];
    /** @var array<string, mixed> */    private array $instances = [];

    public function set(string $id, callable $factory): static
    {
        $this->factories[$id] = $factory;
        unset($this->instances[$id]);
        return $this;
    }

    public function has(string $id): bool
    {
        return isset($this->factories[$id]) || array_key_exists($id, $this->instances);
    }

    public function get(string $id): mixed
    {
        if (array_key_exists($id, $this->instances)) {
            return $this->instances[$id];
        }
        if (!isset($this->factories[$id])) {
            throw new class extends RuntimeException implements NotFoundExceptionInterface {};
        }
        return $this->instances[$id] = ($this->factories[$id])();
    }
}

API 参考

MiGears\Wiring\Wiring

public static function load(string $file, ContainerInterface $container): void

加载 $file,把每个条目登记进 $container。容器按标准的 Psr\Container\ContainerInterface 声明; 由于 PSR-11 没有登记面,load() 还要求一个 set(string $id, callable $factory) 方法,并在开头检查它 是否存在。文件必须返回 array<string, callable>:

  • id 是非空字符串——可以是类名(PDO::class)或任意自定义字符串('app.name')
  • 工厂是 callable;fn (): mixed 与 fn (ContainerInterface $c): mixed 两种写法都行

文件先被完整读取并校验,之后才开始登记。工厂是懒登记的,因此它们在该 id 首次 get() 时才运行, 容器的结果照常被缓存。加载器不依赖任何特定容器:它只需要 PSR-11 加上 set()。

MiGears\Wiring\WiringException

当文件缺失或不可读、未返回数组、含有 id 不是非空字符串或工厂不是 callable 的条目,或容器没有暴露 set() 时,由 load() 抛出。它继承 RuntimeException。

错误

每个失败都是装配错误,因此在初始化时大声失败,而不是稍后在请求里表现为「条目不见了」:

情形 消息
文件缺失或不可读 Wiring config not found or not readable: <file>
文件没有 return 数组 Wiring config must return an array, got <type>: <file>
id 不是非空字符串 Wiring entry ids must be non-empty strings, got <type>: <file>
工厂不是 callable Wiring entry '<id>' must be a callable, got <type>: <file>
容器没有 set() Wiring target must be a writable PSR-11 container (no set() method), got <type>

测试

composer test      # phpunit
composer analyse   # phpstan level 6

许可证

MIT,见 LICENSE。