Skip to content

Commit

Permalink
#9 Added various validations
Browse files Browse the repository at this point in the history
  • Loading branch information
BlackBonjour committed Oct 23, 2024
1 parent c2d5e96 commit bfadd6c
Show file tree
Hide file tree
Showing 15 changed files with 143 additions and 75 deletions.
11 changes: 9 additions & 2 deletions src/AbstractFactory/DynamicFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
namespace BlackBonjour\ServiceManager\AbstractFactory;

use BlackBonjour\ServiceManager\Exception\ContainerException;
use BlackBonjour\ServiceManager\FactoryInterface;
use Psr\Container\ContainerInterface;

/**
* @author Erick Dyck <[email protected]>
* @since 18.09.2019
*/
class DynamicFactory implements AbstractFactoryInterface
final class DynamicFactory implements AbstractFactoryInterface
{
/**
* @inheritDoc
* @throws ContainerException
*/
public function __invoke(ContainerInterface $container, string $service, ?array $options = null)
public function __invoke(ContainerInterface $container, string $service, ?array $options = null): mixed
{
if ($this->canCreate($container, $service) === false) {
throw new ContainerException(sprintf('Cannot create service "%s"!', $service));
Expand All @@ -25,6 +27,11 @@ public function __invoke(ContainerInterface $container, string $service, ?array
$factoryClass = $service . 'Factory';
$factory = new $factoryClass();

assert(
$factory instanceof FactoryInterface || is_callable($factory),
sprintf('Dynamic factory "%s" for service "%s" is not callable!', $factoryClass, $service),
);

return $factory($container, $service, $options);
}

Expand Down
24 changes: 14 additions & 10 deletions src/AbstractFactory/ReflectionFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
* @author Erick Dyck <[email protected]>
* @since 30.09.2019
*/
class ReflectionFactory implements AbstractFactoryInterface
final class ReflectionFactory implements AbstractFactoryInterface
{
/**
* @inheritDoc
Expand All @@ -28,19 +28,20 @@ class ReflectionFactory implements AbstractFactoryInterface
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
public function __invoke(ContainerInterface $container, string $service, ?array $options = null)
public function __invoke(ContainerInterface $container, string $service, ?array $options = null): mixed
{
if ($this->canCreate($container, $service) === false) {
throw new ContainerException(sprintf('Cannot create service "%s"!', $service));
}

/** @var class-string $service */
$reflectionClass = new ReflectionClass($service);
$parameters = $reflectionClass->getConstructor()?->getParameters() ?? [];

if ($parameters) {
$resolvedParameters = array_map(
fn (ReflectionParameter $parameter) => $this->resolveParameter($parameter, $container, $service),
$parameters
fn(ReflectionParameter $parameter): mixed => $this->resolveParameter($parameter, $container, $service),
$parameters,
);

return new $service(...$resolvedParameters);
Expand Down Expand Up @@ -70,8 +71,11 @@ public function canCreate(ContainerInterface $container, string $service): bool
* @throws NotFoundExceptionInterface
* @throws ReflectionException
*/
private function resolveParameter(ReflectionParameter $parameter, ContainerInterface $container, string $service)
{
private function resolveParameter(
ReflectionParameter $parameter,
ContainerInterface $container,
string $service,
): mixed {
$reflectionType = $parameter->getType();
$type = $reflectionType instanceof ReflectionNamedType
? $reflectionType->getName()
Expand All @@ -90,8 +94,8 @@ private function resolveParameter(ReflectionParameter $parameter, ContainerInter
sprintf(
'Unable to create service "%s": Cannot resolve parameter "%s" to a class or interface!',
$service,
$parameter->getName()
)
$parameter->getName(),
),
);
}

Expand All @@ -111,8 +115,8 @@ private function resolveParameter(ReflectionParameter $parameter, ContainerInter
'Unable to create service "%s": Cannot resolve parameter "%s" using type hint "%s"!',
$service,
$parameter->getName(),
$type
)
$type,
),
);
}
}
4 changes: 1 addition & 3 deletions src/Exception/ContainerException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@
* @author Erick Dyck <[email protected]>
* @since 13.05.2019
*/
class ContainerException extends Error implements ContainerExceptionInterface
{
}
class ContainerException extends Error implements ContainerExceptionInterface {}
4 changes: 1 addition & 3 deletions src/Exception/NotFoundException.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,4 @@
* @author Erick Dyck <[email protected]>
* @since 13.05.2019
*/
class NotFoundException extends Error implements NotFoundExceptionInterface
{
}
class NotFoundException extends Error implements NotFoundExceptionInterface {}
6 changes: 3 additions & 3 deletions src/FactoryInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ interface FactoryInterface
/**
* Creates a new service.
*
* @param ContainerInterface $container A container implementing PSR-11.
* @param string $service Name of the service to create a new instance of.
* @param array|null $options Some options that can be passed to build the service.
* @param ContainerInterface $container A container implementing PSR-11.
* @param string $service Name of the service to create a new instance of.
* @param array<string|int, mixed>|null $options Some options that can be passed to build the service.
*/
public function __invoke(ContainerInterface $container, string $service, ?array $options = null);
}
2 changes: 1 addition & 1 deletion src/InvokableFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
* @author Erick Dyck <[email protected]>
* @since 23.02.2023
*/
class InvokableFactory implements FactoryInterface
final class InvokableFactory implements FactoryInterface
{
/**
* @inheritDoc
Expand Down
100 changes: 84 additions & 16 deletions src/ServiceManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,63 +19,123 @@
/**
* @author Erick Dyck <[email protected]>
* @since 13.05.2019
*
* @implements ArrayAccess<string, mixed>
*/
class ServiceManager implements ArrayAccess, ContainerInterface
{
/** @var array<string, string> */
/** @var array<AbstractFactoryInterface> */
private array $abstractFactories;

/** @var array<string, FactoryInterface|callable|class-string> */
private array $factories;

/** @var array<class-string, class-string> */
private array $invokables;

/** @var FactoryInterface[]|callable[] */
/** @var array<string, FactoryInterface|callable> */
private array $resolvedFactories = [];

/** @var array<string, mixed> */
private array $resolvedServices = [];

/** @var array<string, mixed> */
private array $services;

/**
* @param FactoryInterface[]|callable[]|string[] $factories
* @param AbstractFactoryInterface[] $abstractFactories
* @param string[] $invokables
* @param array<string, mixed> $services
* @param array<string, FactoryInterface|callable|class-string> $factories
* @param array<AbstractFactoryInterface> $abstractFactories
* @param array<string|int, class-string> $invokables
*/
public function __construct(
private array $services = [],
private array $factories = [],
private array $abstractFactories = [],
array $services = [],
array $factories = [],
array $abstractFactories = [],
array $invokables = [],
) {
$this->invokables = array_combine($invokables, $invokables);
// Validate services
foreach (array_keys($services) as $id) {
assert(is_string($id), sprintf('Service ID must be a string, "%s" given!', $id));
}

// Validate factories
foreach ($factories as $id => $factory) {
assert(is_string($id), sprintf('Service ID must be a string, "%s" given!', $id));
assert(
$factory instanceof FactoryInterface
|| is_callable($factory)
|| (is_string($factory) && class_exists($factory)),
sprintf('Invalid factory provided for service "%s"!', $id),
);
}

// Validate abstract factories
foreach ($abstractFactories as $abstractFactory) {
assert(
$abstractFactory instanceof AbstractFactoryInterface,
sprintf('Abstract factories must implement %s!', AbstractFactoryInterface::class),
);
}

// Validate invokable classes
foreach ($invokables as $invokable) {
assert(
is_string($invokable) && class_exists($invokable),
sprintf('Invokable class "%s" does not exist!', $invokable),
);
}

// Set properties
$this->abstractFactories = $abstractFactories;
$this->factories = $factories;
$this->invokables = array_combine($invokables, $invokables);
$this->services = $services;
}

public function addAbstractFactory(AbstractFactoryInterface $abstractFactory): void
{
$this->abstractFactories[] = $abstractFactory;
}

public function addFactory(string $id, string|callable $factory): void
public function addFactory(string $id, FactoryInterface|callable|string $factory): void
{
if (is_string($factory) && class_exists($factory) === false) {
throw new ContainerException(sprintf('Factory "%s" does not exist!', $factory));
}

$this->factories[$id] = $factory;
}

public function addInvokable(string $id): void
{
$this->invokables[$id] ??= $id;
if (class_exists($id) === false) {
throw new ContainerException(sprintf('Class "%s" does not exist!', $id));
}

$this->invokables[$id] = $id;
}

public function addService(string $id, $service): void
public function addService(string $id, mixed $service): void
{
$this->services[$id] = $service;
}

/**
* @param array<string|int, mixed>|null $options
*
* @throws ContainerException
*/
public function createService(string $id, ?array $options = null)
public function createService(string $id, ?array $options = null): mixed
{
try {
return $this->getFactory($id)($this, $id, $options);
} catch (Throwable $t) {
throw new ContainerException(sprintf('Service "%s" could not be created!', $id), 0, $t);
throw new ContainerException(sprintf('Service "%s" could not be created!', $id), previous: $t);
}
}

public function get(string $id)
public function get(string $id): mixed
{
if (array_key_exists($id, $this->services)) {
return $this->services[$id];
Expand Down Expand Up @@ -108,6 +168,8 @@ public function has(string $id): bool

public function offsetExists(mixed $offset): bool
{
assert(is_string($offset), 'Service ID must be of type string!');

return $this->has($offset);
}

Expand All @@ -117,16 +179,22 @@ public function offsetExists(mixed $offset): bool
*/
public function offsetGet(mixed $offset): mixed
{
assert(is_string($offset), 'Service ID must be of type string!');

return $this->get($offset);
}

public function offsetSet(mixed $offset, mixed $value): void
{
assert(is_string($offset), 'Service ID must be of type string!');

$this->addService($offset, $value);
}

public function offsetUnset(mixed $offset): void
{
assert(is_string($offset), 'Service ID must be of type string!');

$this->removeService($offset);
}

Expand All @@ -137,7 +205,7 @@ public function removeService(string $id): void
$this->invokables[$id],
$this->resolvedFactories[$id],
$this->resolvedServices[$id],
$this->services[$id]
$this->services[$id],
);
}

Expand Down
10 changes: 5 additions & 5 deletions test/AbstractFactory/ReflectionFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ public function testInvoke(): void
->expects(self::once())
->method('get')
->with(FooBar::class)
->willReturn($this->createMock(FooBar::class));
->willReturn(new FooBar('', ''));

$factory = new ReflectionFactory();

Expand All @@ -50,8 +50,8 @@ public function testInvokeUnknownService(): void
sprintf(
'Unable to create service "%s": Cannot resolve parameter "foo" using type hint "%s"!',
ClassWithoutFactory::class,
FooBar::class
)
FooBar::class,
),
);

