Auto-discovery and lifecycle management for PHPdot packages. Reads Composer's installed.json, topologically sorts by dependencies, runs register then boot.
composer require phpdot/package| Requirement | Version |
|---|---|
| PHP | >= 8.3 |
| phpdot/container | >= 1.1 |
| psr/container | >= 2.0 |
composer.json (per package)
extra.phpdot.package = "Vendor\\Pkg\\PkgPackage"
↓
PackageDiscovery → reads installed.json, filters phpdot packages, caches result
↓
DependencyResolver → topological sort via DFS, detects cycles
↓
PackageKernel
Phase 1: register() → each package feeds definitions into ContainerBuilder
Phase 2: boot() → each package runs post-build logic with live container
Every package implements this. Two methods, zero magic.
use PHPdot\Container\ContainerBuilder;
use PHPdot\Package\PackageInterface;
use Psr\Container\ContainerInterface;
final class MongoDBPackage implements PackageInterface
{
public function register(ContainerBuilder $builder): void
{
$builder->addDefinitions([
Connection::class => scoped(static function (ContainerInterface $c): Connection {
return new Connection($c->get(ConnectionConfig::class));
}),
]);
}
public function boot(ContainerInterface $container): void
{
}
}Register in composer.json:
{
"extra": {
"phpdot": {
"package": "PHPdot\\MongoDB\\MongoDBPackage"
}
}
}Install via Composer. Next boot picks it up automatically. No code changes.
use PHPdot\Container\ContainerBuilder;
use PHPdot\Package\DependencyResolver;
use PHPdot\Package\PackageDiscovery;
use PHPdot\Package\PackageKernel;
$builder = new ContainerBuilder();
$kernel = new PackageKernel(new PackageDiscovery(), new DependencyResolver());
$kernel->register($vendorPath, $builder);
$container = $builder->build();
$kernel->boot($container);Dependencies come from Composer's require keys. Only phpdot-enabled packages are considered.
phpdot/config → deps: []
phpdot/i18n → deps: [phpdot/config]
phpdot/mongodb → deps: [phpdot/config]
company/billing → deps: [phpdot/mongodb]
Sorted: [config, i18n, mongodb, billing]
When any package's register() or boot() runs, every package it depends on has already completed that phase.
Cyclic dependencies throw CyclicDependencyException.
Discovery caches the sorted result as a PHP file in the vendor directory. Cache is invalidated when installed.json is newer than the cache file.
PackageDiscovery::clearCache($vendorPath);Works as a Composer script:
{
"scripts": {
"post-autoload-dump": [
"PHPdot\\Package\\PackageDiscovery::clearCache"
]
}
}interface PackageInterface
register(ContainerBuilder $builder): void
boot(ContainerInterface $container): void
final class PackageKernel
__construct(PackageDiscovery $discovery, DependencyResolver $resolver)
register(string $vendorPath, ContainerBuilder $builder): void
boot(ContainerInterface $container): void
packages(): list<DiscoveredPackage>
instances(): list<PackageInterface>
final class PackageDiscovery
discover(string $vendorPath): list<DiscoveredPackage>
static clearCache(mixed $eventOrVendorPath): void
final class DependencyResolver
resolve(list<DiscoveredPackage> $packages): list<DiscoveredPackage>
final readonly class DiscoveredPackage
public string $name
public string $class (class-string<PackageInterface>)
public array $dependencies (list<string>)
PackageDiscoveryException — class not found, interface not implemented
CyclicDependencyException — circular dependency detected
MIT