Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Refactor and test #3

Draft
wants to merge 5 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/vendor
/composer.phar
/composer.lock
/.phpunit.result.cache
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"psr/container": "^1.1"
},
"require-dev": {
"phpunit/phpunit": "^9.5"
"phpunit/phpunit": "^10.2.6"
},
"provide": {
"psr/container-implementation": "1.1.0"
Expand Down
23 changes: 23 additions & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.2/phpunit.xsd"
bootstrap="vendor/autoload.php"
cacheDirectory=".phpunit.cache"
executionOrder="depends,defects"
requireCoverageMetadata="true"
beStrictAboutCoverageMetadata="true"
beStrictAboutOutputDuringTests="true"
failOnRisky="true"
failOnWarning="true">
<testsuites>
<testsuite name="default">
<directory>test</directory>
</testsuite>
</testsuites>

<source restrictDeprecations="true" restrictNotices="true" restrictWarnings="true">
<include>
<directory>src</directory>
</include>
</source>
</phpunit>
4 changes: 1 addition & 3 deletions src/DTOs/FactoryMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

namespace Me\BjoernBuettner\DependencyInjector\DTOs;

use Me\BjoernBuettner\DependencyInjector\Mapping;

/**
* @public This class is part of the public API and may be used in clients.
*/
final class FactoryMap implements Mapping
final class FactoryMap
{
public function __construct(
public readonly string $created,
Expand Down
5 changes: 1 addition & 4 deletions src/DTOs/InterfaceMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,10 @@

namespace Me\BjoernBuettner\DependencyInjector\DTOs;

use Me\BjoernBuettner\DependencyInjector\Mapping;


/**
* @public This class is part of the public API and may be used in clients.
*/
final class InterfaceMap implements Mapping
final class InterfaceMap
{
public function __construct(
public readonly string $interface,
Expand Down
20 changes: 20 additions & 0 deletions src/DTOs/IntersectionMap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php
declare(strict_types=1);

namespace Me\BjoernBuettner\DependencyInjector\DTOs;

class IntersectionMap
{
/**
* @var string[]
*/
public readonly array $intersectionMap;

public function __construct(
public readonly string $className,
string ...$intersectionMap,
) {
sort($intersectionMap, SORT_STRING);
$this->intersectionMap = $intersectionMap;
}
}
4 changes: 1 addition & 3 deletions src/DTOs/ParameterMap.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,10 @@

namespace Me\BjoernBuettner\DependencyInjector\DTOs;

use Me\BjoernBuettner\DependencyInjector\Mapping;

/**
* @public This class is part of the public API and may be used in clients.
*/
final class ParameterMap implements Mapping
final class ParameterMap
{
public function __construct(
public readonly string $parameter,
Expand Down
133 changes: 67 additions & 66 deletions src/DependencyBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,24 @@

use Me\BjoernBuettner\DependencyInjector\DTOs\FactoryMap;
use Me\BjoernBuettner\DependencyInjector\DTOs\InterfaceMap;
use Me\BjoernBuettner\DependencyInjector\DTOs\IntersectionMap;
use Me\BjoernBuettner\DependencyInjector\DTOs\ParameterMap;
use Me\BjoernBuettner\DependencyInjector\Exceptions\InvalidEnvironment;
use Me\BjoernBuettner\DependencyInjector\Exceptions\NotInEnvironment;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UninstanciableClass;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UninvokableMethod;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UnresolvableClass;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UnresolvableMap;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UnresolvableMethod;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UnresolvableParameter;
use Me\BjoernBuettner\DependencyInjector\Exceptions\UnresolvableRecursion;
use Me\BjoernBuettner\DependencyInjector\Factories\Environment;
use Me\BjoernBuettner\DependencyInjector\Factories\Reflection;
use Me\BjoernBuettner\DependencyInjector\Factories\Type;
use Psr\Container\ContainerInterface;
use ReflectionClass;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;
use ReflectionException;
use ReflectionMethod;
use ReflectionParameter;
use Symfony\Component\String\UnicodeString;

/**
* @public This class is the entry point of the dependency injection container.
Expand All @@ -46,14 +49,20 @@ final class DependencyBuilder implements ContainerInterface
* @var array<string, string>
*/
private array $factories = [];

/**
* @var array<int, bool>
*/
private array $building = [];
private EnvironmentAccess $environment;

/**
* @var array<int, string>
* @var array<string, string>
*/
private array $environment = [];
private array $intersections = [];
private ReflectionFactory $reflectionFactory;
private TypeFactory $typeFactory;
private LoggerInterface $logger;

/**
* @param array<int, string> $environment
Expand All @@ -62,41 +71,28 @@ final class DependencyBuilder implements ContainerInterface
*/
public function __construct(
?array $environment = null,
bool $validateOnConstruct = false,
Mapping ...$maps,
?ReflectionFactory $reflectionFactory = null,
?TypeFactory $typeFactory = null,
LoggerInterface $logger = null,
ParameterMap|InterfaceMap|FactoryMap|IntersectionMap ...$maps,
) {
foreach (($environment ?? $_ENV) as $key => $value) {
if (!is_string($key) || !is_string($value)) {
throw new InvalidEnvironment("Environment must be an array of string keys and values.");
}
if (!preg_match('/^[A-Z][A-Z0-9_]*$/', $key)) {
throw new InvalidEnvironment("Environment key $key must be upper snake case.");
}
$this->environment[(new UnicodeString($key))->lower()->camel()->toString()] = $value;
}
$this->typeFactory = $typeFactory ?? new Type();
$this->reflectionFactory = $reflectionFactory ?? new Reflection();
$this->logger = $logger ?? new NullLogger();
$this->environment = new Environment($environment);
foreach ($maps as $map) {
if ($map instanceof ParameterMap) {
if ($validateOnConstruct && !class_exists($map->class)) {
throw new UnresolvableClass("Class {$map->class} does not exist.");
}
$this->parameters[$map->class . '.' . $map->parameter] = $map->value;
continue;
}
if ($map instanceof InterfaceMap) {
if ($validateOnConstruct && !class_exists($map->implementation)) {
throw new UnresolvableClass("Class {$map->implementation} does not exist.");
}
$this->interfaces[$map->interface] = $map->implementation;
continue;
}
if ($map instanceof FactoryMap) {
if ($validateOnConstruct && !class_exists($map->factory)) {
throw new UnresolvableClass("Class {$map->factory} does not exist.");
}
$this->factories[$map->created] = [$map->factory, $map->method];
continue;
}
throw new UnresolvableMap('Unknown map type ' . get_class($map));
if ($map instanceof IntersectionMap) {
$this->intersections[implode('&', $map->intersectionMap)] = $map->className;
}
}
}

Expand All @@ -111,34 +107,33 @@ private function getParamValue(ReflectionParameter $param, array $variables, str
if (isset($variables[$key])) {
return $variables[$key];
}
if ($type = $param->getType()) {
if (!$type->isBuiltin()) {
return $this->build($type->getName());
}
if (isset($this->environment[$key])) {
return match ($param->getType()->getName()) {
'int' => (int)$this->environment[$key],
'float' => (float)$this->environment[$key],
'bool' => $this->environment[$key] === 'true',
'array' => explode(',', $this->environment[$key]),
default => (string)$this->environment[$key],
};
$parameter = $this->typeFactory->parameter($param);
if ($classes = $parameter->getClasses()) {
if (isset($this->intersections[implode('&', $classes)])) {
return $this->build($this->intersections[implode('&', $classes)]);
}
if (isset($this->environment[$param->getName()])) {
return match ($param->getType()->getName()) {
'int' => (int)$this->environment[$param->getName()],
'float' => (float)$this->environment[$param->getName()],
'bool' => $this->environment[$param->getName()] === 'true',
'array' => explode(',', $this->environment[$param->getName()]),
default => (string)$this->environment[$param->getName()],
};
foreach ($classes as $class) {
try {
$object = $this->build($class);
if ($parameter->areRequirementsMet($object)) {
return $object;
}
} catch (UnresolvableClass) {
}
}
if ($type->allowsNull()) {
return null;
}
if (!in_array($parameter->getBasicType(), ['object', 'callable', 'resource'], true)) {
try {
return $this->environment->get($parameter->getBasicType(), $key, $param->getName());
} catch (NotInEnvironment) {
// ignore
}
}
if ($param->isDefaultValueAvailable()) {
return $param->getDefaultValue();
if (!$parameter->hasDefault()) {
return $parameter->getDefault();
}
if ($parameter->isNullable()) {
return null;
}
throw new UnresolvableParameter("Cannot resolve parameter {$param->getName()}.");
}
Expand All @@ -165,7 +160,7 @@ public function build(string $class): object
return $this->cache[$class] = $this->build($this->interfaces[$class]);
}
try {
$rc = new ReflectionClass($class);
$rc = $this->reflectionFactory->class($class);
} catch (ReflectionException $e) {
throw new UnresolvableClass("Cannot resolve class $class.", 0, $e);
} finally {
Expand All @@ -185,12 +180,8 @@ public function build(string $class): object
}
}
}
$params = $constructor->getParameters();
$this->building[$class] = true;
$args = [];
foreach ($params as $param) {
$args[] = $this->getParamValue($param, $this->parameters, $class . '.' . $param->getName());
}
$args = $this->getArguments($constructor->getParameters(), $this->parameters);
unset($this->building[$class]);
try {
return $this->cache[$class] = $rc->newInstanceArgs($args);
Expand All @@ -212,22 +203,32 @@ public function call(string $class, string $method, array $variables = []): mixe
{
$object = $this->build($class);
try {
$rm = new ReflectionMethod($object, $method);
$rm = $this->reflectionFactory->method($object, $method);
} catch (ReflectionException $e) {
throw new UnresolvableMethod("Cannot resolve method $class::$method.", 0, $e);
}
$params = $rm->getParameters();
$args = [];
foreach ($params as $param) {
$args[] = $this->getParamValue($param, $variables, $param->getName());
}
$args = $this->getArguments($rm->getParameters(), $variables);
try {
return $rm->invokeArgs($object, $args);
} catch (ReflectionException $e) {
throw new UninvokableMethod("Cannot invoke method $class::$method.", 0, $e);
}
}

/**
* @param array<int, ReflectionParameter> $parameters
* @param array<string, mixed> $variables
* @return array<int, mixed>
*/
private function getArguments(array $parameters, array $variables): array
{
$args = [];
foreach ($parameters as $param) {
$args[] = $this->getParamValue($param, $variables, $param->getName());
}
return $args;
}

/**
* @throws UnresolvableParameter
* @throws UnresolvableClass
Expand Down
8 changes: 8 additions & 0 deletions src/EnvironmentAccess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Me\BjoernBuettner\DependencyInjector;

interface EnvironmentAccess
{
public function get(string $type, string ...$keys): mixed;
}
11 changes: 11 additions & 0 deletions src/Exceptions/NotInEnvironment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);

namespace Me\BjoernBuettner\DependencyInjector\Exceptions;

use UnexpectedValueException;

class NotInEnvironment extends UnexpectedValueException
{

}
12 changes: 0 additions & 12 deletions src/Exceptions/UnresolvableMap.php

This file was deleted.

Loading