Git viewing holonet/common / 5e03f43b33fa4c36009ba1c479a1f1d953d8c3a4


Filter

5e03f43b33fa4c36009ba1c479a1f1d953d8c3a4

Matthias Lantsch(1 year, 1 month ago)

Update di container to allow for Provider classes

Browse Files
  • Changed file phpunit.xml
    diff --git a/20dd276382c6594baa54d3f351f845a1e9e7cfa3 b/b745ee6ce22cea66c952365cfe9f617b9fdb429a
    index 20dd276..b745ee6 100644
    --- a/20dd276382c6594baa54d3f351f845a1e9e7cfa3
    +++ b/b745ee6ce22cea66c952365cfe9f617b9fdb429a
    @@ -1,5 +1,14 @@
     <?xml version="1.0"?>
    -<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" bootstrap="vendor/autoload.php" colors="true" xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd" cacheDirectory=".phpunit.cache" requireCoverageMetadata="true">
    +<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    +         bootstrap="vendor/autoload.php"
    +         colors="true"
    +         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.1/phpunit.xsd"
    +         cacheDirectory=".phpunit.cache"
    +         requireCoverageMetadata="true"
    +         displayDetailsOnTestsThatTriggerWarnings="true"
    +         displayDetailsOnTestsThatTriggerDeprecations="true"
    +         displayDetailsOnTestsThatTriggerErrors="true"
    +         displayDetailsOnTestsThatTriggerNotices="true">
       <testsuite name="tests">
         <directory>./tests</directory>
       </testsuite>
  • Changed file psalm.xml
    diff --git a/30ad2403ea728b4eea538c6142922c8b26f14411 b/d97637caf43966a4293e98526aa8804762fe43a2
    index 30ad240..d97637c 100644
    --- a/30ad2403ea728b4eea538c6142922c8b26f14411
    +++ b/d97637caf43966a4293e98526aa8804762fe43a2
    @@ -3,7 +3,8 @@
     	errorLevel="4"
     	totallyTyped="false"
     	reportMixedIssues="false"
    -	resolveFromConfigFile="true"
    +  findUnusedPsalmSuppress="true"
    +  findUnusedBaselineEntry="true"
     	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     	xmlns="https://getpsalm.org/schema/config"
     	xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
  • Changed file PhpConfigParser.php
    diff --git a/12c8b270f22e436b35c5abeddcd64407d7f84e32 b/d3f70e5daf08940bf4c7d26de8747964655fb1fb
    index 12c8b27..d3f70e5 100644
    --- a/12c8b270f22e436b35c5abeddcd64407d7f84e32
    +++ b/d3f70e5daf08940bf4c7d26de8747964655fb1fb
    @@ -24,6 +24,7 @@ class PhpConfigParser extends AbstractParser {
     		 */
     		$ret = require $filename;
     		//either the user sets a variable called "config" or returns an array
    +		/** @psalm-suppress UndefinedVariable */
     		if (!isset($config) && ($config = $ret) !== 1) {
     			throw new FileAccessException("Could not parse php config file '{$filename}'; File must either return an array or define the variable \$config");
     		}
  • Created new file Compiler.php
    <?php
    /**
     * This file is part of the holonet common library
     * (c) Matthias Lantsch.
     *
     * @license http://opensource.org/licenses/gpl-license.php  GNU Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\di;
    
    use holonet\common\di\autowire\provider\ConfigAutoWireProvider;
    use holonet\common\di\autowire\provider\ContainerAutoWireProvider;
    use holonet\common\di\autowire\provider\ForwardAutoWireProvider;
    use holonet\common\di\autowire\provider\ParamAutoWireProvider;
    use holonet\common\di\Container;
    use ReflectionClass;
    use Psr\Container\ContainerInterface;
    use holonet\common\di\autowire\AutoWire;
    use holonet\common\config\ConfigRegistry;
    use holonet\common\di\autowire\AutoWireException;
    use ReflectionFunctionAbstract;
    use ReflectionIntersectionType;
    use ReflectionNamedType;
    use ReflectionObject;
    use ReflectionParameter;
    use ReflectionUnionType;
    use function holonet\common\get_class_short;
    
    /**
     * Compile a static php anonymous class which can create all the current definitions in the container without reflection.
     */
    class Compiler {
    
    	/**
    	 * @var array<string, string> $aliases key mapping with all available services on the container
    	 */
    	protected array $aliases = array();
    
    	/**
    	 * @var ParamAutoWireProvider[]
    	 */
    	protected array $paramProviders;
    
    	/**
    	 * @var array<string, array{string, array}> $wiring Wiring information on how to make certain types of objects.
    	 * Mapped by name / type => class abstract (array with class name and parameters).
    	 */
    	protected array $wiring = array();
    
    	public function __construct(protected Container $container) {
    		$reflection = new ReflectionObject($this->container);
    
    		$this->aliases = $reflection->getProperty('aliases')->getValue($container);
    		$this->wiring = $reflection->getProperty('wiring')->getValue($container);
    
    		$this->paramProviders = array(
    			new ForwardAutoWireProvider(),
    			new ConfigAutoWireProvider(),
    			new ContainerAutoWireProvider(),
    		);
    	}
    
    	public function compile(): string {
    		$methods[] = $this->compileMakeMethod();
    
    		foreach ($this->wiring as $alias => $constructor) {
    			$methods[] = $this->compileWiringMakeMethod($alias);
    		}
    
    		$methods = implode("\n\t", explode("\n", implode("\n\n", $methods)));
    
    		return <<<PHP
    		if (!isset(\$config) || !\$config instanceof \holonet\common\config\ConfigRegistry) {
    			throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class(\$config) extends \holonet\common\di\Container {
    			{$methods}
    		};
    		PHP;
    	}
    
    	/**
    	 * Compile a static version of the make() method of a container
    	 */
    	private function compileMakeMethod(): string {
    		$matchStatements = array();
    
    		foreach ($this->wiring as $abstract => $constructor) {
    			$matchKey = "{$abstract}::class";
    			if (array_key_exists($abstract, $this->aliases)) {
    				$matchKey = "'{$abstract}'";
    			}
    			$matchStatements[] = "\t\t{$matchKey} => \$this->{$this->serviceMakeMethodName($abstract)}()";
    		}
    
    		$matchStatements[] = "\t\tdefault => parent::make(\$abstract, \$extraParams)";
    		$matchStatements = implode(",\n", $matchStatements);
    		return <<<PHP
    		public function make(string \$abstract, array \$extraParams = array()): object {
    			return match (\$abstract) {
    		$matchStatements
    			};
    		}
    		PHP;
    	}
    
    	private function serviceMakeMethodName(string $alias): string {
    		return sprintf('make_%s', str_replace('\\', '_', $alias));
    	}
    
    	private function compileWiringMakeMethod(string $alias): string {
    		list($class, $params) = $this->wiring[$alias];
    
    		return <<<PHP
    		public function {$this->serviceMakeMethodName($alias)}(): {$class} {
    			return {$this->compileNewStatement($class, $params)};
    		}
    		PHP;
    	}
    
    	private function compileNewStatement(string $class, array $params): string {
    		$reflection = new ReflectionClass($class);
    		$constructor = $reflection->getConstructor();
    		if ($constructor === null) {
    			if (!empty($params)) {
    				AutoWireException::failNoConstructor($reflection, $params);
    			}
    
    			return "new $class()";
    		}
    
    		$params = $this->compileAutoWiring($constructor, $params);
    
    		return sprintf('new %s(%s)', $class, implode(', ', $params));
    	}
    
    	protected function compileAutoWiring(ReflectionFunctionAbstract $method, array $givenParams): array {
    		$parameters = $method->getParameters();
    		$compiled = array();
    		foreach ($parameters as $param) {
    			$compiledValue = $this->compileParameter($param, $givenParams[$param->getName()] ?? null);
    			if ($compiledValue !== null) {
    				$compiled[] = "{$param->getName()}: {$compiledValue}";
    			}
    		}
    
    		return $compiled;
    	}
    
    	private function compileParameter(ReflectionParameter $param, mixed $paramValue): ?string {
    		$paramType = $param->getType();
    
    		if ($paramType instanceof ReflectionIntersectionType) {
    			AutoWireException::failParam($param, 'Cannot auto-wire intersection types');
    		}
    
    		if ($paramType === null) {
    			if ($param->isOptional()) {
    				return null;
    			}
    
    			AutoWireException::failParam($param, 'Can only auto-wire typed parameters');
    		}
    
    		if ($paramType instanceof ReflectionUnionType) {
    			return $this->compileUnionType($param, $paramType, $paramValue);
    		}
    
    		return $this->compileNamedType($param, $paramType, $paramValue);
    	}
    
    	private function compileNamedType(ReflectionParameter $param, ReflectionNamedType $type, mixed $paramValue): ?string {
    		foreach ($this->paramProviders as $provider) {
    			$wiredValue = $provider->provide($this->container, $param, $type, $paramValue);
    
    			if ($wiredValue !== null) {
    				return $provider->compile($param, $type, $paramValue);
    			}
    		}
    
    		if ($param->isOptional()) {
    			return null;
    		}
    
    		if ($param->allowsNull()) {
    			return 'null';
    		}
    
    		AutoWireException::failParam($param, "Cannot auto-wire to type '{$type->getName()}'");
    	}
    
    	private function compileUnionType(ReflectionParameter $param, ReflectionUnionType $type, mixed $paramValue): ?string {
    		$types = $type->getTypes();
    		$errors = array();
    
    		foreach ($this->paramProviders as $provider) {
    			foreach ($types as $type) {
    				try {
    					$wiredValue = $provider->provide($this->container, $param, $type, $paramValue);
    
    					if ($wiredValue !== null) {
    						return $provider->compile($param, $type, $paramValue);
    					}
    				} catch (DependencyInjectionException $e) {
    					$errors[$type->getName()] = $e->getMessage();
    				}
    			}
    		}
    
    		$unionType = implode('|', array_map(fn ($type) => $type->getName(), $types));
    		AutoWireException::failParam($param, "Cannot auto-wire to union type '{$unionType}'");
    	}
    
    }
  • Changed file Container.php
    diff --git a/9d2d0de62cba14d97a5b48dac1a4873aa0f5e715 b/be4c710d5864027f3fe7b195fbe969c9315b6ba7
    index 9d2d0de..be4c710 100644
    --- a/9d2d0de62cba14d97a5b48dac1a4873aa0f5e715
    +++ b/be4c710d5864027f3fe7b195fbe969c9315b6ba7
    @@ -42,7 +42,7 @@ class Container implements ContainerInterface {
     	protected array $recursionPath = array();
    
     	/**
    -	 * @var array<string, array{string, array<string, array>}> $wiring Wiring information on how to make certain types of objects.
    +	 * @var array<string, array{string, array}> $wiring Wiring information on how to make certain types of objects.
     	 * Mapped by name / type => class abstract (array with class name and parameters).
     	 */
     	protected array $wiring = array();
    @@ -55,6 +55,8 @@ class Container implements ContainerInterface {
     	 * @template T
     	 * @param class-string<T> $class
     	 * @return T
    +	 * @psalm-suppress InvalidReturnType
    +	 * @psalm-suppress InvalidReturnStatement
     	 */
     	public function byType(string $class, ?string $id = null): object {
     		$keys = array_keys($this->aliases, $class);
    @@ -111,6 +113,8 @@ class Container implements ContainerInterface {
     	 * @template T
     	 * @param class-string<T>|string $abstract
     	 * @return T
    +	 * @psalm-suppress InvalidReturnType
    +	 * @psalm-suppress InvalidReturnStatement
     	 */
     	public function make(string $abstract, array $extraParams = array()): object {
     		if ($this->has($abstract)) {
    @@ -147,7 +151,11 @@ class Container implements ContainerInterface {
     			$reflection = new ReflectionClass($value);
     			$factoryMethod = $reflection->getMethod('make');
    
    -			$this->aliases[$id] = $factoryMethod->getReturnType()->getName();
    +			$returnType = $factoryMethod->getReturnType();
    +			if (!$returnType instanceof \ReflectionNamedType || $returnType->getName() === 'object') {
    +				throw new DependencyInjectionException("Provider factory method {$reflection->getName()}::make() has no return type");
    +			}
    +			$this->aliases[$id] = $returnType->getName();
     			$this->wire($reflection->getName(), $params, $id);
    
     			return;
    @@ -164,7 +172,11 @@ class Container implements ContainerInterface {
     		if (class_exists($value)) {
     			$this->aliases[$id] = $value;
     			$this->wire($value, $params, $id);
    +
    +			return;
     		}
    +
    +		throw new DependencyInjectionException("Could not set dependency '{$id}': value is not an object or class name");
     	}
    
     	/**
    @@ -181,7 +193,11 @@ class Container implements ContainerInterface {
     			$reflection = new ReflectionClass($class);
     			$factoryMethod = $reflection->getMethod('make');
    
    -			$abstract = $factoryMethod->getReturnType()->getName();
    +			$returnType = $factoryMethod->getReturnType();
    +			if (!$returnType instanceof \ReflectionNamedType || $returnType->getName() === 'object') {
    +				throw new DependencyInjectionException("Provider factory method {$class}::make() has no return type");
    +			}
    +			$abstract = $returnType->getName();
     		}
    
     		$abstract ??= $class;
    @@ -190,12 +206,6 @@ class Container implements ContainerInterface {
     	}
    
     	protected function instance(string $class, array $params): object {
    -		if (is_a($class, Provider::class, true)) {
    -			$provider = new $class($this);
    -
    -			return $provider->make();
    -		}
    -
     		$reflection = new ReflectionClass($class);
     		$constructor = $reflection->getConstructor();
     		if ($constructor === null) {
    @@ -203,11 +213,17 @@ class Container implements ContainerInterface {
     				AutoWireException::failNoConstructor($reflection, $params);
     			}
    
    -			return new $class();
    +			$result = new $class();
    +		} else {
    +			$params = $this->autoWiring->autoWire($constructor, $params);
    +
    +			$result = new $class(...$params);
     		}
    
    -		$params = $this->autoWiring->autoWire($constructor, $params);
    +		if ($result instanceof Provider) {
    +			return $result->make();
    +		}
    
    -		return new $class(...$params);
    +		return $result;
     	}
     }
  • Created new file Factory.php
    <?php
    /**
     * This file is part of the holonet common library
     * (c) Matthias Lantsch.
     *
     * @license http://opensource.org/licenses/gpl-license.php  GNU Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\di;
    
    use holonet\common\di\discovery\ConfigDependencyDiscovery;
    use holonet\common\di\discovery\DependencyDiscovery;
    use holonet\common\error\BadEnvironmentException;
    use holonet\common\config\ConfigRegistry;
    
    /**
     * Factory class that is supposed to initialise a container based on a configuration.
     */
    class Factory {
    
    	/**
    	 * @var DependencyDiscovery[] $discoverers
    	 */
    	protected array $discoverers = array();
    
    	public function __construct(protected ConfigRegistry $registry = new ConfigRegistry()) {
    		$this->discoverers[] = new ConfigDependencyDiscovery();
    	}
    
    	public function make(): Container {
    		if ($this->registry->has('di.cache_path')) {
    			return $this->makeCompiledContainer();
    		} else {
    			return $this->makeContainer();
    		}
    	}
    
    	private function makeCompiledContainer(): Container {
    		$cacheFile = $this->cacheFilePath();
    		$config = $this->registry;
    
    		if (file_exists($cacheFile)) {
    			return require $cacheFile;
    		}
    
    		$container = $this->makeContainer();
    		$compiler = new Compiler($container);
    
    		file_put_contents($cacheFile, "<?php\n\n{$compiler->compile()}");
    		return require $cacheFile;
    	}
    
    	private function makeContainer(): Container {
    		$container = new Container($this->registry);
    		foreach ($this->discoverers as $discoverer) {
    			$discoverer->discover($container);
    		}
    		return $container;
    	}
    
    	private function cacheFilePath(): string {
    		$dir = $this->registry->get('di.cache_path');
    
    		if (!is_dir($dir) || !is_writable($dir)) {
    			throw new BadEnvironmentException("Container compile path '{$dir}' is not a writable directory");
    		}
    
    		return "{$dir}/container.php";
    	}
    
    }
  • Changed file ConfigAutoWireProvider.php
    diff --git a/1a126f451e855c2371179dd5255b9d1090e46acb b/a804f82c5c539efd54b028000cf3e16af39cc9f2
    index 1a126f4..a804f82 100644
    --- a/1a126f451e855c2371179dd5255b9d1090e46acb
    +++ b/a804f82c5c539efd54b028000cf3e16af39cc9f2
    @@ -9,6 +9,7 @@
    
     namespace holonet\common\di\autowire\provider;
    
    +use LogicException;
     use ReflectionNamedType;
     use ReflectionParameter;
     use holonet\common\di\Container;
    @@ -51,4 +52,26 @@ class ConfigAutoWireProvider implements ParamAutoWireProvider {
    
     		return $container->registry->get($configKey);
     	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function compile(ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): string {
    +		$expectedType = $type->getName();
    +
    +		// if we got called here, it means provide() has returned something, so we can assume the attribute exists
    +		$attribute = reflection_get_attribute($param, ConfigItem::class) ?? throw new LogicException();
    +
    +		$configKey = ($givenParam ?? $attribute->key);
    +
    +		if (class_exists($expectedType)) {
    +			if ($attribute->verified) {
    +				return "\$this->registry->verifiedDto('{$configKey}', '{$expectedType}')";
    +			}
    +
    +			return "\$this->registry->asDto('{$configKey}', '{$expectedType}')";
    +		}
    +
    +		return "\$this->registry->get('{$configKey}')";
    +	}
     }
  • Changed file ContainerAutoWireProvider.php
    diff --git a/6a282d4f2dffcb87679baa7707d4897cacdb0101 b/7eb6b88995623024b1d501e53ef2d1330804cf11
    index 6a282d4..7eb6b88 100644
    --- a/6a282d4f2dffcb87679baa7707d4897cacdb0101
    +++ b/7eb6b88995623024b1d501e53ef2d1330804cf11
    @@ -31,4 +31,11 @@ class ContainerAutoWireProvider implements ParamAutoWireProvider {
    
     		return null;
     	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function compile(ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): string {
    +		return "\$this->byType('{$type->getName()}', '{$param->getName()}')";
    +	}
     }
  • Changed file ForwardAutoWireProvider.php
    diff --git a/555c5d6d9eb50e9f35ffde61feacc50edec78d73 b/f8b50ee3291c2b3768e60a07e677d6cbe20322a4
    index 555c5d6..f8b50ee 100644
    --- a/555c5d6d9eb50e9f35ffde61feacc50edec78d73
    +++ b/f8b50ee3291c2b3768e60a07e677d6cbe20322a4
    @@ -31,4 +31,11 @@ class ForwardAutoWireProvider implements ParamAutoWireProvider {
    
     		return null;
     	}
    +
    +	/**
    +	 * {@inheritDoc}
    +	 */
    +	public function compile(ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): string {
    +		return var_export($givenParam, true);
    +	}
     }
  • Changed file ParamAutoWireProvider.php
    diff --git a/33fe60f2a1e8e2d233c5d65676569b4b2c649a0f b/e449491089da5dcec21766b56311e0e68ef8a203
    index 33fe60f..e449491 100644
    --- a/33fe60f2a1e8e2d233c5d65676569b4b2c649a0f
    +++ b/e449491089da5dcec21766b56311e0e68ef8a203
    @@ -31,4 +31,11 @@ interface ParamAutoWireProvider {
     	 * If it is appropriate to return a value but it can't it should throw an exception instead.
     	 */
     	public function provide(Container $container, ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): mixed;
    +
    +	/**
    +	 * Return a php code string representation of getting the autowired parameter value.
    +	 * This is used to compile the static container class.
    +	 * Assume that $this in the code refers to the container object.
    +	 */
    +	public function compile(ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): string;
     }
  • Created new file ConfigDependencyDiscovery.php
    <?php
    /**
     * This file is part of the holonet common library
     * (c) Matthias Lantsch.
     *
     * @license http://opensource.org/licenses/gpl-license.php  GNU Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\di\discovery;
    
    use holonet\common\di\Container;
    use holonet\common\error\BadEnvironmentException;
    
    /**
     * Read auto-wiring definitions from the config registry.
     */
    class ConfigDependencyDiscovery implements DependencyDiscovery {
    
    	public function discover(Container $container): void {
    		if (is_array($services = $container->registry->get('di.services'))) {
    			foreach ($services as $service => $abstract) {
    				if (is_string($abstract)) {
    					$abstract = array($abstract);
    				}
    
    				$abstract[1] ??= array();
    				if (!$this->validateAbstract($abstract)) {
    					throw BadEnvironmentException::faultyConfig("di.services.{$service}", 'Abstract must be class name or array with class name and parameters');
    				}
    
    				list($class, $params) = $abstract;
    				$container->set($service, $class, $params);
    			}
    		}
    
    		if (is_array($classWirings = $container->registry->get('di.auto_wire'))) {
    			foreach ($classWirings as $name => $abstract) {
    				if (is_string($abstract)) {
    					$abstract = array($abstract);
    				}
    
    				$abstract[1] ??= array();
    				if (!$this->validateAbstract($abstract)) {
    					throw BadEnvironmentException::faultyConfig("di.auto_wire.{$name}", 'Abstract must be class name or array with class name and parameters');
    				}
    
    				list($class, $params) = $abstract;
    
    				if (!is_string($name)) {
    					$name = null;
    				}
    				$container->wire($class, $params, $name);
    			}
    		}
    	}
    
    	private function validateAbstract(mixed $abstract): bool {
    		// validate form and size of abstract array
    		if(!is_array($abstract) || count($abstract) > 2 || !array_is_list($abstract)) {
    			return false;
    		}
    
    		// validate class name in the abstract array
    		if (!is_string($abstract[0]) && !is_object($abstract[0])) {
    			return false;
    		}
    
    		// validate parameters in the abstract array
    		if (!is_array($abstract[1])) {
    			return false;
    		}
    
    		return true;
    	}
    }
  • Created new file DependencyDiscovery.php
    <?php
    /**
     * This file is part of the holonet common library
     * (c) Matthias Lantsch.
     *
     * @license http://opensource.org/licenses/gpl-license.php  GNU Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\di\discovery;
    
    use holonet\common\di\Container;
    
    interface DependencyDiscovery {
    
    	/**
    	 * Discover auto-wiring definitions for the Container.
    	 */
    	public function discover(Container $container): void;
    
    }
  • Changed file ErrorHandler.php
    diff --git a/63f09dabbe2c79b34b800a12ce7f9c89b55282d6 b/434a329dd0e23a7131f7862de73bb4563cc1bd72
    index 63f09da..434a329 100644
    --- a/63f09dabbe2c79b34b800a12ce7f9c89b55282d6
    +++ b/434a329dd0e23a7131f7862de73bb4563cc1bd72
    @@ -60,11 +60,11 @@ class ErrorHandler {
     			return null;
     		}
    
    -		list('type' => $type, 'name' => $name) = (self::ERROR_LEVEL_LOOKUP[$errno] ?? self::ERROR_LEVEL_LOOKUP[\E_ERROR]);
    +		list('level' => $level, 'name' => $name) = (self::ERROR_LEVEL_LOOKUP[$errno] ?? self::ERROR_LEVEL_LOOKUP[\E_ERROR]);
    
     		if ($this->logger !== null) {
     			$this->logger->log(
    -				$type,
    +				$level,
     				"{$name}: {$msg}",
     				array(
     					'code' => $errno,
  • Changed file functions.php
    diff --git a/4f404341265bc36f5fae637c5c92e9f2c3559f7c b/a3a91deda75f58c115b6a114421878067ab6ab24
    index 4f40434..a3a91de 100644
    --- a/4f404341265bc36f5fae637c5c92e9f2c3559f7c
    +++ b/a3a91deda75f58c115b6a114421878067ab6ab24
    @@ -141,23 +141,6 @@ if (!function_exists(__NAMESPACE__.'\\indentText')) {
     	}
     }
    
    -if (!function_exists(__NAMESPACE__.'\\isAssoc')) {
    -	/**
    -	 * function used to check if an array is associative.
    -	 * @param array $arr The array to check
    -	 * @return bool true or false on is associative or not
    -	 */
    -	function isAssoc(array $arr): bool {
    -		if ($arr === array()) {
    -			return false;
    -		}
    -		ksort($arr);
    -
    -		/** @psalm-suppress DocblockTypeContradiction */
    -		return array_keys($arr) !== range(0, count($arr) - 1);
    -	}
    -}
    -
     if (!function_exists(__NAMESPACE__.'\\readableDurationString')) {
     	/**
     	 * function used to transform a duration into a human readable string.
    @@ -184,7 +167,7 @@ if (!function_exists(__NAMESPACE__.'\\readableDurationString')) {
     				return $time / 60 .'min';
     			}
    
    -			return (int)($time / 60).'min '.(int)($time % 60).'s';
    +			return (int)($time / 60).'min '. $time % 60 .'s';
     		}
    
     		return $time.'s';
  • Created new file container.php
    <?php
    
    if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    	throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    }
    
    return new class($config) extends \holonet\common\di\Container {
    	public function make(string $abstract, array $extraParams = array()): object {
    		return match ($abstract) {
    			'service1' => $this->make_service1(),
    			default => parent::make($abstract, $extraParams)
    		};
    	}
    
    	public function make_service1(): holonet\common\tests\di\DiAnonDep {
    		return new holonet\common\tests\di\DiAnonDep();
    	}
    };
  • Created new file CompilerTest.php
    <?php
    /**
     * This file is part of the hdev common library package
     * (c) Matthias Lantsch.
     *
     * @license http://www.wtfpl.net/ Do what the fuck you want Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\tests\di;
    
    use Countable;
    use FilesystemIterator;
    use holonet\common\config\ConfigRegistry;
    use holonet\common\di\autowire\provider\ConfigAutoWireProvider;
    use holonet\common\di\autowire\provider\ContainerAutoWireProvider;
    use holonet\common\di\autowire\provider\ForwardAutoWireProvider;
    use holonet\common\di\Compiler;
    use holonet\common\di\Factory;
    use holonet\common\Noun;
    use Stringable;
    use PHPUnit\Framework\TestCase;
    use holonet\common\di\Container;
    use holonet\common\di\autowire\AutoWire;
    use PHPUnit\Framework\Attributes\CoversClass;
    use holonet\common\di\autowire\AutoWireException;
    use holonet\common\di\DependencyNotFoundException;
    use holonet\common\di\DependencyInjectionException;
    
    #[CoversClass(Container::class)]
    #[CoversClass(Compiler::class)]
    #[CoversClass(AutoWire::class)]
    #[CoversClass(AutoWireException::class)]
    #[CoversClass(ConfigAutoWireProvider::class)]
    #[CoversClass(ContainerAutoWireProvider::class)]
    #[CoversClass(ForwardAutoWireProvider::class)]
    class CompilerTest extends TestCase
    {
    	public function testParameterForwardCompile(): void
    	{
    		$container = new Container();
    
    		$params = array(
    			'string' => 'gojsdgoisjdgio',
    			'int' => 5,
    			'float' => 10.5,
    			'boolean' => true,
    			'array' => array('value1', 'value2')
    		);
    		$container->wire(DependencyForwardAutoWire::class, $params);
    
    		$compiler = new Compiler($container);
    
    		$compiled = <<<'COMPILED'
    		if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    			throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class($config) extends \holonet\common\di\Container {
    			public function make(string $abstract, array $extraParams = array()): object {
    				return match ($abstract) {
    					holonet\common\tests\di\DependencyForwardAutoWire::class => $this->make_holonet_common_tests_di_DependencyForwardAutoWire(),
    					default => parent::make($abstract, $extraParams)
    				};
    			}
    
    			public function make_holonet_common_tests_di_DependencyForwardAutoWire(): holonet\common\tests\di\DependencyForwardAutoWire {
    				return new holonet\common\tests\di\DependencyForwardAutoWire(string: 'gojsdgoisjdgio', int: 5, float: 10.5, boolean: true, array: array (
    		 	  0 => 'value1',
    		 	  1 => 'value2',
    			));
    			}
    		};
    		COMPILED;
    		$actual = $compiler->compile();
    
    		$this->assertEqualsIgnoringIndentation($compiled, $actual);
    		$this->assertValidCompiledContainer($actual, $container->registry);
    	}
    
    	public function testClassWithoutConstructorProvidedParams(): void
    	{
    		$this->expectException(AutoWireException::class);
    		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\NoConstructorDependency\': Has no constructor, but 1 parameters were given');
    
    		$container = new Container();
    
    		$container->wire(NoConstructorDependency::class, array('test' => 'value'));
    
    		$compiler = new Compiler($container);
    		$compiler->compile();
    	}
    
    	public function testClassWithoutConstructorCompiles(): void
    	{
    		$container = new Container();
    
    		$container->wire(NoConstructorDependency::class);
    
    		$compiler = new Compiler($container);
    
    		$expected = <<<'COMPILED'
    		if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    			throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class($config) extends \holonet\common\di\Container {
    			public function make(string $abstract, array $extraParams = array()): object {
    				return match ($abstract) {
    					holonet\common\tests\di\NoConstructorDependency::class => $this->make_holonet_common_tests_di_NoConstructorDependency(),
    					default => parent::make($abstract, $extraParams)
    				};
    			}
    
    			public function make_holonet_common_tests_di_NoConstructorDependency(): holonet\common\tests\di\NoConstructorDependency {
    				return new holonet\common\tests\di\NoConstructorDependency();
    			}
    		};
    		COMPILED;
    		$actual = $compiler->compile();
    
    		$this->assertEqualsIgnoringIndentation($expected, $actual);
    		$this->assertValidCompiledContainer($actual, $container->registry);
    	}
    
    	public function testIntersectionTypesCannotBeCompiled(): void
    	{
    		$this->expectException(AutoWireException::class);
    		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\DependencyWithIntersectionType::__construct\': Parameter #0: param: Cannot auto-wire intersection types');
    
    		$container = new Container();
    
    		$container->wire(DependencyWithIntersectionType::class);
    
    		$compiler = new Compiler($container);
    		$compiler->compile();
    	}
    
    	public function testCannotAutowireUntypedParameter(): void
    	{
    		$this->expectException(AutoWireException::class);
    		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\UntypedParamsDependency::__construct\': Parameter #0: param1: Can only auto-wire typed parameters');
    
    		$container = new Container();
    		$container->wire(UntypedParamsDependency::class);
    
    		$compiler = new Compiler($container);
    		$compiler->compile();
    	}
    
    	public function testCanAutowireUntypedOptionalParameter(): void
    	{
    		$container = new Container();
    		$container->wire(UntypedParamOptionalDependency::class);
    
    		$compiler = new Compiler($container);
    		$this->assertNotEmpty($compiler->compile());
    	}
    
    	public function testCompilesOptionalOrNullableParameter(): void
    	{
    		$container = new Container();
    		$container->wire(OptionalAndNullableParamsDependency::class);
    
    		$compiler = new Compiler($container);
    
    		$expected = <<<'COMPILED'
    		if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    			throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class($config) extends \holonet\common\di\Container {
    			public function make(string $abstract, array $extraParams = array()): object {
    				return match ($abstract) {
    					holonet\common\tests\di\OptionalAndNullableParamsDependency::class => $this->make_holonet_common_tests_di_OptionalAndNullableParamsDependency(),
    					default => parent::make($abstract, $extraParams)
    				};
    			}
    
    			public function make_holonet_common_tests_di_OptionalAndNullableParamsDependency(): holonet\common\tests\di\OptionalAndNullableParamsDependency {
    				return new holonet\common\tests\di\OptionalAndNullableParamsDependency(param1: null);
    			}
    		};
    		COMPILED;
    		$actual = $compiler->compile();
    
    		$this->assertEqualsIgnoringIndentation($expected, $actual);
    		$this->assertValidCompiledContainer($actual, $container->registry);
    	}
    
    	public function testUnionTypeCanBeCompiled(): void
    	{
    		$container = new Container();
    		$container->wire(UnionTypeDependency::class);
    
    		$compiler = new Compiler($container);
    
    		$expected = <<<'COMPILED'
    		if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    		       throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class($config) extends \holonet\common\di\Container {
    		       public function make(string $abstract, array $extraParams = array()): object {
    		               return match ($abstract) {
    		                       holonet\common\tests\di\UnionTypeDependency::class => $this->make_holonet_common_tests_di_UnionTypeDependency(),
    		                       default => parent::make($abstract, $extraParams)
    		               };
    		       }
    
    		       public function make_holonet_common_tests_di_UnionTypeDependency(): holonet\common\tests\di\UnionTypeDependency {
    		               return new holonet\common\tests\di\UnionTypeDependency(param: $this->byType('holonet\common\Noun', 'param'));
    		       }
    		};
    		COMPILED;
    		$actual = $compiler->compile();
    
    		$this->assertEqualsIgnoringIndentation($expected, $actual);
    		$this->assertValidCompiledContainer($actual, $container->registry);
    	}
    
    	public function testCannotCompileNonWireableDependency(): void {
    		$this->expectException(AutoWireException::class);
    		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\NonWireableDependency::__construct\': Parameter #0: param: Cannot auto-wire to type \'string\'');
    
    		$container = new Container();
    		$container->wire(NonWireableDependency::class);
    
    		$compiler = new Compiler($container);
    		$compiler->compile();
    	}
    
    	public function testUnionTypeNonWireable(): void {
    		$this->expectException(AutoWireException::class);
    		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\DependencyTest::__construct\': Parameter #0: param: Cannot auto-wire to union type \'holonet\common\tests\di\NonWireableDependency|string\'');
    
    		$container = new Container();
    		$container->wire(DependencyTest::class);
    
    		$compiler = new Compiler($container);
    		$compiler->compile();
    	}
    
    	public function testCompileConfigParam(): void {
    		$container = new Container();
    
    		$value = array('test', 'cool');
    		$container->registry->set('config.just_an_array_value', $value);
    		$container->registry->set('service.other', array('stringValue' => 'test'));
    		$container->registry->set('service.config', array('stringValue' => 'test'));
    
    		$container->wire(ServiceWithArrayConfigValue::class);
    		$container->wire(OtherDependency::class);
    		$container->wire(Dependency::class, array('config' => 'service.config'));
    
    		$compiler = new Compiler($container);
    
    		$expected = <<<'COMPILED'
    		if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    			throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class($config) extends \holonet\common\di\Container {
    			public function make(string $abstract, array $extraParams = array()): object {
    				return match ($abstract) {
    					holonet\common\tests\di\ServiceWithArrayConfigValue::class => $this->make_holonet_common_tests_di_ServiceWithArrayConfigValue(),
    					holonet\common\tests\di\OtherDependency::class => $this->make_holonet_common_tests_di_OtherDependency(),
    					holonet\common\tests\di\Dependency::class => $this->make_holonet_common_tests_di_Dependency(),
    					default => parent::make($abstract, $extraParams)
    				};
    			}
    
    			public function make_holonet_common_tests_di_ServiceWithArrayConfigValue(): holonet\common\tests\di\ServiceWithArrayConfigValue {
    				return new holonet\common\tests\di\ServiceWithArrayConfigValue(value: $this->registry->get('config.just_an_array_value'));
    			}
    
    			public function make_holonet_common_tests_di_OtherDependency(): holonet\common\tests\di\OtherDependency {
    				return new holonet\common\tests\di\OtherDependency(config: $this->registry->verifiedDto('service.other', 'holonet\common\tests\di\Config'));
    			}
    
    			public function make_holonet_common_tests_di_Dependency(): holonet\common\tests\di\Dependency {
    				return new holonet\common\tests\di\Dependency(config: $this->registry->asDto('service.config', 'holonet\common\tests\di\Config'));
    			}
    		};
    		COMPILED;
    		$actual = $compiler->compile();
    
    		$this->assertEqualsIgnoringIndentation($expected, $actual);
    		$this->assertValidCompiledContainer($actual, $container->registry);
    	}
    
    	public function testCompileNamedService(): void {
    		$registry = new ConfigRegistry();
    
    		$container = new Container($registry);
    		$container->set('service1', DiAnonDep::class);
    		$compiler = new Compiler($container);
    		$expected = <<<'COMPILED'
    		if (!isset($config) || !$config instanceof \holonet\common\config\ConfigRegistry) {
    			throw new \InvalidArgumentException('The config parameter must be an instance of \holonet\common\config\ConfigRegistry');
    		}
    
    		return new class($config) extends \holonet\common\di\Container {
    			public function make(string $abstract, array $extraParams = array()): object {
    				return match ($abstract) {
    					'service1' => $this->make_service1(),
    					default => parent::make($abstract, $extraParams)
    				};
    			}
    
    			public function make_service1(): holonet\common\tests\di\DiAnonDep {
    				return new holonet\common\tests\di\DiAnonDep();
    			}
    		};
    		COMPILED;
    
    		$actual = $compiler->compile();
    
    		$this->assertEqualsIgnoringIndentation($expected, $actual);
    		$this->assertValidCompiledContainer($actual, $registry);
    	}
    
    	protected function assertValidCompiledContainer(string $code, ConfigRegistry $config): void {
    		$container = eval("{$code}");
    		$this->assertTrue(str_contains(get_class($container), '@anonymous'));
    		$this->assertInstanceOf(Container::class, $container);
    	}
    
    	protected function assertEqualsIgnoringIndentation(string $expect, string $actual, string $message = ''): void
    	{
    		$expect = preg_replace('/\s+/', ' ', $expect);
    		$actual = preg_replace('/\s+/', ' ', $actual);
    		$this->assertEquals($expect, $actual, $message);
    	}
    }
    
    class DependencyTest
    {
    	public function __construct(NonWireableDependency|string $param)
    	{
    	}
    }
    
    class NonWireableDependency
    {
    	public function __construct(string $param)
    	{
    	}
    }
    
    class UnionTypeDependency
    {
    	public function __construct(string|Noun $param)
    	{
    	}
    }
    
    class OptionalAndNullableParamsDependency
    {
    	public function __construct(?string $param1, string $param2 = 'default')
    	{
    	}
    }
    
    class UntypedParamOptionalDependency
    {
    	public function __construct($param1 = null)
    	{
    	}
    }
    
    class UntypedParamsDependency
    {
    	public function __construct($param1, $param2 = null)
    	{
    	}
    }
    
    class NoConstructorDependency
    {
    }
    
    class DependencyWithIntersectionType
    {
    	public function __construct(Stringable&Countable $param)
    	{
    	}
    }
  • Changed file ConfigAutoWireProviderTest.php
    diff --git a/0df0d02e596d31fad3620fb5c7f61ad88afc3c21 b/e6c8bd796e8437689fe533201abee7263bf65335
    index 0df0d02..e6c8bd7 100644
    --- a/0df0d02e596d31fad3620fb5c7f61ad88afc3c21
    +++ b/e6c8bd796e8437689fe533201abee7263bf65335
    @@ -7,7 +7,7 @@
      * @author  Matthias Lantsch <[email protected]>
      */
    
    -namespace holonet\common\tests;
    +namespace holonet\common\tests\di;
    
     use PHPUnit\Framework\TestCase;
     use holonet\common\di\Container;
    @@ -86,7 +86,7 @@ class ConfigAutoWireProviderTest extends TestCase {
    
     	public function testUserMustSupplyConfigKeyForInjection(): void {
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\Dependency::__construct\': Parameter #0: config: Cannot auto-wire to a config dto object without supplying a config key');
    +		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\Dependency::__construct\': Parameter #0: config: Cannot auto-wire to a config dto object without supplying a config key');
    
     		$container = new Container();
  • Created new file ConfigDependencyDiscoveryTest.php
    <?php
    /**
     * This file is part of the hdev common library package
     * (c) Matthias Lantsch.
     *
     * @license http://www.wtfpl.net/ Do what the fuck you want Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\tests\di;
    
    use holonet\common\collection\Registry;
    use holonet\common\config\ConfigRegistry;
    use holonet\common\di\discovery\ConfigDependencyDiscovery;
    use PHPUnit\Framework\TestCase;
    use holonet\common\di\Container;
    use holonet\common\di\autowire\AutoWire;
    use PHPUnit\Framework\Attributes\CoversClass;
    use holonet\common\error\BadEnvironmentException;
    use holonet\common\di\DependencyInjectionException;
    use holonet\common\verifier\rules\string\MaxLength;
    use holonet\common\di\autowire\attribute\ConfigItem;
    use holonet\common\di\autowire\provider\ConfigAutoWireProvider;
    
    #[CoversClass(ConfigDependencyDiscovery::class)]
    class ConfigDependencyDiscoveryTest extends TestCase {
    	public function testDiscover() {
    		$registry = new ConfigRegistry();
    		$registry->set('di', [
    			'services' => [
    				'service1' => SimpleDependency::class,
    				'service2' => [DependencyWithParameter::class, ['mustBeSuppliedParameter' => 'value1']],
    				'service3' => AutoWire::class,
    			],
    			'auto_wire' => [
    				'name1' => SimpleDependency::class,
    				'name2' => [DependencyWithParameter::class, ['mustBeSuppliedParameter' => 'value1']],
    				'name3' => AutoWire::class,
    			],
    		]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    
    		$this->assertInstanceOf(SimpleDependency::class, $container->get('service1'));
    		$this->assertInstanceOf(DependencyWithParameter::class, $container->get('service2'));
    		$this->assertInstanceOf(AutoWire::class, $container->get('service3'));
    
    
    		$this->assertInstanceOf(SimpleDependency::class, $container->make('name1'));
    		$this->assertInstanceOf(DependencyWithParameter::class, $container->make('name2'));
    		$this->assertInstanceOf(AutoWire::class, $container->make('name3'));
    	}
    
    	public function testInvalidOrEmptyAbstractDefinitionServices(): void {
    		$this->expectException(BadEnvironmentException::class);
    		$this->expectExceptionMessage('Faulty config with key \'di.services.service1\': Abstract must be class name or array with class name and parameters');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => []]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    
    		$this->expectExceptionMessage('Faulty config with key \'di.services.service1\': Abstract must be class name or array with class name and parameters');
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => ['one' => 1, 'two' => 2, 'three' => 3, 'four' => 4]]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    	}
    
    	public function testInvalidOrEmptyAbstractDefinition(): void {
    		$this->expectException(BadEnvironmentException::class);
    		$this->expectExceptionMessage('Faulty config with key \'di.auto_wire.service1\': Abstract must be class name or array with class name and parameters');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.auto_wire', ['service1' => []]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    
    		$this->expectExceptionMessage('Faulty config with key \'di.auto_wire.service1\': Abstract must be class name or array with class name and parameters');
    		$registry = new ConfigRegistry();
    		$registry->set('di.auto_wire', ['service1' => ['one' => 1, 'two' => 2, 'three' => 3, 'four' => 4]]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    	}
    
    	public function testInvalidClassNameAbstract(): void {
    		$this->expectException(BadEnvironmentException::class);
    		$this->expectExceptionMessage('Faulty config with key \'di.auto_wire.service1\': Abstract must be class name or array with class name and parameters');
    
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.auto_wire', ['service1' => [100500, ['param1' => 'cool']]]);
    		$container = new Container($registry);
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    		$dependencyDiscovery->discover($container);
    	}
    
    	public function testNonStringWiringKeyIsIgnored(): void {
    		$this->expectException(DependencyInjectionException::class);
    		$this->expectExceptionMessage('No idea how to make \'5251\'. Class does not exist and no wire directive was set');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.auto_wire', [5251 => AutoWire::class]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    
    		$container->make(5251);
    	}
    
    	public function testInvalidParameters(): void {
    		$this->expectException(BadEnvironmentException::class);
    		$this->expectExceptionMessage('Faulty config with key \'di.services.service1\': Abstract must be class name or array with class name and parameters');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => [SimpleDependency::class, 'not an array']]);
    		$container = new Container($registry);
    
    		$dependencyDiscovery = new ConfigDependencyDiscovery();
    
    		$dependencyDiscovery->discover($container);
    	}
    }
    
    class SimpleDependency {
    
    }
  • Changed file ContainerAutoWireProviderTest.php
    diff --git a/3f71c359ff1bdf75071ed6011e7581300f32293b b/bd8a7a0e03147aa299dbd54274c2ceec02366369
    index 3f71c35..bd8a7a0 100644
    --- a/3f71c359ff1bdf75071ed6011e7581300f32293b
    +++ b/bd8a7a0e03147aa299dbd54274c2ceec02366369
    @@ -7,7 +7,7 @@
      * @author  Matthias Lantsch <[email protected]>
      */
    
    -namespace holonet\common\tests;
    +namespace holonet\common\tests\di;
    
     use PHPUnit\Framework\TestCase;
     use holonet\common\di\Container;
    @@ -25,7 +25,7 @@ use holonet\common\di\autowire\provider\ContainerAutoWireProvider;
     class ContainerAutoWireProviderTest extends TestCase {
     	public function testInjectionFailedThrowsException(): void {
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\DependencyWithParameter::__construct\': Parameter #0: mustBeSuppliedParameter: Cannot auto-wire to type \'string\'');
    +		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\DependencyWithParameter::__construct\': Parameter #0: mustBeSuppliedParameter: Cannot auto-wire to type \'string\'');
    
     		$container = new Container();
  • Changed file ContainerTest.php
    diff --git a/3ff8253d93e1ba3ce84dbb936608920ad6ae4548 b/6bab33777b0b63b90b277bfd0dc760658f2656b2
    index 3ff8253..6bab337 100644
    --- a/3ff8253d93e1ba3ce84dbb936608920ad6ae4548
    +++ b/6bab33777b0b63b90b277bfd0dc760658f2656b2
    @@ -7,7 +7,7 @@
      * @author  Matthias Lantsch <[email protected]>
      */
    
    -namespace holonet\common\tests;
    +namespace holonet\common\tests\di;
    
     use Countable;
     use Stringable;
    @@ -42,7 +42,7 @@ class ContainerTest extends TestCase {
    
     	public function testIntersectionTypesCannotBeAutoWired(): void {
     		$this->expectException(AutoWireException::class);
    -		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\IntersectionTypes::__construct\': Parameter #0: intersection: Cannot auto-wire intersection types');
    +		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\IntersectionTypes::__construct\': Parameter #0: intersection: Cannot auto-wire intersection types');
    
     		$container = new Container();
    
    @@ -61,7 +61,7 @@ class ContainerTest extends TestCase {
    
     	public function testMakeCalledWithJustAnInterface(): void {
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('No idea how to make \'holonet\common\tests\MyInterface\'. Class does not exist and no wire directive was set');
    +		$this->expectExceptionMessage('No idea how to make \'holonet\common\tests\di\MyInterface\'. Class does not exist and no wire directive was set');
    
     		$container = new Container();
    
    @@ -88,7 +88,7 @@ class ContainerTest extends TestCase {
    
     	public function testMakeThrowsErrorIfConstructorArgumentsAreNotGiven(): void {
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\SomeService::__construct\': Parameter #0: parameter: Cannot auto-wire to type \'string\'');
    +		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\SomeService::__construct\': Parameter #0: parameter: Cannot auto-wire to type \'string\'');
    
     		$container = new Container();
    
    @@ -97,7 +97,7 @@ class ContainerTest extends TestCase {
    
     	public function testMakeThrowsErrorIfParametersAreGivenForConstructorLessAbstract(): void {
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\DiAnonDep\': Has no constructor, but 1 parameters were given');
    +		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\DiAnonDep\': Has no constructor, but 1 parameters were given');
    
     		$container = new Container();
    
    @@ -126,7 +126,7 @@ class ContainerTest extends TestCase {
    
     		// if accessing by type without supplying a name hint, an exception should be thrown
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('Ambiguous dependency of type \'holonet\common\tests\DiAnonDep\' requested: found 2 dependencies of that type');
    +		$this->expectExceptionMessage('Ambiguous dependency of type \'holonet\common\tests\di\DiAnonDep\' requested: found 2 dependencies of that type');
     		$container->byType(DiAnonDep::class);
     	}
    
    @@ -140,7 +140,7 @@ class ContainerTest extends TestCase {
    
     	public function testRecursionDetectionMake(): void {
     		$this->expectException(DependencyInjectionException::class);
    -		$this->expectExceptionMessage('Recursive dependency definition detected: holonet\common\tests\RecursionA => holonet\common\tests\RecursionB => holonet\common\tests\RecursionC');
    +		$this->expectExceptionMessage('Recursive dependency definition detected: holonet\common\tests\di\RecursionA => holonet\common\tests\di\RecursionB => holonet\common\tests\di\RecursionC');
    
     		$container = new Container();
    
    @@ -162,7 +162,7 @@ class ContainerTest extends TestCase {
    
     	public function testTypelessParametersThrowAnException(): void {
     		$this->expectException(AutoWireException::class);
    -		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\TypelessClass::__construct\': Parameter #0: test: Can only auto-wire typed parameters');
    +		$this->expectExceptionMessage('Failed to auto-wire \'holonet\common\tests\di\TypelessClass::__construct\': Parameter #0: test: Can only auto-wire typed parameters');
    
     		$container = new Container();
    
    @@ -172,9 +172,9 @@ class ContainerTest extends TestCase {
     	public function testUnionTypesFailedInjection(): void {
     		$this->expectException(AutoWireException::class);
     		$this->expectExceptionMessage(<<<'Message'
    -		Failed to auto-wire 'holonet\common\tests\UnionTypesMultipleFailures::__construct': Parameter #0: service: Cannot auto-wire to union type 'holonet\common\tests\SomeService|holonet\common\tests\SomeServiceTwo':
    -		Failed to auto-wire 'holonet\common\tests\SomeService::__construct': Parameter #0: parameter: Cannot auto-wire to type 'string'
    -		Failed to auto-wire 'holonet\common\tests\SomeServiceTwo::__construct': Parameter #0: parameter: Cannot auto-wire to type 'string'
    +		Failed to auto-wire 'holonet\common\tests\di\UnionTypesMultipleFailures::__construct': Parameter #0: service: Cannot auto-wire to union type 'holonet\common\tests\di\SomeService|holonet\common\tests\di\SomeServiceTwo':
    +		Failed to auto-wire 'holonet\common\tests\di\SomeService::__construct': Parameter #0: parameter: Cannot auto-wire to type 'string'
    +		Failed to auto-wire 'holonet\common\tests\di\SomeServiceTwo::__construct': Parameter #0: parameter: Cannot auto-wire to type 'string'
     		Message
     		);
    
    @@ -213,6 +213,15 @@ class ContainerTest extends TestCase {
     		$this->assertInstanceOf(TestClass::class, $container->make(MyInterface::class));
     		$this->assertInstanceOf(TestClass::class, $container->make(AbstractBaseClass::class));
     	}
    +
    +	public function testSetNonsenseAsService(): void {
    +		$this->expectException(DependencyInjectionException::class);
    +		$this->expectExceptionMessage('Could not set dependency \'nonsense\': value is not an object or class name');
    +
    +		$container = new Container();
    +
    +		$container->set('nonsense', '\\nonsense\\class\\TestClass');
    +	}
     }
    
     class RecursionA {
  • Created new file FactoryTest.php
    <?php
    /**
     * This file is part of the hdev common library package
     * (c) Matthias Lantsch.
     *
     * @license http://www.wtfpl.net/ Do what the fuck you want Public License
     * @author  Matthias Lantsch <[email protected]>
     */
    
    namespace holonet\common\tests\di;
    
    use holonet\common\config\ConfigRegistry;
    use holonet\common\di\Container;
    use holonet\common\di\Factory;
    use holonet\common\error\BadEnvironmentException;
    use PHPUnit\Framework\TestCase;
    use PHPUnit\Framework\Attributes\CoversClass;
    use holonet\common\di\autowire\AutoWireException;
    use holonet\common\di\DependencyNotFoundException;
    use holonet\common\di\DependencyInjectionException;
    use ReflectionObject;
    use function holonet\common\get_class_short;
    
    #[CoversClass(Factory::class)]
    class FactoryTest extends TestCase {
    
    	public function testCompilerDisabledFactory(): void {
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => [Dependency::class]]);
    
    		$factory = new Factory($registry);
    
    		$container = $factory->make();
    		$this->assertSame(Container::class, get_class($container));
    	}
    
    	public function testFactoryReturnsOldCompiledContainer(): void {
    		$this->assertFileExists(dirname(__DIR__).'/data/container.php');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => [DiAnonDep::class]]);
    		$registry->set('di.cache_path', dirname(__DIR__).'/data');
    
    		$factory = new Factory($registry);
    
    		$container = $factory->make();
    		$this->assertTrue(str_contains(get_class($container), '@anonymous'));
    		$reflection = new ReflectionObject($container);
    		$this->assertTrue($reflection->hasMethod('make_service1'), 'Factory did not create the make_service1 method');
    		$this->assertInstanceOf(Container::class, $container);
    	}
    
    	public function testCompileContainerToFile(): void {
    		@unlink(dirname(__DIR__).'/data/container.php');
    
    		$this->assertFileDoesNotExist(dirname(__DIR__).'/data/container.php');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => DiAnonDep::class]);
    		$registry->set('di.cache_path', dirname(__DIR__).'/data');
    
    		$factory = new Factory($registry);
    
    		$container = $factory->make();
    		$this->assertTrue(str_contains(get_class($container), '@anonymous'));
    		$reflection = new ReflectionObject($container);
    		$this->assertTrue($reflection->hasMethod('make_service1'), 'Factory did not create the make_service1 method');
    		$this->assertInstanceOf(Container::class, $container);
    		$this->assertFileExists(dirname(__DIR__).'/data/container.php');
    	}
    
    	public function testBadCachePathCausesAnException(): void {
    		$this->expectException(BadEnvironmentException::class);
    		$this->expectExceptionMessage('Container compile path \'/rubbish/path/does/not/exist\' is not a writable directory');
    
    		$registry = new ConfigRegistry();
    		$registry->set('di.services', ['service1' => DiAnonDep::class]);
    		$registry->set('di.cache_path', '/rubbish/path/does/not/exist');
    
    		$factory = new Factory($registry);
    		$factory->make();
    	}
    
    }
  • Changed file ForwardAutoWireProviderTest.php
    diff --git a/fc27e47f43acf4fe52452935f063a8e5a6745082 b/33e9ac84ce4b1cf2333b1db321c545cd301be8bc
    index fc27e47..33e9ac8 100644
    --- a/fc27e47f43acf4fe52452935f063a8e5a6745082
    +++ b/33e9ac84ce4b1cf2333b1db321c545cd301be8bc
    @@ -7,7 +7,7 @@
      * @author  Matthias Lantsch <[email protected]>
      */
    
    -namespace holonet\common\tests;
    +namespace holonet\common\tests\di;
    
     use PHPUnit\Framework\TestCase;
     use holonet\common\di\Container;
  • Changed file ProviderTest.php
    diff --git a/2bcbbb1f347c8d3e6e8e6ef1d28f2a0b21896bff b/cfa19927af3ad5d28a925a0efc29ca6a47151666
    index 2bcbbb1..cfa1992 100644
    --- a/2bcbbb1f347c8d3e6e8e6ef1d28f2a0b21896bff
    +++ b/cfa19927af3ad5d28a925a0efc29ca6a47151666
    @@ -7,7 +7,7 @@
      * @author  Matthias Lantsch <[email protected]>
      */
    
    -namespace holonet\common\tests;
    +namespace holonet\common\tests\di;
    
     use Countable;
     use holonet\common\di\Provider;
    @@ -23,10 +23,6 @@ use holonet\common\di\DependencyInjectionException;
     #[CoversClass(Container::class)]
     #[CoversClass(Provider::class)]
     class ProviderTest extends TestCase {
    -	// test provider for service
    -	// test provider for instance
    -	// test provider for type byType
    -	// test provider calling make() with the actual class
    
     	public function testProviderSetForService(): void {
     		$container = new Container();
    @@ -71,6 +67,24 @@ class ProviderTest extends TestCase {
     		$this->assertInstanceOf(ProvidedDependency::class, $result);
     		$this->assertFalse($result === $container->make(ProvidedDependency::class));
     	}
    +
    +	public function testMakeWithBadProviderWithoutTypehintCausesException(): void {
    +		$this->expectException(DependencyInjectionException::class);
    +		$this->expectExceptionMessage('Provider factory method holonet\common\tests\di\BadProviderWithoutTypehint::make() has no return type');
    +
    +		$container = new Container();
    +
    +		$container->wire(BadProviderWithoutTypehint::class);
    +	}
    +
    +	public function testSetWithBadProviderWithoutTypehintCausesException(): void {
    +		$this->expectException(DependencyInjectionException::class);
    +		$this->expectExceptionMessage('Provider factory method holonet\common\tests\di\BadProviderWithoutTypehint::make() has no return type');
    +
    +		$container = new Container();
    +
    +		$container->set('test', BadProviderWithoutTypehint::class);
    +	}
     }
    
     class TestProvider extends Provider {
    @@ -81,6 +95,14 @@ class TestProvider extends Provider {
    
     }
    
    +class BadProviderWithoutTypehint extends Provider {
    +
    +	public function make(): object {
    +		return new ProvidedDependency();
    +	}
    +
    +}
    +
     class ProvidedDependency {
    
     }