Initial commit

This commit is contained in:
2025-08-30 23:12:22 +02:00
commit c63206aa71
16 changed files with 923 additions and 0 deletions
+33
View File
@@ -0,0 +1,33 @@
<?php
namespace NoccyLabs\Hotwire\Attributes;
use Attribute;
use NoccyLabs\Hotwire\Container;
#[Attribute(Attribute::TARGET_CLASS)]
class AsService
{
/**
*
* @param bool $early If true, and the service is shared, an instance will be created in advance
* @param bool $shared If true, every request for this service will return the same instance
*/
public function __construct(
public readonly bool $early = false,
public readonly bool $shared = false,
public readonly ?string $alias = null,
)
{
}
public function define(string $className, Container $container): void
{
$name = $this->alias ? $this->alias : $className;
if ($this->shared) {
$container->addShared($name, $className);
} else {
$container->add($name, $className);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace NoccyLabs\Hotwire;
use RecursiveIteratorIterator;
use RecursiveFilesystemIterator;
use Psr\Container\ContainerInterface;
interface BuilderInterface
{
public function buildServices(Container $container);
}
+113
View File
@@ -0,0 +1,113 @@
<?php
namespace NoccyLabs\Hotwire;
use RecursiveIteratorIterator;
use RecursiveFilesystemIterator;
use Psr\Container\ContainerInterface;
#[Attributes\AsService(shared: true)]
class Container implements ContainerInterface
{
/** @var list<BuilderInterface> */
private array $builders = [];
/** @var array<string,Definition}> */
private array $services = [];
/** @var array<string,mixed> Parameters to bind to variables in constructors */
private array $parameters = [];
/** @var list<string> The order in which to load services to keep dependencies from breaking */
private array $loadOrder = [];
private DependencyResolver $resolver;
public function __construct(
// ?string $magic = null
)
{
$this->resolver = new DependencyResolver();
}
public function has(string $id): bool
{
return array_key_exists($id, $this->services);
}
public function get(string $id): object
{
$def = $this->services[$id];
if ($def->shared && $def->instance) return $def->instance;
$inst = $def->newInstance($this, $this->resolver);
if ($def->shared) {
$def->instance = $inst;
}
return $inst;
}
public function add(string $id, ?string $alias = null, ?object $instance = null): Definition
{
// printf("container:add{id=%s,instance=%s}\n", $id, is_object($instance) ? spl_object_hash($instance) : $instance);
$this->services[$id] = new Definition(
id: $id,
instance: $instance,
shared: $instance ? true : false,
);
return $this->services[$id];
}
public function addShared(string $id, ?string $alias = null, ?object $instance = null): Definition
{
// printf("container:add{shared=true,id=%s,instance=%s}\n", $id, is_object($instance) ? spl_object_hash($instance) : $instance);
$this->services[$id] = new Definition(
id: $id,
instance: $instance,
shared: true,
);
return $this->services[$id];
}
public function addBuilder(BuilderInterface $builder): void
{
$this->builders[] = $builder;
}
public function setParameter(string $name, mixed $value): void
{
$this->parameters[$name] = $value;
}
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
public function hasParameter(string $name): bool
{
return array_key_exists($name, $this->parameters);
}
public function getParameter(string $name, mixed $default = null): mixed
{
return $this->parameters[$name] ?? $default;
}
public function build(): void
{
foreach ($this->builders as $builder) {
$builder->buildServices($this);
}
$this->loadEarlyServices();
}
private function loadEarlyServices(): void
{
foreach ($this->services as $service) {
if ($service->early && $service->shared && !$service->instance) {
$service->instance = $service->newInstance($this, $this->resolver);
}
}
}
}
+61
View File
@@ -0,0 +1,61 @@
<?php
namespace NoccyLabs\Hotwire;
use ReflectionClass;
use ReflectionMethod;
class Definition
{
public function __construct(
public string $id,
public ?string $alias = null,
public ?object $instance = null,
public array $arguments = [],
public bool $shared = false,
public bool $early = false,
)
{
}
public function isShared(): bool
{
return $this->shared;
}
public function hasInstance(): bool
{
return null !== $this->instance;
}
public function getConstructorArguments(): array
{
$rc = new ReflectionClass($this->id);
if (!$rc->hasMethod('__construct')) {
return [];
}
$rm = $rc->getMethod('__construct');
return $rm->getParameters();
}
public function newInstance(Container $container, DependencyResolver $resolver): object
{
$ctargs = $resolver->autowireConstructorArguments($container, $this->id);
// printf("new: %s (%s)\n", $this->id, join(", ", array_map(fn($v)=>var_export($v,true), $ctargs)));
return new $this->id(...$ctargs);
}
public function getArguments(): array
{
return $this->arguments;
}
public function addArguments(array $args): self
{
$this->arguments = [ ...$this->arguments, ...$args ];
return $this;
}
}
+126
View File
@@ -0,0 +1,126 @@
<?php
namespace NoccyLabs\Hotwire;
use ReflectionClass;
use ReflectionFunction;
use ReflectionNamedType;
use ReflectionParameter;
class DependencyResolver
{
/**
* Create a closure that autowires a method while allowing to override parameters.
*
* Ex:
* $ret = $resolver->autowire($container, [$controller,$method])(
* request: $request
* );
*
* @param Container $container
* @param callable $callable
* @return callable
*/
public function autowire(Container $container, callable $callable): callable
{
return function(...$args) use ($container, $callable) {
$callableArgs = $this->autowireArguments($container, $callable);
foreach ($callableArgs as $name => $value) {
if (isset($args[$name])) {
$callableArgs[$name] = $args[$name];
}
}
return call_user_func($callable, ...$callableArgs);
};
}
/**
* Autowire the arguments for a provided callable
*
* @param Container $container
* @param callable $callable
* @return array
*/
public function autowireArguments(Container $container, callable $callable): array
{
$arguments = [];
$rc = new ReflectionFunction($callable);
$params = $rc->getParameters();
foreach ($params as $param) {
$name = $param->getName();
$type = $param->getType();
if ($type->isBuiltin()) {
if ($container->hasParameter($name)) {
// TODO match type? (intParameter, stringParameter, ..)
$arguments[$name] = $container->getParameter($name);
} else {
$arguments[$name] = $param->isDefaultValueAvailable()
? $param->getDefaultValue()
: null
;
}
} else {
if ($type instanceof ReflectionNamedType) {
$fqcn = $type->getName();
if ($container->has($fqcn)) {
$arguments[$name] = $container->get($fqcn);
} else {
$arguments[$name] = null;
}
} else {
$arguments[$name] = null;
}
}
}
return $arguments;
}
/**
* Autowire the arguments for a provided callable
*
* @param Container $container
* @param callable $callable
* @return array
*/
public function autowireConstructorArguments(Container $container, string $class): array
{
$arguments = [];
$rc = (new ReflectionClass($class))->getMethod('__construct');
$params = $rc->getParameters();
foreach ($params as $param) {
$name = $param->getName();
$type = $param->getType();
if ($type->isBuiltin()) {
if ($container->hasParameter($name)) {
// TODO match type? (intParameter, stringParameter, ..)
$arguments[$name] = $container->getParameter($name);
} else {
$arguments[$name] = $param->isDefaultValueAvailable()
? $param->getDefaultValue()
: null
;
}
} else {
if ($type instanceof ReflectionNamedType) {
$fqcn = $type->getName();
if ($container->has($fqcn)) {
$arguments[$name] = $container->get($fqcn);
} else {
$arguments[$name] = null;
}
} else {
$arguments[$name] = null;
}
}
}
return $arguments;
}
}
+54
View File
@@ -0,0 +1,54 @@
<?php
namespace NoccyLabs\Hotwire;
use NoccyLabs\Hotwire\Attributes\AsService;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;
use Psr\Container\ContainerInterface;
class DirectoryBuilder implements BuilderInterface
{
/** @var array<string,object{class:string,service:AsService} */
private array $foundClasses = [];
public function __construct()
{
}
public function scan(string $directory, string $nsroot)
{
$directory = rtrim($directory,"/")."/";
$iter = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($directory));
foreach ($iter as $filename=>$info) {
if (fnmatch("*.php", $filename)) {
$relFile = str_replace($directory, "", $filename);
$expect = $nsroot . strtr(substr($relFile, 0, -4), [ "/" => "\\" ]);
$this->scanFile($filename, $expect);
}
}
}
private function scanFile(string $filename, string $expect): void
{
// echo $expect."\n";
if (class_exists($expect)) {
$rc = new \ReflectionClass($expect);
$ra = $rc->getAttributes(Attributes\AsService::class);
if (count($ra) == 0) return;
$this->foundClasses[$expect] = (object)[
'class' => $rc->getName(),
'service' => $ra[0]->newInstance()
];
}
}
public function buildServices(Container $container): void
{
foreach ($this->foundClasses as $s) {
$s->service->define($s->class, $container);
}
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace NoccyLabs\Hotwire;
use RecursiveIteratorIterator;
use RecursiveFilesystemIterator;
use Psr\Container\ContainerInterface;
class StaticBuilder implements BuilderInterface
{
public function __construct(
private array $services = []
)
{
}
public function addService(string $id, array $arguments): void
{
$this->services[$id] = $arguments;
}
public function buildServices(Container $container): void
{
foreach ($this->services as $id=>$args) {
$container->addShared($id)->addArguments($args);
}
}
}