$container = $this->createMock(ContainerInterface::class);
Expand All @@ -68,8 +68,8 @@ public function testInvokeWithNonOptionalScalarParams(): void
$this->expectExceptionMessage(
sprintf(
'Unable to create service "%s": Cannot resolve parameter "id" to a class or interface!',
ClassWithoutFactoryAndScalarTypeHint::class
)
ClassWithoutFactoryAndScalarTypeHint::class,
),
);

$container = $this->createMock(ContainerInterface::class);
Expand Down
7 changes: 3 additions & 4 deletions test/Asset/ClassWithoutDependencies.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,9 @@

namespace BlackBonjourTest\ServiceManager\Asset;

class ClassWithoutDependencies
final readonly class ClassWithoutDependencies
{
public function __construct(
public readonly int $id = 1,
) {
}
public int $id = 1,
) {}
}
11 changes: 5 additions & 6 deletions test/Asset/ClassWithoutFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,11 @@
* @author Erick Dyck <[email protected]>
* @since 30.09.2019
*/
class ClassWithoutFactory
final readonly class ClassWithoutFactory
{
public function __construct(
public readonly FooBar $foo,
public readonly array $bar,
public readonly int $baz = 123,
) {
}
public FooBar $foo,
public array $bar,
public int $baz = 123,
) {}
}
Loading

0 comments on commit bfadd6c

Please sign in to comment.