Initial commit
This commit is contained in:
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace NoccyLabs\Hotwire;
|
||||
|
||||
use RecursiveIteratorIterator;
|
||||
use RecursiveFilesystemIterator;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
interface BuilderInterface
|
||||
{
|
||||
public function buildServices(Container $container);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user