Skip to content

Commit

Permalink
Merge pull request #7 from young-steveo/more-container-features
Browse files Browse the repository at this point in the history
More container features
  • Loading branch information
young-steveo committed Jun 4, 2023
2 parents 3c862af + 0a81426 commit 04813c8
Show file tree
Hide file tree
Showing 30 changed files with 417 additions and 108 deletions.
60 changes: 57 additions & 3 deletions src/Cabinet/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ class Container implements \ArrayAccess, ContainerInterface
*/
protected array $providers = [];

/**
* @var array<class-string, array<callable(object): object>>
*/
protected array $decorators = [];

/**
* Container uses a resolver to instantiate services.
*/
Expand Down Expand Up @@ -63,6 +68,19 @@ public function service(string $serviceName, string|null $implementation = null)
});
}

/**
* Register a service on the container while defining its dependencies.
*
* @param class-string $serviceName
* @param class-string[] $dependencies
*/
public function serviceWith(string $serviceName, array $dependencies): void
{
$this->factory($serviceName, function (Container $container) use ($serviceName, $dependencies) {
return $container->resolver->resolveWith($serviceName, $dependencies);
});
}

/**
* Register a service factory on the container.
*
Expand Down Expand Up @@ -121,6 +139,24 @@ public function prototypeFactory(string $serviceName, \Closure $factory): void
$this->provider($serviceName, PrototypeProvider::fromFactory($factory));
}

/**
* Register a decorator on the container.
*
* Decorators are applied to the service when it is requested from the
* container for the first time.
*
* @param class-string $serviceName
* @param callable(object): object $decorator
*/
public function decorator(string $serviceName, callable $decorator): void
{
if (!isset($this->decorators[$serviceName])) {
$this->decorators[$serviceName] = [];
}

$this->decorators[$serviceName][] = $decorator;
}

/**
* ArrayAccess methods
*/
Expand Down Expand Up @@ -161,19 +197,37 @@ public function offsetUnset($offset): void
/**
* offsetGet
*
* This is where all the magic happens.
*
* @param class-string $offset
* @throws Error\InvalidKey
* @throws Error\OutOfBounds
*/
public function offsetGet($offset): mixed
{
if (!is_string($offset)) {
throw new Error\InvalidKey("Invalid key type: " . gettype($offset));
}

// Provide the instance.
$provider = $this->providers[$offset] ?? new NullProvider();
if ($instance = $provider($this)) {
return $instance;
$instance = $provider($this);
if (!$instance) {
throw new Error\OutOfBounds("No entry was found for this identifier: $offset");
}

// Apply decorators.
$decorators = $this->decorators[$offset] ?? [];
foreach ($decorators as $decorator) {
$instance = $decorator($instance);
}
throw new Error\OutOfBounds("No entry was found for this identifier: $offset");

// Remove the decorator if the provider is not a prototype.
if (!$provider instanceof PrototypeProvider) {
unset($this->decorators[$offset]);
}

return $instance;
}

/**
Expand Down
11 changes: 11 additions & 0 deletions src/Codex/ClassResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,15 @@ interface ClassResolver
* @throws Error\UnresolvableUnionType
*/
public function resolve(string|callable $className, bool $isDependency = false): object;

/**
* Resolve a class with arguments.
*
* @template T of object
* @param class-string<T> $className
* @param class-string[] $arguments
* @return T
* @throws Error\UnresolvableClass
*/
public function resolveWith(string $className, array $arguments): object;
}
54 changes: 54 additions & 0 deletions src/Codex/Resolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,60 @@ public function resolve(string|callable $className, bool $isDependency = false):
return $this->finalize($instance);
}

/**
* Resolve a class with arguments.
*
* @template T of object
* @param class-string<T> $className
* @param class-string[] $arguments
* @return T
* @throws Error\UnresolvableClass
*/
public function resolveWith(string $className, array $arguments): object
{
$image = new \ReflectionClass($className);

// If it is not instantiable, we cannot resolve it.
if (!$image->isInstantiable()) {
throw new Error\UnresolvableClass(message: $className);
}

$constructor = $image->getConstructor();

// If it has no constructor, we should just resolve it.
if ($constructor === null) {
return $this->resolve($className);
}

$parameters = $constructor->getParameters();

// If it has a constructor, but no parameters, we should just resolve it.
if (count($parameters) === 0) {
return $this->resolve($className);
}

// Since we are going to resolve the dependencies, let's first
// notify listeners that a class was requested.
$this->notify(new Event\ClassRequested($className));

// Now we can resolve the dependencies.
$dependencies = [];
foreach ($parameters as $index => $parameter) {
$argument = $arguments[$index] ?? null;
$dependencies[] = match (true) {
$argument !== null => $this->resolve($argument, isDependency: true),
$parameter->isDefaultValueAvailable() => $parameter->getDefaultValue(),
default => throw new Error\UnresolvableClass(
"$className requires a parameter at index $index, but none was provided."
)
};
}

/** @var T */
$instance = $image->newInstanceArgs($dependencies);
return $this->finalize($instance);
}

/**
* Get the class name of the parameter, or null if it is not a class.
*
Expand Down
Loading

0 comments on commit 04813c8

Please sign in to comment.