Git viewing holonet/common / 4ff572e075fec82aa599b1763ea0883dcca0164b


Filter

4ff572e075fec82aa599b1763ea0883dcca0164b

Matthias Lantsch(1 year, 1 month ago)

New dependency container; Use reflection to inject dependencies straight into the constructor

Browse Files
  • Changed file .gitignore
    diff --git a/fb87637f5789154a756e5bd96a9ed70053a0c14c b/c1d5638deaf7b1c51c309857c7e807c64efcd6e9
    index fb87637..c1d5638 100644
    --- a/fb87637f5789154a756e5bd96a9ed70053a0c14c
    +++ b/c1d5638deaf7b1c51c309857c7e807c64efcd6e9
    @@ -6,4 +6,4 @@
     tests/coverage/*
     /composer.lock
     .idea/
    -
    +.phpunit.cache/*
  • Changed file .php-cs-fixer.dist.php
    diff --git a/37d4bfaa150067f345365be862a851b1ed6eb797 b/8f4f756c403f1c26cc94027b78c732f59bb5605b
    index 37d4bfa..8f4f756 100644
    --- a/37d4bfaa150067f345365be862a851b1ed6eb797
    +++ b/8f4f756c403f1c26cc94027b78c732f59bb5605b
    @@ -1,6 +1,9 @@
     <?php
    
     $config = require 'vendor/holonet/hdev/.php-cs-fixer.dist.php';
    +$config->setRules([
    +	'php_unit_test_class_requires_covers' => false
    +]);
     $config->setFinder(PhpCsFixer\Finder::create()
     	->exclude('vendor')
     	->exclude('tests/data')
  • Changed file composer.json
    diff --git a/31b6bd769cb6f516c6a7a4c79def5b3f8b16e8cc b/097579b7772e2459e7e7e22a743958a8a7cba60e
    index 31b6bd7..097579b 100644
    --- a/31b6bd769cb6f516c6a7a4c79def5b3f8b16e8cc
    +++ b/097579b7772e2459e7e7e22a743958a8a7cba60e
    @@ -15,7 +15,7 @@
     	},
     	"require-dev": {
     		"holonet/hdev": "~1.1.0@dev",
    -		"phpunit/phpunit": "^9.5"
    +		"phpunit/phpunit": "^10.0.0"
     	},
     	"provide": {
     		"psr/container-implementation": "2.0.2"
    @@ -26,6 +26,7 @@
     			"url": "https://holonet.easylabs.ch/hgit/composer/"
     		}
     	],
    +	"minimum-stability": "dev",
     	"autoload": {
     		"psr-4": {
     			"holonet\\common\\": "src/"
  • Changed file phpunit.xml
    diff --git a/dcfabe5d30bfe0cff973c8208a48d78cfa85ae6f b/20dd276382c6594baa54d3f351f845a1e9e7cfa3
    index dcfabe5..20dd276 100644
    --- a/dcfabe5d30bfe0cff973c8208a48d78cfa85ae6f
    +++ b/20dd276382c6594baa54d3f351f845a1e9e7cfa3
    @@ -1,16 +1,16 @@
    -<phpunit
    -  forceCoversAnnotation="true"
    -  bootstrap="vendor/autoload.php"
    -  colors="true">
    +<?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">
       <testsuite name="tests">
         <directory>./tests</directory>
       </testsuite>
    -  <coverage processUncoveredFiles="true">
    -    <include>
    -      <directory suffix=".php">./src</directory>
    -    </include>
    +  <coverage>
         <report>
           <html outputDirectory="tests/coverage/html"/>
         </report>
       </coverage>
    +  <source>
    +    <include>
    +      <directory suffix=".php">./src</directory>
    +    </include>
    +  </source>
     </phpunit>
  • Changed file ConfigRegistry.php
    diff --git a/438250bf06145e861d2a47ff32f2ddbc6f498a4b b/9cd09b5663fc7ee8ef75de35be5def0279e7b7a2
    index 438250b..9cd09b5 100644
    --- a/438250bf06145e861d2a47ff32f2ddbc6f498a4b
    +++ b/9cd09b5663fc7ee8ef75de35be5def0279e7b7a2
    @@ -21,11 +21,15 @@ use holonet\common\error\BadEnvironmentException;
     class ConfigRegistry extends Registry {
     	/**
     	 * @template T
    -	 * Get a verified instance of a config dto class supplied by the user.
    +	 * Get an instance of a config dto class supplied by the user.
     	 * @param class-string<T>|T $cfgDto
     	 * @return T
     	 */
    -	public function verifiedDto(string $configKey, string|object $cfgDto): object {
    +	public function asDto(string $configKey, string|object $cfgDto): object {
    +		if (!$this->has($configKey)) {
    +			throw BadEnvironmentException::faultyConfig($configKey, 'Config item doesn\'t exist');
    +		}
    +
     		$value = $this->get($configKey);
     		if (!is_array($value)) {
     			$value = array($value);
    @@ -41,6 +45,18 @@ class ConfigRegistry extends Registry {
     			throw BadEnvironmentException::faultyConfig($configKey, "TypeError: {$e->getMessage()}");
     		}
    
    +		return $cfgDto;
    +	}
    +
    +	/**
    +	 * @template T
    +	 * Get a verified instance of a config dto class supplied by the user.
    +	 * @param class-string<T>|T $cfgDto
    +	 * @return T
    +	 */
    +	public function verifiedDto(string $configKey, string|object $cfgDto): object {
    +		$cfgDto = $this->asDto($configKey, $cfgDto);
    +
     		$proof = verify($cfgDto);
     		if ($proof->pass()) {
     			return $cfgDto;
  • Changed file Container.php
    diff --git a/72fa9456440f9ab849554b8370a8c2f1fba63060 b/5c0e9405c1307a4ff5cfa11a8ba408317d2fd718
    index 72fa945..5c0e940 100644
    --- a/72fa9456440f9ab849554b8370a8c2f1fba63060
    +++ b/5c0e9405c1307a4ff5cfa11a8ba408317d2fd718
    @@ -9,110 +9,182 @@
    
     namespace holonet\common\di;
    
    -use TypeError;
     use ReflectionClass;
    -use ReflectionException;
     use Psr\Container\ContainerInterface;
    +use holonet\common\di\autowire\AutoWire;
    +use holonet\common\config\ConfigRegistry;
    +use holonet\common\di\autowire\AutoWireException;
    
     /**
      * Dependency Injection container conforming with PSR-11.
      */
     class Container implements ContainerInterface {
     	/**
    -	 * @var string DI_PREFIX Prefix value for the injected class properties
    +	 * @var array<string, string> $aliases key mapping with all available services on the container
     	 */
    -	public const DI_PREFIX = 'di_';
    +	protected array $aliases = array();
    +
    +	protected AutoWire $autoWiring;
    +
    +	/**
    +	 * @var array<string, array<string, array{string, array}>> $callers Method calls with injection definitions
    +	 */
    +	protected array $callers = array();
    +
    +	/**
    +	 * @var array<string, object> $instances a key value storage with dependency objects
    +	 */
    +	protected array $instances = array();
    +
    +	/**
    +	 * @var string[] $recursionPath Array used keep track of injections (to prevent recursive dependencies)
    +	 */
    +	protected array $recursionPath = array();
    
     	/**
    -	 * @var array<string, object> $dependencies A key value storage with dependency objects
    +	 * @var array<string, 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).
     	 */
    -	private array $dependencies = array();
    +	protected array $wiring = array();
    +
    +	public function __construct(public ConfigRegistry $registry = new ConfigRegistry()) {
    +		$this->autoWiring = new AutoWire($this);
    +	}
    
     	/**
    -	 * @var array<string, array> $lazyLoadedDeps Lazily loaded dependency objects
    +	 * @template T
    +	 * @param class-string<T> $class
    +	 * @return T
     	 */
    -	private array $lazyLoadedDeps = array();
    +	public function byType(string $class, ?string $id = null): object {
    +		$keys = array_keys($this->aliases, $class);
    +		if (count($keys) === 1) {
    +			return $this->get(reset($keys));
    +		}
    +
    +		if ($id === null) {
    +			throw new DependencyInjectionException(sprintf('Ambiguous dependency of type \'%s\' requested: found %d dependencies of that type', $class, count($keys)));
    +		}
    +
    +		if (!in_array($id, $keys)) {
    +			// we don't have it, let's try to make it
    +			return $this->make($class);
    +		}
    +
    +		return $this->get($id);
    +	}
    
     	/**
     	 * {@inheritDoc}
    -	 * @param string[] $getFor Array used keep track of injections (to prevent recursive dependencies)
     	 */
    -	public function get($id, array $getFor = array()) {
    -		if (in_array($id, $getFor)) {
    -			throw new DependencyInjectionException('Recursive dependency definition detected: '.implode(' => ', $getFor));
    +	public function get(string $id): object {
    +		if (in_array($id, $this->recursionPath)) {
    +			throw new DependencyInjectionException(sprintf('Recursive dependency definition detected: %s', implode(' => ', $this->recursionPath)));
     		}
    
    -		if (isset($this->dependencies[$id])) {
    -			return $this->dependencies[$id];
    +		if (!$this->has($id)) {
    +			throw new DependencyNotFoundException("Container has no named dependency called '{$id}'");
     		}
    -		if (isset($this->lazyLoadedDeps[$id])) {
    -			try {
    -				list('class' => $class, 'args' => $args) = $this->lazyLoadedDeps[$id];
    -				$rfc = new ReflectionClass($class);
    -				$value = $rfc->newInstanceWithoutConstructor();
    -				$getFor[] = $id;
    -				$this->inject($value, true, $getFor);
    -				if (method_exists($value, '__construct')) {
    -					$value->__construct(...$args);
    -				}
    -				if (method_exists($value, 'init')) {
    -					trigger_error('Relying on init() to initialise dependency objects after injecting is no longer required and deprecated', \E_USER_DEPRECATED);
    -				}
    -				$this->dependencies[$id] = $value;
    -
    -				return $value;
    -			} catch (TypeError | ReflectionException $e) {
    -				throw new DependencyInjectionException("Cannot initialise dependency '{$id}' on Dependency Container: '{$e->getMessage()}'", (int)($e->getCode()), $e);
    -			}
    -		} else {
    -			throw new DependencyNotFoundException("Dependency '{$id}' does not exist on Dependency Container");
    +
    +		// if we have the dependency, just return it
    +		if (isset($this->instances[$id])) {
    +			return $this->instances[$id];
     		}
    +
    +		list($class, $params) = $this->wiring[$id];
    +
    +		$this->recursionPath[] = $id;
    +		$this->instances[$id] = $this->instance($class, $params);
    +		array_pop($this->recursionPath);
    +
    +		return $this->instances[$id];
     	}
    
     	/**
     	 * {@inheritDoc}
     	 */
    -	public function has($id) {
    -		return isset($this->dependencies[$id]) || isset($this->lazyLoadedDeps[$id]);
    +	public function has($id): bool {
    +		return isset($this->aliases[$id]);
     	}
    
     	/**
    -	 * Method used to inject dependencies into an object, here called "the user of the dependencies".
    -	 * @param object $dependencyUser The object to be injected
    -	 * @param bool $forceInjection Whether to throw an exception if a dependency cannot be found
    -	 * @param array $injectFor Array used keep track of injections (to prevent recursive dependencies)
    +	 * @template T
    +	 * @param class-string<T>|string $abstract
    +	 * @return T
     	 */
    -	public function inject(object $dependencyUser, bool $forceInjection = true, array $injectFor = array()): void {
    -		foreach (array_keys(get_class_vars(get_class($dependencyUser))) as $propertyName) {
    -			$propertyName = (string)$propertyName;
    -			if (mb_strpos($propertyName, self::DI_PREFIX) === 0) {
    -				$depKey = str_replace(self::DI_PREFIX, '', $propertyName);
    -				if (!$this->has($depKey) && $forceInjection) {
    -					throw new DependencyNotFoundException("Dependency '{$depKey}' does not exist on Dependency Container");
    -				}
    -				$dependencyUser->{$propertyName} = $this->get($depKey, $injectFor);
    +	public function make(string $abstract, array $extraParams = array()): object {
    +		if ($this->has($abstract)) {
    +			return $this->get($abstract);
    +		}
    +
    +		if (in_array($abstract, $this->recursionPath)) {
    +			throw new DependencyInjectionException(sprintf('Recursive dependency definition detected: %s', implode(' => ', $this->recursionPath)));
    +		}
    +
    +		$this->recursionPath[] = $abstract;
    +		if (isset($this->wiring[$abstract])) {
    +			list($class, $params) = $this->wiring[$abstract];
    +			$instance = $this->instance($class, array_merge($params, $extraParams));
    +		} else {
    +			if (!class_exists($abstract)) {
    +				throw new DependencyInjectionException("No idea how to make '{$abstract}'. Class does not exist and no wire directive was set");
     			}
    +
    +			$instance = $this->instance($abstract, $extraParams);
     		}
    +		array_pop($this->recursionPath);
    +
    +		return $instance;
     	}
    
     	/**
     	 * Method used to set a dependency in this class.
    -	 * If the given value is an object, it will get injected and saved under the key
    +	 * If the given value is an object, be saved under the key
     	 * If the given value is a string a class name is assumed and the class / argument combination will be saved for later instantiation.
    -	 * @param string $id The key to save the dependency under
    -	 * @param mixed $value The dependency to save
    -	 * @param mixed ...$constructorArgs Arguments for the class instantiation
     	 */
    -	public function set(string $id, $value, ...$constructorArgs): void {
    -		if (is_string($value) && class_exists($value)) {
    -			$this->lazyLoadedDeps[$id] = array('class' => $value, 'args' => $constructorArgs);
    -		} else {
    -			if (!is_object($value)) {
    -				throw new DependencyInjectionException("Cannot create dependency '{$id}' on Dependency Container");
    +	public function set(string $id, object|string $value, array $params = array()): void {
    +		// as the object has already been created, we must assume it has its dependencies
    +		if (is_object($value)) {
    +			$this->aliases[$id] = get_class($value);
    +			$this->instances[$id] = $value;
    +
    +			return;
    +		}
    +
    +		if (class_exists($value)) {
    +			$this->aliases[$id] = $value;
    +			$this->wire($value, $params, $id);
    +		}
    +	}
    +
    +	/**
    +	 * Set up a wiring from an abstract to an actual implementation.
    +	 * This can be used to choose strategy pattern possibilities based on config.
    +	 * The wired object will also get additional params autowired.
    +	 */
    +	public function wire(string $class, array $params = array(), ?string $abstract = null): void {
    +		if (!class_exists($class)) {
    +			throw new DependencyInjectionException("Could not auto-wire abstract '{$class}': class does not exist");
    +		}
    +
    +		$abstract ??= $class;
    +
    +		$this->wiring[$abstract] = array($class, $params);
    +	}
    +
    +	protected function instance(string $class, array $params): object {
    +		$reflection = new ReflectionClass($class);
    +		$constructor = $reflection->getConstructor();
    +		if ($constructor === null) {
    +			if (!empty($params)) {
    +				AutoWireException::failNoConstructor($reflection, $params);
     			}
    
    -			$this->inject($value);
    -			$this->dependencies[$id] = $value;
    +			return new $class();
     		}
    +
    +		$params = $this->autoWiring->autoWire($constructor, $params);
    +
    +		return new $class(...$params);
     	}
     }
  • Created new file AutoWire.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\autowire;
    
    use ReflectionNamedType;
    use ReflectionParameter;
    use ReflectionUnionType;
    use ReflectionFunctionAbstract;
    use ReflectionIntersectionType;
    use holonet\common\di\Container;
    use holonet\common\di\DependencyInjectionException;
    use holonet\common\di\autowire\provider\ParamAutoWireProvider;
    use holonet\common\di\autowire\provider\ConfigAutoWireProvider;
    use holonet\common\di\autowire\provider\ForwardAutoWireProvider;
    use holonet\common\di\autowire\provider\ContainerAutoWireProvider;
    
    /**
     * Small helper class which uses reflection to try and auto-wire parameters for a function / method.
     * Uses a given set of provider classes which can try to find a parameter based on it's name, user supplied value and type.
     */
    class AutoWire {
    	/**
    	 * @var ParamAutoWireProvider[]
    	 */
    	protected array $paramProviders;
    
    	public function __construct(protected Container $container) {
    		$this->paramProviders = array(
    			new ForwardAutoWireProvider(),
    			new ConfigAutoWireProvider(),
    			new ContainerAutoWireProvider(),
    		);
    	}
    
    	public function autoWire(ReflectionFunctionAbstract $method, array $givenParams = array()): array {
    		$parameters = $method->getParameters();
    		$mapped = array();
    		foreach ($parameters as $param) {
    			$autoWiredValue = $this->autoWireParameter($param, $givenParams[$param->getName()] ?? null);
    			// if we are here and the auto-wired value is null, it must be because the parameter
    			// has a default or null works as a value for the parameter.
    			if ($autoWiredValue !== null || !$param->isDefaultValueAvailable()) {
    				$mapped[$param->getName()] = $autoWiredValue;
    			}
    		}
    
    		return $mapped;
    	}
    
    	private function autoWireNamedType(ReflectionParameter $param, ReflectionNamedType $type, mixed $paramValue): mixed {
    		foreach ($this->paramProviders as $provider) {
    			$wiredValue = $provider->provide($this->container, $param, $type, $paramValue);
    
    			if ($wiredValue !== null) {
    				return $wiredValue;
    			}
    		}
    
    		if ($param->allowsNull() || $param->isOptional()) {
    			return null;
    		}
    
    		AutoWireException::failParam($param, "Cannot auto-wire to type '{$type->getName()}'");
    	}
    
    	private function autoWireParameter(ReflectionParameter $param, mixed $paramValue): mixed {
    		$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->autoWireUnionType($param, $paramType, $paramValue);
    		}
    
    		return $this->autoWireNamedType($param, $paramType, $paramValue);
    	}
    
    	private function autoWireUnionType(ReflectionParameter $param, ReflectionUnionType $type, mixed $paramValue): mixed {
    		$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 $wiredValue;
    					}
    				} catch (DependencyInjectionException $e) {
    					$errors[$type->getName()] = $e->getMessage();
    				}
    			}
    		}
    
    		$unionType = implode('|', array_keys($errors));
    		$errors = implode("\n", $errors);
    		AutoWireException::failParam($param, "Cannot auto-wire to union type '{$unionType}': \n{$errors}");
    	}
    }
  • Created new file AutoWireException.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\autowire;
    
    use ReflectionClass;
    use ReflectionMethod;
    use ReflectionParameter;
    use ReflectionFunctionAbstract;
    use holonet\common\di\DependencyInjectionException;
    
    class AutoWireException extends DependencyInjectionException {
    	private function __construct(ReflectionFunctionAbstract|ReflectionClass $reflection, string $message) {
    		$identifier = $reflection->getName();
    		if ($reflection instanceof ReflectionMethod) {
    			$identifier = sprintf('%s::%s', $reflection->getDeclaringClass()->getName(), $reflection->getName());
    		}
    		parent::__construct("Failed to auto-wire '{$identifier}': {$message}");
    	}
    
    	public static function failNoConstructor(ReflectionClass $reflection, array $params): void {
    		throw new static($reflection, sprintf('Has no constructor, but %d parameters were given', count($params)));
    	}
    
    	public static function failParam(ReflectionParameter $param, string $message): never {
    		throw new static($param->getDeclaringFunction(), "Parameter #{$param->getPosition()}: {$param->getName()}: {$message}");
    	}
    }
  • Created new file ConfigItem.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\autowire\attribute;
    
    use Attribute;
    
    #[Attribute(Attribute::TARGET_PARAMETER)]
    class ConfigItem {
    	public function __construct(public ?string $key = null, public bool $verified = true) {
    	}
    }
  • Created new file ConfigAutoWireProvider.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\autowire\provider;
    
    use ReflectionNamedType;
    use ReflectionParameter;
    use holonet\common\di\Container;
    use holonet\common\di\autowire\AutoWireException;
    use holonet\common\di\autowire\attribute\ConfigItem;
    use function holonet\common\reflection_get_attribute;
    
    /**
     * Provider which will automatically inject a config dto object read from the registry.
     * This is achieved using a special marker attribute on the parameter.
     * The corresponding config key that will be used to collect the config data can be supplied using:
     *   - a given parameter (a string) which represents the config key
     *   - the property in the marker attribute.
     */
    class ConfigAutoWireProvider implements ParamAutoWireProvider {
    	/**
    	 * {@inheritDoc}
    	 */
    	public function provide(Container $container, ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): mixed {
    		$expectedType = $type->getName();
    
    		$attribute = reflection_get_attribute($param, ConfigItem::class);
    		if ($attribute === null) {
    			return null;
    		}
    
    		$configKey = ($givenParam ?? $attribute->key);
    
    		if (!is_string($configKey)) {
    			AutowireException::failParam($param, 'Cannot auto-wire to a config dto object without supplying a config key');
    		}
    
    		if (class_exists($expectedType)) {
    			if ($attribute->verified) {
    				return $container->registry->verifiedDto($configKey, $expectedType);
    			}
    
    			return $container->registry->asDto($configKey, $expectedType);
    		}
    
    		return $container->registry->get($configKey);
    	}
    }
  • Created new file ContainerAutoWireProvider.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\autowire\provider;
    
    use ReflectionNamedType;
    use ReflectionParameter;
    use holonet\common\di\Container;
    use holonet\common\di\DependencyInjectionException;
    
    class ContainerAutoWireProvider implements ParamAutoWireProvider {
    	/**
    	 * {@inheritDoc}
    	 */
    	public function provide(Container $container, ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): mixed {
    		if (class_exists($type->getName())) {
    			try {
    				return $container->byType($type->getName(), $param->getName());
    			} catch (DependencyInjectionException $e) {
    				if (!$type->allowsNull() && !$param->isOptional()) {
    					throw $e;
    				}
    			}
    		}
    
    		return null;
    	}
    }
  • Created new file ForwardAutoWireProvider.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\autowire\provider;
    
    use ReflectionNamedType;
    use ReflectionParameter;
    use holonet\common\di\Container;
    
    /**
     * Special provider to check the types of a given parameter and just forward it if the types are the same.
     * This is so a user can just provide an actual scalar value in a configuration array.
     */
    class ForwardAutoWireProvider implements ParamAutoWireProvider {
    	/**
    	 * {@inheritDoc}
    	 */
    	public function provide(Container $container, ReflectionParameter $param, ReflectionNamedType $type, mixed $givenParam): mixed {
    		$givenType = gettype($givenParam);
    		$expectedType = $type->getName();
    		$givenType = match ($givenType) {
    			'integer' => 'int',
    			'double' => 'float',
    			'boolean' => 'bool',
    			default => $givenType
    		};
    
    		if (in_array($givenType, explode('|', $expectedType)) || $givenParam instanceof $expectedType) {
    			return $givenParam;
    		}
    
    		return null;
    	}
    }
  • Created new file ParamAutoWireProvider.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\autowire\provider;
    
    use ReflectionNamedType;
    use ReflectionParameter;
    use holonet\common\di\Container;
    
    /**
     * Interface for parameter auto-wire behaviours.
     * An implementation is supposed to provide a way to auto-wire a given type of parameter.
     */
    interface ParamAutoWireProvider {
    	/**
    	 * @param ReflectionParameter $param reflection object for the parameter that should be auto-wired
    	 * @param ReflectionNamedType $type reflection type for the given parameter
    	 * @param mixed $givenParam Parameter provided by the user
    	 *                          The given parameter could come from:
    	 *                          - an array that the user supplied to a get() or wire() call on the container
    	 *                          - a config array in the container configuration on the registry
    	 *
    	 * This method should return a mapped parameter value. If it is not appropriate for this provider to supply
    	 * such a value, it should return null
    	 * 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;
    }
  • Changed file BadEnvironmentException.php
    diff --git a/f6be009859ad696ecaeda9ff88bd0600f6ae2045 b/e6264a6fc5a8db85091f27cfb89808cd7da17f2f
    index f6be009..e6264a6 100644
    --- a/f6be009859ad696ecaeda9ff88bd0600f6ae2045
    +++ b/e6264a6fc5a8db85091f27cfb89808cd7da17f2f
    @@ -18,7 +18,7 @@ use function holonet\common\stringify;
      */
     class BadEnvironmentException extends RuntimeException {
     	public static function faultyConfig(string $key, string $errors): static {
    -		return new static("Faulty config with key {$key}: {$errors}");
    +		return new static("Faulty config with key '{$key}': {$errors}");
     	}
    
     	public static function faultyConfigFromProof(string $key, Proof $proof): static {
  • Changed file functions.php
    diff --git a/953d80ba05844aec4f5f5d13a1f9cafd12ddfd3d b/4f404341265bc36f5fae637c5c92e9f2c3559f7c
    index 953d80b..4f40434 100644
    --- a/953d80ba05844aec4f5f5d13a1f9cafd12ddfd3d
    +++ b/4f404341265bc36f5fae637c5c92e9f2c3559f7c
    @@ -10,7 +10,10 @@
     namespace holonet\common;
    
     use ReflectionClass;
    +use RuntimeException;
     use ReflectionProperty;
    +use ReflectionParameter;
    +use InvalidArgumentException;
     use holonet\common\verifier\Proof;
     use holonet\common\verifier\Verifier;
     use holonet\common\code\FileUseStatementParser;
    @@ -89,7 +92,7 @@ if (!function_exists(__NAMESPACE__.'\\reflection_get_attribute')) {
     	 * @param class-string<T> $class
     	 * @return ?T
     	 */
    -	function reflection_get_attribute(ReflectionClass|ReflectionProperty $reflection, string $class): ?object {
    +	function reflection_get_attribute(ReflectionClass|ReflectionProperty|ReflectionParameter $reflection, string $class): ?object {
     		$attrs = $reflection->getAttributes($class);
    
     		return reset($attrs) ? reset($attrs)->newInstance() : null;
    @@ -125,18 +128,6 @@ if (!function_exists(__NAMESPACE__.'\\stringify')) {
     	}
     }
    
    -if (!function_exists(__NAMESPACE__.'\\trigger_error_context')) {
    -	/**
    -	 * function using the php debug backtrace to trigger an error on the calling line.
    -	 * @param string $message The message to throw in the error
    -	 * @param int $level Error level integer, defaults to E_USER_ERROR
    -	 */
    -	function trigger_error_context(string $message, int $level = \E_USER_ERROR): void {
    -		$caller = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS, 1)[0];
    -		trigger_error("{$message} in file {$caller['file']} on line {$caller['line']}", $level);
    -	}
    -}
    -
     if (!function_exists(__NAMESPACE__.'\\indentText')) {
     	/**
     	 * function used to indent a text with newlines in it
  • Changed file ConfigReaderTest.php
    diff --git a/53e6ecd4f6caacca9a7e94d20b11a93b01606e35 b/0aaa5641701648defe20647d3c9b2dc40d1509d7
    index 53e6ecd..0aaa564 100644
    --- a/53e6ecd4f6caacca9a7e94d20b11a93b01606e35
    +++ b/0aaa5641701648defe20647d3c9b2dc40d1509d7
    @@ -12,21 +12,21 @@ namespace holonet\common\tests;
     use Exception;
     use PHPUnit\Framework\TestCase;
     use holonet\common\config\ConfigReader;
    +use PHPUnit\Framework\Attributes\CoversClass;
    +use PHPUnit\Framework\Attributes\DataProvider;
    +use holonet\common\config\parsers\IniConfigParser;
    +use holonet\common\config\parsers\PhpConfigParser;
    +use holonet\common\config\parsers\JsonConfigParser;
    
    -/**
    - * Tests the functionality of the ConfigReader class.
    - *
    - * @covers  \holonet\common\config\ConfigReader
    - *
    - * @internal
    - *
    - * @small
    - */
    +#[CoversClass(ConfigReader::class)]
    +#[CoversClass(IniConfigParser::class)]
    +#[CoversClass(JsonConfigParser::class)]
    +#[CoversClass(PhpConfigParser::class)]
     class ConfigReaderTest extends TestCase {
     	/**
     	 * Return an entry for each test config file there is so we can test all the file formats.
     	 */
    -	public function configTestProvider() {
    +	public static function configTestProvider(): array {
     		$ret = array();
     		foreach (glob(__DIR__.'/data/config.*') as $file) {
     			$ext = pathinfo($file, \PATHINFO_EXTENSION);
    @@ -48,13 +48,8 @@ class ConfigReaderTest extends TestCase {
     		$this->assertSame("File path 'iSurelyDon'tExist.ini' does not exist", $msg);
     	}
    
    -	/**
    -	 * @dataProvider configTestProvider
    -	 * @covers  \holonet\common\config\parsers\IniConfigParser
    -	 * @covers  \holonet\common\config\parsers\JsonConfigParser
    -	 * @covers  \holonet\common\config\parsers\PhpConfigParser
    -	 */
    -	public function testParseFiles($file): void {
    +	#[DataProvider('configTestProvider')]
    +	public function testParseFiles(string $file): void {
     		$expectedData = array('toplevel' => 'value', 'sublevel' => array('config' => 'sub'));
    
     		$configreader = new ConfigReader();
  • Changed file ConfigRegistryTest.php
    diff --git a/7bed3cc4d042d4745f9961d325b6f4c9c169e0fb b/3ea67ae6b991f4e14968e807b579c09577823dec
    index 7bed3cc..3ea67ae 100644
    --- a/7bed3cc4d042d4745f9961d325b6f4c9c169e0fb
    +++ b/3ea67ae6b991f4e14968e807b579c09577823dec
    @@ -10,17 +10,18 @@
     namespace holonet\common\tests;
    
     use PHPUnit\Framework\TestCase;
    +use holonet\common\collection\Registry;
     use holonet\common\config\ConfigRegistry;
    +use PHPUnit\Framework\Attributes\CoversClass;
    +use PHPUnit\Framework\Attributes\CoversFunction;
     use holonet\common\error\BadEnvironmentException;
     use holonet\common\verifier\rules\string\MinLength;
     use holonet\common\verifier\rules\string\ExactLength;
    
    -/**
    - * @covers  \holonet\common\config\ConfigRegistry
    - * @covers  \holonet\common\collection\Registry
    - * @covers  \holonet\common\dot_key_get()
    - * @covers  \holonet\common\dot_key_set()
    - */
    +#[CoversClass(ConfigRegistry::class)]
    +#[CoversClass(Registry::class)]
    +#[CoversFunction('holonet\common\dot_key_get')]
    +#[CoversFunction('holonet\common\dot_key_set')]
     class ConfigRegistryTest extends TestCase {
     	protected function setUp(): void {
     		$_ENV['ENV_VALUE'] = 'cool env config value';
    @@ -55,7 +56,7 @@ class ConfigRegistryTest extends TestCase {
    
     	public function testVerifiedDto(): void {
     		$this->expectException(BadEnvironmentException::class);
    -		$this->expectExceptionMessage('Faulty config with key test.testProp: testProp must be exactly 11 characters long');
    +		$this->expectExceptionMessage('Faulty config with key \'test.testProp\': testProp must be exactly 11 characters long');
    
     		$dto = new class() {
     			public function __construct(
    @@ -73,7 +74,7 @@ class ConfigRegistryTest extends TestCase {
    
     	public function testVerifiedDtoTypeError(): void {
     		$this->expectException(BadEnvironmentException::class);
    -		$this->expectExceptionMessage('Faulty config with key test: TypeError: Cannot assign array to property class@anonymous::$testProp of type string');
    +		$this->expectExceptionMessage('Faulty config with key \'test\': TypeError: Cannot assign array to property class@anonymous::$testProp of type string');
    
     		$dto = new class() {
     			public function __construct(
  • Removed file DiContainerTest.php
  • Changed file FunctionsTest.php
    diff --git a/e28d3153661a1139572c179a2e39815ade95df72 b/cbf6f01cd8edf9ded6f164c75e0de2f6bf2868f0
    index e28d315..cbf6f01 100644
    --- a/e28d3153661a1139572c179a2e39815ade95df72
    +++ b/cbf6f01cd8edf9ded6f164c75e0de2f6bf2868f0
    @@ -14,22 +14,22 @@ use holonet\common as co;
     use PHPUnit\Framework\TestCase;
     use holonet\common\verifier\Proof;
     use function holonet\common\verify;
    +use holonet\common\FilesystemUtils;
     use holonet\common\verifier\Verifier;
     use holonet\common\verifier\rules\Required;
    +use PHPUnit\Framework\Attributes\CoversClass;
     use function holonet\common\get_absolute_path;
    +use holonet\common\code\FileUseStatementParser;
    +use PHPUnit\Framework\Attributes\CoversFunction;
    
    -/**
    - * @coversNothing
    - */
    +#[CoversClass(FileUseStatementParser::class)]
    +#[CoversClass(FilesystemUtils::class)]
    +#[CoversFunction('holonet\common\verify')]
     class FunctionsTest extends TestCase {
     	protected function tearDown(): void {
     		verify(new stdClass(), new Verifier());
     	}
    
    -	/**
    -	 * @covers \holonet\common\FilesystemUtils::dirpath()
    -	 * @covers \holonet\common\FilesystemUtils::filepath()
    -	 */
     	public function testAbsolutePaths(): void {
     		$expected = implode(\DIRECTORY_SEPARATOR, array(__DIR__, 'subfolder', 'subsubfolder')).\DIRECTORY_SEPARATOR;
     		$this->assertSame($expected, co\FilesystemUtils::dirpath(__DIR__, 'subfolder', 'subsubfolder'));
    @@ -38,10 +38,6 @@ class FunctionsTest extends TestCase {
     		$this->assertSame($expected, co\FilesystemUtils::filepath(__DIR__, 'subfolder', 'subsubfolder', 'test.txt'));
     	}
    
    -	/**
    -	 * @covers \holonet\common\file_get_use_statements()
    -	 * @covers \holonet\common\code\FileUseStatementParser
    -	 */
     	public function testFileGetUseStatements(): void {
     		$this->assertSame(array(
     			'class' => array(
    @@ -70,19 +66,10 @@ class FunctionsTest extends TestCase {
     		), co\file_get_use_statements(__DIR__.'/data/use_statements.txt'));
     	}
    
    -	/**
    -	 * @covers \holonet\common\get_absolute_path()
    -	 */
     	public function testGetAbsolutePath(): void {
     		$this->assertSame('this/a/test/is', get_absolute_path('this/is/../a/./test/./is'));
     	}
    
    -	/**
    -	 * @covers \holonet\common\FilesystemUtils::dirpath()
    -	 * @covers \holonet\common\FilesystemUtils::filepath()
    -	 * @covers \holonet\common\FilesystemUtils::reldirpath()
    -	 * @covers \holonet\common\FilesystemUtils::relfilepath()
    -	 */
     	public function testRelativePaths(): void {
     		$expected = implode(\DIRECTORY_SEPARATOR, array(__DIR__, 'subfolder', 'subsubfolder')).\DIRECTORY_SEPARATOR;
     		$this->assertSame($expected, co\FilesystemUtils::reldirpath('subfolder', 'subsubfolder'));
    @@ -91,26 +78,6 @@ class FunctionsTest extends TestCase {
     		$this->assertSame($expected, co\FilesystemUtils::relfilepath('subfolder', 'subsubfolder', 'test.txt'));
     	}
    
    -	/**
    -	 * @covers \holonet\common\trigger_error_context()
    -	 */
    -	public function testTriggerErrorContext(): void {
    -		$msg = '';
    -
    -		try {
    -			$line = (__LINE__) + 1;
    -			co\trigger_error_context('oh nos');
    -		} catch (\PHPUnit\Exception $e) {
    -			$msg = $e->getMessage();
    -		}
    -
    -		$expected = 'oh nos in file '.__FILE__." on line {$line}";
    -		$this->assertSame($expected, $msg);
    -	}
    -
    -	/**
    -	 * @covers \holonet\common\verify()
    -	 */
     	public function testVerifierCanBeInjectedIntoVerify(): void {
     		$test = new class() {
     			#[Required]
  • Changed file RegistryTest.php
    diff --git a/a583344fb79c4c8018111f00a36bac4b011270d9 b/9f92571b617b7c98d18637656df09c9e29452e81
    index a583344..9f92571 100644
    --- a/a583344fb79c4c8018111f00a36bac4b011270d9
    +++ b/9f92571b617b7c98d18637656df09c9e29452e81
    @@ -11,12 +11,12 @@ namespace holonet\common\tests;
    
     use PHPUnit\Framework\TestCase;
     use holonet\common\collection\Registry;
    +use PHPUnit\Framework\Attributes\CoversClass;
    +use PHPUnit\Framework\Attributes\CoversFunction;
    
    -/**
    - * @covers  \holonet\common\collection\Registry
    - * @covers  \holonet\common\dot_key_get()
    - * @covers  \holonet\common\dot_key_set()
    - */
    +#[CoversClass(Registry::class)]
    +#[CoversFunction('holonet\common\dot_key_get')]
    +#[CoversFunction('holonet\common\dot_key_set')]
     class RegistryTest extends TestCase {
     	public function testArrayAccessCalls(): void {
     		$registry = new Registry();
  • Created new file ConfigAutoWireProviderTest.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;
    
    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(Container::class)]
    #[CoversClass(ConfigAutoWireProvider::class)]
    #[CoversClass(AutoWire::class)]
    #[CoversClass(ConfigItem::class)]
    class ConfigAutoWireProviderTest extends TestCase {
    	public function testConfigItemInjectionKeyInArguments(): void {
    		$container = new Container();
    
    		$container->registry->set('service.config', array('stringValue' => 'test'));
    
    		$result = $container->make(Dependency::class, array('config' => 'service.config'));
    
    		$this->assertSame('test', $result->config->stringValue);
    	}
    
    	public function testConfigItemInjectionKeyInAttribute(): void {
    		$container = new Container();
    
    		$container->registry->set('service.other', array('stringValue' => 'test'));
    
    		$result = $container->make(OtherDependency::class);
    
    		$this->assertSame('test', $result->config->stringValue);
    	}
    
    	public function testConfigItemIsVerified(): void {
    		$this->expectException(BadEnvironmentException::class);
    		$this->expectExceptionMessage('Faulty config with key \'service.config.stringValue\': stringValue must be at most 10 characters long');
    
    		$container = new Container();
    
    		$container->registry->set('service.config', array('stringValue' => 'test_longer_than_10'));
    
    		$container->make(OtherDependency::class, array('config' => 'service.config'));
    	}
    
    	public function testInjectArrayConfigValue(): void {
    		$container = new Container();
    
    		$value = array('test', 'cool');
    		$container->registry->set('config.just_an_array_value', $value);
    
    		$result = $container->make(ServiceWithArrayConfigValue::class);
    
    		$this->assertSame($value, $result->value);
    	}
    
    	public function testInjectStringConfigValue(): void {
    		$container = new Container();
    
    		$container->registry->set('config.just_a_string_value', 'configured method');
    
    		$result = $container->make(ServiceWithStringConfigValue::class);
    
    		$this->assertSame('configured method', $result->value);
    	}
    
    	public function testPropertyWithoutAttributeIsIgnored(): void {
    		$container = new Container();
    
    		$result = $container->make(ClassWithoutAttribute::class);
    
    		$this->assertNotNull($result);
    	}
    
    	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');
    
    		$container = new Container();
    
    		$container->make(Dependency::class);
    	}
    }
    
    class ClassWithoutAttribute {
    	public function __construct(Simple $config) {
    	}
    }
    
    class Simple {
    }
    
    class Dependency {
    	public function __construct(
    		#[ConfigItem(verified: false)]
    		public Config $config
    	) {
    	}
    }
    
    class OtherDependency {
    	public function __construct(
    		#[ConfigItem(key: 'service.other')]
    		public Config $config
    	) {
    	}
    }
    
    class ServiceWithStringConfigValue {
    	public function __construct(
    		#[ConfigItem(key: 'config.just_a_string_value')]
    		public string $value
    	) {
    	}
    }
    
    class ServiceWithArrayConfigValue {
    	public function __construct(
    		#[ConfigItem(key: 'config.just_an_array_value')]
    		public array $value
    	) {
    	}
    }
    
    class Config {
    	public function __construct(
    		#[MaxLength(10)]
    		public string $stringValue,
    		public array $array = array()) {
    	}
    }
  • Created new file ContainerAutoWireProviderTest.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;
    
    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\DependencyInjectionException;
    use holonet\common\di\autowire\provider\ContainerAutoWireProvider;
    
    #[CoversClass(Container::class)]
    #[CoversClass(ContainerAutoWireProvider::class)]
    #[CoversClass(AutoWire::class)]
    #[CoversClass(AutoWireException::class)]
    #[CoversClass(DependencyInjectionException::class)]
    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\'');
    
    		$container = new Container();
    
    		$container->make(ServiceMultipleVersionDependencies::class);
    	}
    
    	public function testInjectionIgnoresMissingOptionalParams(): void {
    		$container = new Container();
    
    		$result = $container->make(ServiceOptionalDep::class);
    
    		$this->assertNull($result->dependency);
    		$this->assertNotNull($result->optional);
    	}
    
    	public function testInjectionOfMultipleVersionsOfService(): void {
    		$container = new Container();
    
    		$serviceOne = new DependencyWithParameter('string_one');
    		$serviceTwo = new DependencyWithParameter('string_two');
    
    		$container->set('serviceOne', $serviceOne);
    		$container->set('serviceTwo', $serviceTwo);
    
    		$result = $container->make(ServiceMultipleVersionDependencies::class);
    
    		$this->assertSame($serviceOne, $result->serviceOne);
    		$this->assertSame($serviceTwo, $result->serviceTwo);
    	}
    
    	public function testInjectionUsingMake(): void {
    		$container = new Container();
    
    		$container->wire(DependencyWithParameter::class, array('mustBeSuppliedParameter' => 'test_string'));
    
    		$one = $container->make(ServiceWithDependencyInjectUsingMake::class);
    		$two = $container->make(ServiceWithDependencyInjectUsingMake::class);
    
    		// because it's injected using make, they should be two different instances
    		$this->assertTrue($one->dep !== $two->dep);
    		$this->assertSame('test_string', $one->dep->mustBeSuppliedParameter);
    		$this->assertSame('test_string', $two->dep->mustBeSuppliedParameter);
    	}
    
    	public function testServiceInjection(): void {
    		$container = new Container();
    
    		$dependency = new DependencyContainerAutoWire();
    		$container->set('dependency', $dependency);
    
    		$result = $container->make(Service::class);
    
    		$this->assertSame($dependency, $result->dependency);
    	}
    }
    
    class ServiceMultipleVersionDependencies {
    	public function __construct(public DependencyWithParameter $serviceOne, public DependencyWithParameter $serviceTwo) {
    	}
    }
    
    class ServiceOptionalDep {
    	public function __construct(public ?DependencyWithParameter $dependency = null, public DependencyWithParameter $optional = new DependencyWithParameter('test')) {
    	}
    }
    
    class Service {
    	public function __construct(public DependencyContainerAutoWire $dependency) {
    	}
    }
    
    class ServiceWithDependencyInjectUsingMake {
    	public function __construct(public DependencyWithParameter $dep) {
    	}
    }
    
    class DependencyContainerAutoWire {
    	public function __construct() {
    	}
    }
    
    class DependencyWithParameter {
    	public function __construct(public string $mustBeSuppliedParameter) {
    	}
    }
  • Created new file ContainerTest.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;
    
    use Countable;
    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(AutoWire::class)]
    #[CoversClass(AutoWireException::class)]
    #[CoversClass(DependencyInjectionException::class)]
    class ContainerTest extends TestCase {
    	public function testGetNonExistingDependency(): void {
    		$this->expectException(DependencyNotFoundException::class);
    		$this->expectExceptionMessage("Container has no named dependency called 'kaudermelsh'");
    
    		$container = new Container();
    		$container->get('kaudermelsh');
    	}
    
    	public function testInjectionWithConstructor(): void {
    		$container = new Container();
    		$container->set('anonDep', DiAnonDep::class);
    		$container->set('anonClassTwo', DiAnonClassTwo::class);
    
    		$this->assertSame('test', $container->get('anonClassTwo')->test);
    	}
    
    	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');
    
    		$container = new Container();
    
    		$container->make(IntersectionTypes::class);
    	}
    
    	public function testLazyLoadReturnsOneInstance(): void {
    		$container = new Container();
    		$container->set('anonDep', DiAnonDep::class);
    
    		$one = $container->get('anonDep');
    		$two = $container->get('anonDep');
    
    		$this->assertSame($one, $two);
    	}
    
    	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');
    
    		$container = new Container();
    
    		$container->make(MyInterface::class);
    	}
    
    	public function testMakeReturningGivenInstancesIfAvailable(): void {
    		$container = new Container();
    
    		$override = new DiAnonDep();
    		$container->set(DiAnonDep::class, $override);
    
    		$this->assertSame($override, $container->make(DiAnonDep::class));
    	}
    
    	public function testMakeReturnsNewInstanceEveryCall(): void {
    		$container = new Container();
    
    		$one = $container->make(DiAnonDep::class);
    		$two = $container->make(DiAnonDep::class);
    
    		$this->assertNotSame($one, $two);
    	}
    
    	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\'');
    
    		$container = new Container();
    
    		$container->make(SomeService::class);
    	}
    
    	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');
    
    		$container = new Container();
    
    		$container->make(DiAnonDep::class, array('cool'));
    	}
    
    	public function testMultipleInstancesOfServiceBothAvailable(): void {
    		$one = new DiAnonDep();
    		$two = new DiAnonDep();
    
    		$this->assertNotSame($one, $two);
    
    		$container = new Container();
    		$container->set('config_one', $one);
    		$container->set('config_two', $two);
    
    		// both should be available by their ids
    		$this->assertSame($one, $container->get('config_one'));
    		$this->assertSame($two, $container->get('config_two'));
    
    		// both should be available by their type when supplying a name hint
    		$this->assertSame($one, $container->byType(DiAnonDep::class, 'config_one'));
    		$this->assertNotSame($two, $container->byType(DiAnonDep::class, 'config_one'));
    		$this->assertSame($one, $container->byType(DiAnonDep::class, 'config_one'));
    		$this->assertNotSame($two, $container->byType(DiAnonDep::class, 'config_one'));
    
    		// 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');
    		$container->byType(DiAnonDep::class);
    	}
    
    	public function testOptionalTypelessParametersAreIgnored(): void {
    		$container = new Container();
    
    		$result = $container->make(TypelessClassOptional::class);
    
    		$this->assertSame('test', $result->test);
    	}
    
    	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');
    
    		$container = new Container();
    
    		$container->make(RecursionA::class);
    	}
    
    	public function testRecursionDetectionServiceGet(): void {
    		$this->expectException(DependencyInjectionException::class);
    		$this->expectExceptionMessage('Recursive dependency definition detected: A => B => C');
    
    		$container = new Container();
    
    		$container->set('A', RecursionA::class);
    		$container->set('B', RecursionB::class);
    		$container->set('C', RecursionC::class);
    
    		$container->get('A');
    	}
    
    	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');
    
    		$container = new Container();
    
    		$container->make(TypelessClass::class);
    	}
    
    	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'
    		Message
    		);
    
    		$container = new Container();
    
    		$container->make(UnionTypesMultipleFailures::class);
    	}
    
    	public function testWireNonExistingClassThrowsError(): void {
    		$this->expectException(DependencyInjectionException::class);
    		$this->expectExceptionMessage('Could not auto-wire abstract \'\nonsense\class\TestClass\': class does not exist');
    
    		$container = new Container();
    
    		$container->wire('\\nonsense\\class\\TestClass');
    	}
    
    	public function testWireParametersGetAppliedWiring(): void {
    		$container = new Container();
    
    		$container->wire(SomeService::class, array('parameter' => 'test'));
    
    		$one = $container->make(SomeService::class);
    		$two = $container->make(SomeService::class);
    
    		$this->assertNotSame($one, $two);
    		$this->assertTrue($one->parameter === $two->parameter);
    	}
    
    	public function testWiringAnInterfaceToAnImplementation(): void {
    		$container = new Container();
    
    		$container->wire(TestClass::class, abstract: MyInterface::class);
    		$container->wire(TestClass::class, abstract: AbstractBaseClass::class);
    
    		$this->assertInstanceOf(TestClass::class, $container->make(MyInterface::class));
    		$this->assertInstanceOf(TestClass::class, $container->make(AbstractBaseClass::class));
    	}
    }
    
    class RecursionA {
    	public function __construct(RecursionB $recursionB) {
    	}
    }
    
    class RecursionB {
    	public function __construct(RecursionC $recursionC) {
    	}
    }
    
    class RecursionC {
    	public function __construct(RecursionA $recursionA) {
    	}
    }
    
    class UnionTypesMultipleFailures {
    	public function __construct(SomeService|SomeServiceTwo $service) {
    	}
    }
    
    class IntersectionTypes {
    	public function __construct(Countable&Stringable $intersection) {
    	}
    }
    
    class TypelessClass {
    	public function __construct(public $test) {
    	}
    }
    
    class TypelessClassOptional {
    	public function __construct(public $test = 'test') {
    	}
    }
    
    class DiAnonClassTwo {
    	public string $test;
    
    	public function __construct(public DiAnonDep $anonDep) {
    		$this->test = $this->anonDep->test();
    	}
    }
    
    class DiAnonDep {
    	public function test(): string {
    		return 'test';
    	}
    }
    
    class SomeService {
    	public function __construct(public string $parameter) {
    	}
    }
    
    class SomeServiceTwo {
    	public function __construct(public string $parameter) {
    	}
    }
    
    abstract class AbstractBaseClass {
    }
    
    class TestClass extends AbstractBaseClass implements MyInterface {
    }
    
    interface MyInterface {
    }
  • Created new file ForwardAutoWireProviderTest.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;
    
    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\DependencyInjectionException;
    use holonet\common\di\autowire\provider\ForwardAutoWireProvider;
    
    #[CoversClass(Container::class)]
    #[CoversClass(ForwardAutoWireProvider::class)]
    #[CoversClass(AutoWire::class)]
    #[CoversClass(AutoWireException::class)]
    #[CoversClass(DependencyInjectionException::class)]
    class ForwardAutoWireProviderTest extends TestCase {
    	public function testBasicScalarParameterForwarding(): void {
    		$container = new Container();
    
    		$params = array(
    			'string' => 'gojsdgoisjdgio',
    			'int' => 5,
    			'float' => 10.5,
    			'boolean' => true,
    			'array' => array('value1', 'value2')
    		);
    
    		$result = $container->make(DependencyForwardAutoWire::class, $params);
    
    		$this->assertSame($params, get_object_vars($result));
    	}
    
    	public function testObjectTypesForwarding(): void {
    		$container = new Container();
    
    		$apples = new Apples();
    		$result = $container->make(DependencyWithObjectParam::class, array('apples' => $apples));
    
    		$this->assertSame($apples, $result->apples);
    	}
    
    	public function testObjectTypesForwardingUnionTypes(): void {
    		$container = new Container();
    
    		$apples = new Apples();
    		$other = new DependencyWithUnionTypeHints();
    
    		$one = $container->make(DependencyWithUnionTypeHintsObjects::class, array('other' => $apples));
    		$two = $container->make(DependencyWithUnionTypeHintsObjects::class, array('other' => $other));
    
    		$this->assertSame($apples, $one->other);
    		$this->assertSame($other, $two->other);
    	}
    
    	public function testScalarUnionTypesForwarding(): void {
    		$container = new Container();
    
    		$one = $container->make(DependencyWithUnionTypeHints::class, array('testUnion' => 'string_value'));
    		$two = $container->make(DependencyWithUnionTypeHints::class, array('testUnion' => 5.4));
    
    		$this->assertSame('string_value', $one->testUnion);
    		$this->assertSame(5.4, $two->testUnion);
    	}
    }
    
    class DependencyForwardAutoWire {
    	public function __construct(public string $string, public int $int, public float $float, public bool $boolean, public array $array) {
    	}
    }
    
    class DependencyWithObjectParam {
    	public function __construct(public Apples $apples) {
    	}
    }
    
    class Apples {
    }
    
    class DependencyWithUnionTypeHints {
    	public function __construct(public string|float $testUnion = 5.0) {
    	}
    }
    
    class DependencyWithUnionTypeHintsObjects {
    	public function __construct(public Apples|DependencyWithUnionTypeHints $other) {
    	}
    }
  • Changed file BadEnvironmentExceptionTest.php
    diff --git a/daafa15b0100ba09f9842716b0304e40123c4494 b/74bf67637083c186bc49cabdc05304c19bdd4a9d
    index daafa15..74bf676 100644
    --- a/daafa15b0100ba09f9842716b0304e40123c4494
    +++ b/74bf67637083c186bc49cabdc05304c19bdd4a9d
    @@ -12,25 +12,24 @@ namespace holonet\common\tests\error;
     use PHPUnit\Framework\TestCase;
     use holonet\common\verifier\Proof;
     use function holonet\common\stringify;
    +use PHPUnit\Framework\Attributes\CoversClass;
     use holonet\common\error\BadEnvironmentException;
    
    -/**
    - * @covers \holonet\common\error\BadEnvironmentException
    - */
    +#[CoversClass(BadEnvironmentException::class)]
     class BadEnvironmentExceptionTest extends TestCase {
     	public function testFaultyConfigFactoryMethod(): void {
     		$proof = new Proof();
     		$proof->add('guard_enabled', 'guard_enabled is required');
    
     		$ex = BadEnvironmentException::faultyConfigFromProof('app.auth', $proof);
    -		$this->assertSame('Faulty config with key app.auth.guard_enabled: guard_enabled is required', $ex->getMessage());
    +		$this->assertSame('Faulty config with key \'app.auth.guard_enabled\': guard_enabled is required', $ex->getMessage());
    
     		$proof->add('guard_enabled', 'guard_enabled is invalid');
     		$ex = BadEnvironmentException::faultyConfigFromProof('app.auth', $proof);
    -		$this->assertSame(sprintf('Faulty config with key app.auth.guard_enabled: %s', stringify($proof->attr('guard_enabled'), true)), $ex->getMessage());
    +		$this->assertSame(sprintf('Faulty config with key \'app.auth.guard_enabled\': %s', stringify($proof->attr('guard_enabled'), true)), $ex->getMessage());
    
     		$proof->add('handler', 'handler must be a subclass of Handler');
     		$ex = BadEnvironmentException::faultyConfigFromProof('app.auth', $proof);
    -		$this->assertSame(sprintf('Faulty config with key app.auth: %s', stringify($proof->all(), true)), $ex->getMessage());
    +		$this->assertSame(sprintf('Faulty config with key \'app.auth\': %s', stringify($proof->all(), true)), $ex->getMessage());
     	}
     }
  • Changed file BaseVerifyTest.php
    diff --git a/494b15fbc2a78b7ad13f8b8265a89dda5768f2ec b/f00281b8c26289957e76b9af1a86a002958a67d3
    index 494b15f..f00281b 100644
    --- a/494b15fbc2a78b7ad13f8b8265a89dda5768f2ec
    +++ b/f00281b8c26289957e76b9af1a86a002958a67d3
    @@ -15,15 +15,14 @@ use holonet\common\verifier\Proof;
     use function holonet\common\verify;
     use function holonet\common\stringify;
     use holonet\common\verifier\rules\Rule;
    +use PHPUnit\Framework\Attributes\CoversClass;
     use holonet\common\verifier\rules\CheckValueRuleInterface;
     use holonet\common\verifier\rules\TransformValueRuleInterface;
    
    -/**
    - * @covers \holonet\common\verifier\rules\Rule
    - * @covers \holonet\common\verifier\rules\CheckValueRuleInterface
    - * @covers \holonet\common\verifier\rules\TransformValueRuleInterface
    - */
    -abstract class BaseVerifyTest extends TestCase {
    +#[CoversClass(Rule::class)]
    +#[CoversClass(CheckValueRuleInterface::class)]
    +#[CoversClass(TransformValueRuleInterface::class)]
    +class BaseVerifyTest extends TestCase {
     	public function assertProofContainsError(Proof $actual, string $attr, string $error): void {
     		$this->assertContains($error, $actual->flat());
     		$this->assertArrayHasKey($attr, $actual->all());
  • Changed file ProofTest.php
    diff --git a/77af731f282bf9754bc120e887ba614897aedcfa b/4236f2cd6fda4a1f812b902935aaa7b4d5b48792
    index 77af731..4236f2c 100644
    --- a/77af731f282bf9754bc120e887ba614897aedcfa
    +++ b/4236f2cd6fda4a1f812b902935aaa7b4d5b48792
    @@ -11,10 +11,9 @@ namespace holonet\common\tests\verifier;
    
     use PHPUnit\Framework\TestCase;
     use holonet\common\verifier\Proof;
    +use PHPUnit\Framework\Attributes\CoversClass;
    
    -/**
    - * @covers \holonet\common\verifier\Proof
    - */
    +#[CoversClass(Proof::class)]
     class ProofTest extends TestCase {
     	public function testProofErrorBag(): void {
     		$proof = new Proof();
  • Changed file VerifyFilesystemRulesTest.php
    diff --git a/33e9b2e50f013fff52b5943d80512ae840b6a87e b/ea9b6e1d69b34346bc4fbed11a3a2e4ace7d9245
    index 33e9b2e..ea9b6e1 100644
    --- a/33e9b2e50f013fff52b5943d80512ae840b6a87e
    +++ b/ea9b6e1d69b34346bc4fbed11a3a2e4ace7d9245
    @@ -10,20 +10,22 @@
     namespace holonet\common\tests\verifier;
    
     use function holonet\common\verify;
    +use holonet\common\verifier\Verifier;
    +use holonet\common\verifier\rules\Rule;
    +use PHPUnit\Framework\Attributes\CoversClass;
    +use holonet\common\verifier\rules\filesystem\PathRule;
     use holonet\common\verifier\rules\filesystem\Readable;
     use holonet\common\verifier\rules\filesystem\Writable;
     use holonet\common\verifier\rules\filesystem\Directory;
     use holonet\common\verifier\rules\filesystem\ValidPath;
    
    -/**
    - * @covers \holonet\common\verifier\Verifier
    - * @covers \holonet\common\verifier\rules\Rule
    - * @covers \holonet\common\verifier\rules\filesystem\ValidPath
    - * @covers \holonet\common\verifier\rules\filesystem\PathRule
    - * @covers \holonet\common\verifier\rules\filesystem\Readable
    - * @covers \holonet\common\verifier\rules\filesystem\Writable
    - * @covers \holonet\common\verifier\rules\filesystem\Directory
    - */
    +#[CoversClass(Verifier::class)]
    +#[CoversClass(Rule::class)]
    +#[CoversClass(ValidPath::class)]
    +#[CoversClass(PathRule::class)]
    +#[CoversClass(Readable::class)]
    +#[CoversClass(Writable::class)]
    +#[CoversClass(Directory::class)]
     class VerifyFilesystemRulesTest extends BaseVerifyTest {
     	public function testCheckPathIsDirectory(): void {
     		$test = new class('/path/surely/doesnt/exist') {
  • Changed file VerifyInArrayTest.php
    diff --git a/71231eaba38753c4f9e242c9c0917e9ae72510c4 b/ada4f330c646b72f2899c4e9d286b8f032b87d6f
    index 71231ea..ada4f33 100644
    --- a/71231eaba38753c4f9e242c9c0917e9ae72510c4
    +++ b/ada4f330c646b72f2899c4e9d286b8f032b87d6f
    @@ -10,13 +10,14 @@
     namespace holonet\common\tests\verifier;
    
     use function holonet\common\verify;
    +use holonet\common\verifier\Verifier;
    +use holonet\common\verifier\rules\Rule;
     use holonet\common\verifier\rules\InArray;
    +use PHPUnit\Framework\Attributes\CoversClass;
    
    -/**
    - * @covers \holonet\common\verifier\Verifier
    - * @covers \holonet\common\verifier\rules\Rule
    - * @covers \holonet\common\verifier\rules\InArray
    - */
    +#[CoversClass(Verifier::class)]
    +#[CoversClass(Rule::class)]
    +#[CoversClass(InArray::class)]
     class VerifyInArrayTest extends BaseVerifyTest {
     	public function testCheckInArray(): void {
     		$test = new class('itsy bitsy') {
  • Changed file VerifyNumericRulesTest.php
    diff --git a/1a9c5ea9b7a9ab83db793232cf68ae8a02e7c5a3 b/c752a6114e923f168061e07cd99c7e83d9974e86
    index 1a9c5ea..c752a61 100644
    --- a/1a9c5ea9b7a9ab83db793232cf68ae8a02e7c5a3
    +++ b/c752a6114e923f168061e07cd99c7e83d9974e86
    @@ -10,19 +10,20 @@
     namespace holonet\common\tests\verifier;
    
     use function holonet\common\verify;
    +use holonet\common\verifier\Verifier;
    +use holonet\common\verifier\rules\Rule;
    +use PHPUnit\Framework\Attributes\CoversClass;
     use holonet\common\verifier\rules\numeric\Between;
     use holonet\common\verifier\rules\numeric\Maximum;
     use holonet\common\verifier\rules\numeric\Minimum;
     use holonet\common\verifier\rules\numeric\Numeric;
    
    -/**
    - * @covers \holonet\common\verifier\Verifier
    - * @covers \holonet\common\verifier\rules\Rule
    - * @covers \holonet\common\verifier\rules\numeric\Between
    - * @covers \holonet\common\verifier\rules\numeric\Maximum
    - * @covers \holonet\common\verifier\rules\numeric\Minimum
    - * @covers \holonet\common\verifier\rules\numeric\Numeric
    - */
    +#[CoversClass(Verifier::class)]
    +#[CoversClass(Rule::class)]
    +#[CoversClass(Between::class)]
    +#[CoversClass(Maximum::class)]
    +#[CoversClass(Minimum::class)]
    +#[CoversClass(Numeric::class)]
     class VerifyNumericRulesTest extends BaseVerifyTest {
     	public function testCheckBetween(): void {
     		$test = new class(22) {
  • Changed file VerifyRequiredTest.php
    diff --git a/80b5f71e13fd549713985f1a8d8566a5e9f6e0f2 b/584d677f73277b98041cc2d3210d6473b334148f
    index 80b5f71..584d677 100644
    --- a/80b5f71e13fd549713985f1a8d8566a5e9f6e0f2
    +++ b/584d677f73277b98041cc2d3210d6473b334148f
    @@ -10,12 +10,12 @@
     namespace holonet\common\tests\verifier;
    
     use function holonet\common\verify;
    +use holonet\common\verifier\Verifier;
     use holonet\common\verifier\rules\Required;
    +use PHPUnit\Framework\Attributes\CoversClass;
    
    -/**
    - * @covers \holonet\common\verifier\Verifier
    - * @covers \holonet\common\verifier\rules\Required
    - */
    +#[CoversClass(Verifier::class)]
    +#[CoversClass(Required::class)]
     class VerifyRequiredTest extends BaseVerifyTest {
     	public function testCheckForRequiredAfterUnset(): void {
     		$test = new class('test') {
  • Changed file VerifyStringRulesTest.php
    diff --git a/fc743d37e7f225b85240df507706039762c9dcd6 b/3eab70ae7a1d94c088dd310f8a848135c6655100
    index fc743d3..3eab70a 100644
    --- a/fc743d37e7f225b85240df507706039762c9dcd6
    +++ b/3eab70ae7a1d94c088dd310f8a848135c6655100
    @@ -10,21 +10,22 @@
     namespace holonet\common\tests\verifier;
    
     use function holonet\common\verify;
    +use holonet\common\verifier\Verifier;
    +use holonet\common\verifier\rules\Rule;
    +use PHPUnit\Framework\Attributes\CoversClass;
     use holonet\common\verifier\rules\string\Pattern;
     use holonet\common\verifier\rules\string\MaxLength;
     use holonet\common\verifier\rules\string\MinLength;
     use holonet\common\verifier\rules\string\ExactLength;
     use holonet\common\verifier\rules\string\LengthBetween;
    
    -/**
    - * @covers \holonet\common\verifier\Verifier
    - * @covers \holonet\common\verifier\rules\Rule
    - * @covers \holonet\common\verifier\rules\string\MaxLength
    - * @covers \holonet\common\verifier\rules\string\MinLength
    - * @covers \holonet\common\verifier\rules\string\ExactLength
    - * @covers \holonet\common\verifier\rules\string\LengthBetween
    - * @covers \holonet\common\verifier\rules\string\Pattern
    - */
    +#[CoversClass(Verifier::class)]
    +#[CoversClass(Rule::class)]
    +#[CoversClass(MaxLength::class)]
    +#[CoversClass(MinLength::class)]
    +#[CoversClass(ExactLength::class)]
    +#[CoversClass(LengthBetween::class)]
    +#[CoversClass(Pattern::class)]
     class VerifyStringRulesTest extends BaseVerifyTest {
     	public function testCheckExactLength(): void {
     		$test = new class('itsy bitsy') {