php-sdk/src/Capability/Registry/Container.php
2025-08-29 11:19:51 +02:00

188 lines
8.2 KiB
PHP

<?php
/*
* This file is part of the official PHP MCP SDK.
*
* A collaboration between Symfony and the PHP Foundation.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Mcp\Capability\Registry;
use Mcp\Exception\ContainerException;
use Mcp\Exception\ServiceNotFoundException;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\ContainerInterface;
use Psr\Container\NotFoundExceptionInterface;
/**
* A basic PSR-11 container implementation with simple constructor auto-wiring.
*
* Supports instantiating classes with parameterless constructors or constructors
* where all parameters are type-hinted classes/interfaces known to the container,
* or have default values. Does NOT support scalar/built-in type injection without defaults.
*
* @author Kyrian Obikwelu <koshnawaza@gmail.com>
*/
final class Container implements ContainerInterface
{
/**
* @var array<string, object> Cache for already created instances (shared singletons)
*/
private array $instances = [];
/**
* @var array<string, bool> Track classes currently being resolved to detect circular dependencies
*/
private array $resolving = [];
/**
* Finds an entry of the container by its identifier and returns it.
*
* @param string $id identifier of the entry to look for (usually a FQCN)
*
* @return mixed entry
*
* @throws NotFoundExceptionInterface no entry was found for **this** identifier
* @throws ContainerExceptionInterface Error while retrieving the entry (e.g., dependency resolution failure, circular dependency).
*/
public function get(string $id): mixed
{
// 1. Check instance cache
if (isset($this->instances[$id])) {
return $this->instances[$id];
}
// 2. Check if class exists
if (!class_exists($id) && !interface_exists($id)) { // Also check interface for bindings
throw new ServiceNotFoundException(\sprintf('Class, interface, or entry "%s" not found.', $id));
}
// 7. Circular Dependency Check
if (isset($this->resolving[$id])) {
throw new ContainerException("Circular dependency detected while resolving '{$id}'. Resolution path: ".implode(' -> ', array_keys($this->resolving))." -> {$id}");
}
$this->resolving[$id] = true; // Mark as currently resolving
try {
// 3. Reflect on the class
$reflector = new \ReflectionClass($id);
// Check if class is instantiable (abstract classes, interfaces cannot be directly instantiated)
if (!$reflector->isInstantiable()) {
// We might have an interface bound to a concrete class via set()
// This check is slightly redundant due to class_exists but good practice
throw new ContainerException("Class '{$id}' is not instantiable (e.g., abstract class or interface without explicit binding).");
}
// 4. Get the constructor
$constructor = $reflector->getConstructor();
// 5. If no constructor or constructor has no parameters, instantiate directly
if (null === $constructor || 0 === $constructor->getNumberOfParameters()) {
$instance = $reflector->newInstance();
} else {
// 6. Constructor has parameters, attempt to resolve them
$parameters = $constructor->getParameters();
$resolvedArgs = [];
foreach ($parameters as $parameter) {
$resolvedArgs[] = $this->resolveParameter($parameter, $id);
}
// Instantiate with resolved arguments
$instance = $reflector->newInstanceArgs($resolvedArgs);
}
// Cache the instance
$this->instances[$id] = $instance;
return $instance;
} catch (\ReflectionException $e) {
throw new ContainerException(\sprintf('Reflection failed for %s.', $id), 0, $e);
} catch (ContainerExceptionInterface $e) { // Re-throw container exceptions directly
throw $e;
} catch (\Throwable $e) { // Catch other instantiation errors
throw new ContainerException("Failed to instantiate or resolve dependencies for '{$id}': ".$e->getMessage(), (int) $e->getCode(), $e);
} finally {
// 7. Remove from resolving stack once done (success or failure)
unset($this->resolving[$id]);
}
}
/**
* Attempts to resolve a single constructor parameter.
*
* @throws ContainerExceptionInterface if a required dependency cannot be resolved
*/
private function resolveParameter(\ReflectionParameter $parameter, string $consumerClassId): mixed
{
// Check for type hint
$type = $parameter->getType();
if ($type instanceof \ReflectionNamedType && !$type->isBuiltin()) {
// Type hint is a class or interface name
$typeName = $type->getName();
try {
// Recursively get the dependency
return $this->get($typeName);
} catch (NotFoundExceptionInterface $e) {
// Dependency class not found, fail ONLY if required
if (!$parameter->isOptional() && !$parameter->allowsNull()) {
throw new ContainerException("Unresolvable dependency '{$typeName}' required by '{$consumerClassId}' constructor parameter \${$parameter->getName()}.", 0, $e);
}
// If optional or nullable, proceed (will check allowsNull/Default below)
} catch (ContainerExceptionInterface $e) {
// Dependency itself failed to resolve (e.g., its own deps, circular)
throw new ContainerException("Failed to resolve dependency '{$typeName}' for '{$consumerClassId}' parameter \${$parameter->getName()}: ".$e->getMessage(), 0, $e);
}
}
// Check if parameter has a default value
if ($parameter->isDefaultValueAvailable()) {
return $parameter->getDefaultValue();
}
// Check if parameter allows null (and wasn't resolved above)
if ($parameter->allowsNull()) {
return null;
}
// Check if it was a built-in type without a default (unresolvable by this basic container)
if ($type instanceof \ReflectionNamedType && $type->isBuiltin()) {
throw new ContainerException("Cannot auto-wire built-in type '{$type->getName()}' for required parameter \${$parameter->getName()} in '{$consumerClassId}' constructor. Provide a default value or use a more advanced container.");
}
// Check if it was a union/intersection type without a default (also unresolvable)
if (null !== $type && !$type instanceof \ReflectionNamedType) {
throw new ContainerException("Cannot auto-wire complex type (union/intersection) for required parameter \${$parameter->getName()} in '{$consumerClassId}' constructor. Provide a default value or use a more advanced container.");
}
// If we reach here, it's an untyped, required parameter without a default.
// Or potentially an unresolvable optional class dependency where null is not allowed (edge case).
throw new ContainerException("Cannot resolve required parameter \${$parameter->getName()} for '{$consumerClassId}' constructor (untyped or unresolvable complex type).");
}
/**
* Returns true if the container can return an entry for the given identifier.
* Checks explicitly set instances and if the class/interface exists.
* Does not guarantee `get()` will succeed if auto-wiring fails.
*/
public function has(string $id): bool
{
return isset($this->instances[$id]) || class_exists($id) || interface_exists($id);
}
/**
* Adds a pre-built instance or a factory/binding to the container.
* This basic version only supports pre-built instances (singletons).
*/
public function set(string $id, object $instance): void
{
// Could add support for closures/factories later if needed
$this->instances[$id] = $instance;
}
}