mirror of
https://github.com/crazywhalecc/static-php-cli.git
synced 2026-07-07 08:45:40 +08:00
v3 base
This commit is contained in:
195
src/StaticPHP/DI/ApplicationContext.php
Normal file
195
src/StaticPHP/DI/ApplicationContext.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\DI;
|
||||
|
||||
use DI\Container;
|
||||
use DI\ContainerBuilder;
|
||||
use Psr\Container\ContainerInterface;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
use function DI\factory;
|
||||
|
||||
/**
|
||||
* ApplicationContext manages the DI container lifecycle and provides
|
||||
* a centralized access point for dependency injection.
|
||||
*
|
||||
* This replaces the scattered spc_container()->set() calls throughout the codebase.
|
||||
*/
|
||||
class ApplicationContext
|
||||
{
|
||||
private static ?Container $container = null;
|
||||
|
||||
private static ?CallbackInvoker $invoker = null;
|
||||
|
||||
private static bool $debug = false;
|
||||
|
||||
/**
|
||||
* Initialize the container with configuration.
|
||||
* Should only be called once at application startup.
|
||||
*
|
||||
* @param array $options Initialization options
|
||||
* - 'debug': Enable debug mode (disables compilation)
|
||||
* - 'definitions': Additional container definitions
|
||||
*
|
||||
* @throws \RuntimeException If already initialized
|
||||
*/
|
||||
public static function initialize(array $options = []): Container
|
||||
{
|
||||
if (self::$container !== null) {
|
||||
throw new \RuntimeException('ApplicationContext already initialized. Use reset() first if you need to reinitialize.');
|
||||
}
|
||||
|
||||
$builder = new ContainerBuilder();
|
||||
$builder->useAutowiring(true);
|
||||
$builder->useAttributes(true);
|
||||
|
||||
// Load default definitions
|
||||
self::configureDefaults($builder);
|
||||
|
||||
// Add custom definitions if provided
|
||||
if (isset($options['definitions']) && is_array($options['definitions'])) {
|
||||
$builder->addDefinitions($options['definitions']);
|
||||
}
|
||||
|
||||
// Set debug mode
|
||||
self::$debug = $options['debug'] ?? false;
|
||||
|
||||
self::$container = $builder->build();
|
||||
self::$invoker = new CallbackInvoker(self::$container);
|
||||
|
||||
return self::$container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the container instance.
|
||||
* If not initialized, initializes with default configuration.
|
||||
*/
|
||||
public static function getContainer(): Container
|
||||
{
|
||||
if (self::$container === null) {
|
||||
self::initialize();
|
||||
}
|
||||
return self::$container;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a service from the container.
|
||||
*
|
||||
* @template T
|
||||
*
|
||||
* @param class-string<T> $id Service identifier
|
||||
*
|
||||
* @return T
|
||||
*/
|
||||
public static function get(string $id): mixed
|
||||
{
|
||||
return self::getContainer()->get($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a service exists in the container.
|
||||
*/
|
||||
public static function has(string $id): bool
|
||||
{
|
||||
return self::getContainer()->has($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a service in the container.
|
||||
* Use sparingly - prefer configuration-based definitions.
|
||||
*/
|
||||
public static function set(string $id, mixed $value): void
|
||||
{
|
||||
self::getContainer()->set($id, $value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Bind command-line context to the container.
|
||||
* Called at the start of each command execution.
|
||||
*/
|
||||
public static function bindCommandContext(InputInterface $input, OutputInterface $output): void
|
||||
{
|
||||
$container = self::getContainer();
|
||||
$container->set(InputInterface::class, $input);
|
||||
$container->set(OutputInterface::class, $output);
|
||||
self::$debug = $output->isDebug();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the callback invoker instance.
|
||||
*/
|
||||
public static function getInvoker(): CallbackInvoker
|
||||
{
|
||||
if (self::$invoker === null) {
|
||||
self::$invoker = new CallbackInvoker(self::getContainer());
|
||||
}
|
||||
return self::$invoker;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke a callback with automatic dependency injection and context.
|
||||
*
|
||||
* @param callable $callback The callback to invoke
|
||||
* @param array $context Context parameters for injection
|
||||
*/
|
||||
public static function invoke(callable $callback, array $context = []): mixed
|
||||
{
|
||||
logger()->debug('[INVOKE] ' . (is_array($callback) ? (is_object($callback[0]) ? get_class($callback[0]) : $callback[0]) . '::' . $callback[1] : (is_string($callback) ? $callback : 'Closure')));
|
||||
return self::getInvoker()->invoke($callback, $context);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if debug mode is enabled.
|
||||
*/
|
||||
public static function isDebug(): bool
|
||||
{
|
||||
return self::$debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set debug mode.
|
||||
*/
|
||||
public static function setDebug(bool $debug): void
|
||||
{
|
||||
self::$debug = $debug;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset the container.
|
||||
* Primarily used for testing to ensure isolation between tests.
|
||||
*/
|
||||
public static function reset(): void
|
||||
{
|
||||
self::$container = null;
|
||||
self::$invoker = null;
|
||||
self::$debug = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configure default container definitions.
|
||||
*/
|
||||
private static function configureDefaults(ContainerBuilder $builder): void
|
||||
{
|
||||
$builder->addDefinitions([
|
||||
// Self-reference for container
|
||||
ContainerInterface::class => factory(function (Container $c) {
|
||||
return $c;
|
||||
}),
|
||||
Container::class => factory(function (Container $c) {
|
||||
return $c;
|
||||
}),
|
||||
|
||||
// CallbackInvoker is created separately to avoid circular dependency
|
||||
CallbackInvoker::class => factory(function (Container $c) {
|
||||
return new CallbackInvoker($c);
|
||||
}),
|
||||
|
||||
// Command context (set at runtime via bindCommandContext)
|
||||
InputInterface::class => \DI\value(null),
|
||||
OutputInterface::class => \DI\value(null),
|
||||
]);
|
||||
}
|
||||
}
|
||||
98
src/StaticPHP/DI/CallbackInvoker.php
Normal file
98
src/StaticPHP/DI/CallbackInvoker.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace StaticPHP\DI;
|
||||
|
||||
use DI\Container;
|
||||
|
||||
/**
|
||||
* CallbackInvoker is responsible for invoking callbacks with automatic dependency injection.
|
||||
* It supports context-based parameter resolution, allowing temporary bindings without polluting the container.
|
||||
*/
|
||||
class CallbackInvoker
|
||||
{
|
||||
public function __construct(
|
||||
private Container $container
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Invoke a callback with automatic dependency injection.
|
||||
*
|
||||
* Resolution order for each parameter:
|
||||
* 1. Context array (by type name)
|
||||
* 2. Context array (by parameter name)
|
||||
* 3. Container (by type)
|
||||
* 4. Default value
|
||||
* 5. Null (if nullable)
|
||||
*
|
||||
* @param callable $callback The callback to invoke
|
||||
* @param array $context Context parameters (type => value or name => value)
|
||||
*
|
||||
* @return mixed The return value of the callback
|
||||
*
|
||||
* @throws \RuntimeException If a required parameter cannot be resolved
|
||||
*/
|
||||
public function invoke(callable $callback, array $context = []): mixed
|
||||
{
|
||||
$reflection = new \ReflectionFunction(\Closure::fromCallable($callback));
|
||||
$args = [];
|
||||
|
||||
foreach ($reflection->getParameters() as $param) {
|
||||
$type = $param->getType();
|
||||
$typeName = $type instanceof \ReflectionNamedType ? $type->getName() : null;
|
||||
$paramName = $param->getName();
|
||||
|
||||
// 1. Look up by type name in context
|
||||
if ($typeName !== null && array_key_exists($typeName, $context)) {
|
||||
$args[] = $context[$typeName];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Look up by parameter name in context
|
||||
if (array_key_exists($paramName, $context)) {
|
||||
$args[] = $context[$paramName];
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Look up in container by type
|
||||
if ($typeName !== null && !$this->isBuiltinType($typeName) && $this->container->has($typeName)) {
|
||||
$args[] = $this->container->get($typeName);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 4. Use default value if available
|
||||
if ($param->isDefaultValueAvailable()) {
|
||||
$args[] = $param->getDefaultValue();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 5. Allow null if nullable
|
||||
if ($param->allowsNull()) {
|
||||
$args[] = null;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Cannot resolve parameter
|
||||
throw new \RuntimeException(
|
||||
"Cannot resolve parameter '{$paramName}'" .
|
||||
($typeName ? " of type '{$typeName}'" : '') .
|
||||
' for callback invocation'
|
||||
);
|
||||
}
|
||||
|
||||
return $callback(...$args);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a type name is a PHP builtin type.
|
||||
*/
|
||||
private function isBuiltinType(string $typeName): bool
|
||||
{
|
||||
return in_array($typeName, [
|
||||
'string', 'int', 'float', 'bool', 'array',
|
||||
'object', 'callable', 'iterable', 'mixed',
|
||||
'void', 'null', 'false', 'true', 'never',
|
||||
], true);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user