diff --git a/.docker/.env b/.docker/.env new file mode 100644 index 0000000..81ff041 --- /dev/null +++ b/.docker/.env @@ -0,0 +1 @@ +PROJECT_NAME= diff --git a/.docker/docker-compose.yml b/.docker/docker-compose.yml new file mode 100644 index 0000000..a2bc9b5 --- /dev/null +++ b/.docker/docker-compose.yml @@ -0,0 +1,29 @@ +x-php-base: &php-base + build: + context: . + dockerfile: php/Dockerfile + container_name: ${PROJECT_NAME:-package}_php + volumes: + - ../:/app + environment: + UID: "${UID:-1000}" + GID: "${GID:-1000}" + working_dir: /app + +services: + php: + <<: *php-base + build: + context: . + dockerfile: php/Dockerfile + args: + PHP_VERSION: 7.4 + container_name: ${PROJECT_NAME:-package}_php74 + php83: + <<: *php-base + build: + context: . + dockerfile: php/Dockerfile + args: + PHP_VERSION: 8.3 + container_name: ${PROJECT_NAME:-package}_php83 diff --git a/.docker/php/Dockerfile b/.docker/php/Dockerfile new file mode 100644 index 0000000..7f11927 --- /dev/null +++ b/.docker/php/Dockerfile @@ -0,0 +1,27 @@ +ARG PHP_VERSION + +FROM php:${PHP_VERSION}-cli + +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + unzip \ + zip \ + libzip-dev \ + libonig-dev \ + && docker-php-ext-install zip \ + && pecl install pcov \ + && docker-php-ext-enable pcov \ + && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* + +WORKDIR /app + +COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer + +ARG UID=1000 +ARG GID=1000 +RUN groupadd -g ${GID} appgroup && \ + useradd -u ${UID} -g appgroup -m appuser && \ + chown -R appuser:appgroup /app +USER appuser + +CMD ["php", "-v"] \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4c3840c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,22 @@ + +## Directories to ignore when exporting the package +.docker/ export-ignore +.github/ export-ignore +tests/ export-ignore + +## Files to ignore when exporting the package +/.env export-ignore +/.gitattributes export-ignore +/.gitignore export-ignore +/AGENTS.md export-ignore +/bone.json export-ignore +/codeception.* export-ignore +/codeception.yml export-ignore +/example.php export-ignore +/infection.json.dist export-ignore +/Makefile export-ignore +/phpbench.json export-ignore +/phpcs.xml export-ignore +/phpstan.neon.dist export-ignore +/index.php export-ignore +/rector.php export-ignore diff --git a/.github/workflows/static-analysis.yml b/.github/workflows/static-analysis.yml index 41d71af..d786a95 100644 --- a/.github/workflows/static-analysis.yml +++ b/.github/workflows/static-analysis.yml @@ -22,5 +22,5 @@ jobs: - uses: ramsey/composer-install@v2 - - name: Psalm - run: composer run psalm \ No newline at end of file + - name: PHPStan + run: composer run stan diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index bea3074..a99e1b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,26 +8,40 @@ on: jobs: tests: - name: Test on PHP ${{ matrix.php_versions }} + name: Test on PHP ${{ matrix.php }} runs-on: ubuntu-latest - continue-on-error: ${{ matrix.php_versions == '8.1' }} + continue-on-error: ${{ contains('8.1,8.2,8.5', matrix.php) }} if: "!contains(github.event.head_commit.message, '--skip ci') && !github.event.pull_request.draft" strategy: matrix: - php_versions: ['7.4', '8.0', '8.1'] + php: + - '7.4' + - '8.0' + - '8.1' + - '8.2' + dependencies: + - "lowest" + - "highest" + include: + - php: '8.5' + composer-options: "--ignore-platform-reqs" steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 with: - php-version: ${{ matrix.php_versions }} - coverage: xdebug2 + php-version: ${{ matrix.php }} + coverage: xdebug + ini-values: register_argc_argv=On - - uses: ramsey/composer-install@v2 + - uses: ramsey/composer-install@v3 + with: + dependency-versions: ${{ matrix.dependencies }} + composer-options: ${{ matrix.composer-options }} - name: Run test suite run: vendor/bin/codecept run unit --coverage-text diff --git a/.gitignore b/.gitignore index 1907aa0..d1f300e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +/.vscode/ /vendor/ /_others/ @@ -6,4 +7,5 @@ codeception.yml *.lock c3.php -rector.php \ No newline at end of file +rector.php +bone.json \ No newline at end of file diff --git a/bridge/Injector.php b/bridge/Injector.php deleted file mode 100644 index 4a40c06..0000000 --- a/bridge/Injector.php +++ /dev/null @@ -1,838 +0,0 @@ -reflector = $reflector ?: new CachingReflector(); - } - - public function __clone() - { - $this->inProgressMakes = array(); - } - - /** - * Define instantiation directives for the specified class - * - * @param string $name The class (or alias) whose constructor arguments we wish to define - * @param array $args An array mapping parameter names to values/instructions - * - * @return \Auryn\Injector - */ - public function define($name, array $args) - { - list(, $normalizedName) = $this->resolveAlias($name); - $this->classDefinitions[$normalizedName] = $args; - - return $this; - } - - /** - * Assign a global default value for all parameters named $paramName - * - * Global parameter definitions are only used for parameters with no typehint, pre-defined or - * call-time definition. - * - * @param string $paramName The parameter name for which this value applies - * @param mixed $value The value to inject for this parameter name - * @return self - */ - public function defineParam($paramName, $value) - { - $this->paramDefinitions[$paramName] = $value; - - return $this; - } - - /** - * Define an alias for all occurrences of a given typehint - * - * Use this method to specify implementation classes for interface and abstract class typehints. - * - * @param string $original The typehint to replace - * @param string $alias The implementation name - * @throws ConfigException if any argument is empty or not a string - * @return self - */ - public function alias($original, $alias) - { - if (empty($original) || !is_string($original)) { - throw new ConfigException( - self::M_NON_EMPTY_STRING_ALIAS, - self::E_NON_EMPTY_STRING_ALIAS - ); - } - if (empty($alias) || !is_string($alias)) { - throw new ConfigException( - self::M_NON_EMPTY_STRING_ALIAS, - self::E_NON_EMPTY_STRING_ALIAS - ); - } - - $originalNormalized = $this->normalizeName($original); - - if (isset($this->shares[$originalNormalized])) { - throw new ConfigException( - sprintf( - self::M_SHARED_CANNOT_ALIAS, - $this->normalizeName(get_class($this->shares[$originalNormalized])), - $alias - ), - self::E_SHARED_CANNOT_ALIAS - ); - } - - if (array_key_exists($originalNormalized, $this->shares)) { - $aliasNormalized = $this->normalizeName($alias); - $this->shares[$aliasNormalized] = null; - unset($this->shares[$originalNormalized]); - } - - $this->aliases[$originalNormalized] = $alias; - - return $this; - } - - private function normalizeName($className) - { - return ltrim(strtolower($className), '\\'); - } - - /** - * Share the specified class/instance across the Injector context - * - * @param mixed $nameOrInstance The class or object to share - * @throws ConfigException if $nameOrInstance is not a string or an object - * @return self - */ - public function share($nameOrInstance) - { - if (is_string($nameOrInstance)) { - $this->shareClass($nameOrInstance); - } elseif (is_object($nameOrInstance)) { - $this->shareInstance($nameOrInstance); - } else { - throw new ConfigException( - sprintf( - self::M_SHARE_ARGUMENT, - __CLASS__, - gettype($nameOrInstance) - ), - self::E_SHARE_ARGUMENT - ); - } - - return $this; - } - - private function shareClass($nameOrInstance) - { - list(, $normalizedName) = $this->resolveAlias($nameOrInstance); - $this->shares[$normalizedName] = isset($this->shares[$normalizedName]) - ? $this->shares[$normalizedName] - : null; - } - - private function resolveAlias($name) - { - $normalizedName = $this->normalizeName($name); - if (isset($this->aliases[$normalizedName])) { - $name = $this->aliases[$normalizedName]; - $normalizedName = $this->normalizeName($name); - } - - return array($name, $normalizedName); - } - - private function shareInstance($obj) - { - $normalizedName = $this->normalizeName(get_class($obj)); - if (isset($this->aliases[$normalizedName])) { - // You cannot share an instance of a class name that is already aliased - throw new ConfigException( - sprintf( - self::M_ALIASED_CANNOT_SHARE, - $normalizedName, - $this->aliases[$normalizedName] - ), - self::E_ALIASED_CANNOT_SHARE - ); - } - $this->shares[$normalizedName] = $obj; - } - - /** - * Register a prepare callable to modify/prepare objects of type $name after instantiation - * - * Any callable or provisionable invokable may be specified. Preparers are passed two - * arguments: the instantiated object to be mutated and the current Injector instance. - * - * @param string $name - * @param mixed $callableOrMethodStr Any callable or provisionable invokable method - * @throws InjectionException if $callableOrMethodStr is not a callable. - * See https://github.com/rdlowrey/auryn#injecting-for-execution - * @return self - */ - public function prepare($name, $callableOrMethodStr) - { - if ($this->isExecutable($callableOrMethodStr) === false) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr - ); - } - - list(, $normalizedName) = $this->resolveAlias($name); - $this->prepares[$normalizedName] = $callableOrMethodStr; - - return $this; - } - - private function isExecutable($exe) - { - if (is_callable($exe)) { - return true; - } - if (is_string($exe) && method_exists($exe, '__invoke')) { - return true; - } - if (is_array($exe) && isset($exe[0], $exe[1]) && method_exists($exe[0], $exe[1])) { - return true; - } - - return false; - } - - /** - * Delegate the creation of $name instances to the specified callable, receiving arguments based on the callables - * signature. - * - * @param string $name - * @param mixed $callableOrMethodStr Any callable or provisionable invokable method - * @throws ConfigException if $callableOrMethodStr is not a callable. - * @return self - */ - public function delegate($name, $callableOrMethodStr) - { - if (!$this->isExecutable($callableOrMethodStr)) { - $this->generateInvalidCallableError($callableOrMethodStr); - } - - $normalizedName = $this->normalizeName($name); - $this->delegates[$normalizedName] = $callableOrMethodStr; - - return $this; - } - - /** - * Retrieve stored data for the specified definition type - * - * Exposes introspection of existing binds/delegates/shares/etc for decoration and composition. - * - * @param string $nameFilter An optional class name filter - * @param int $typeFilter A bitmask of Injector::* type constant flags - * @return array - */ - public function inspect($nameFilter = null, $typeFilter = null) - { - $result = array(); - $name = $nameFilter ? $this->normalizeName($nameFilter) : null; - - if (empty($typeFilter)) { - $typeFilter = self::I_ALL; - } - - $types = array( - self::I_BINDINGS => "classDefinitions", - self::I_DELEGATES => "delegates", - self::I_PREPARES => "prepares", - self::I_ALIASES => "aliases", - self::I_SHARES => "shares" - ); - - foreach ($types as $type => $source) { - if ($typeFilter & $type) { - $result[$type] = $this->filter($this->{$source}, $name); - } - } - - return $result; - } - - private function filter($source, $name) - { - if (empty($name)) { - return $source; - } elseif (array_key_exists($name, $source)) { - return array($name => $source[$name]); - } else { - return array(); - } - } - - /** - * Proxy the specified class across the Injector context. - * - * @param string $name The class to proxy - * - * @param $callableOrMethodStr - * @return Injector - * @throws ConfigException - */ - public function proxy(string $name, $callableOrMethodStr) - { - if (!$this->isExecutable($callableOrMethodStr)) { - $this->generateInvalidCallableError($callableOrMethodStr); - } - - list($className, $normalizedName) = $this->resolveAlias($name); - $this->proxies[$normalizedName] = $callableOrMethodStr; - - return $this; - } - - /** - * Instantiate/provision a class instance - * - * @param string $name - * @param array $args - * @throws InjectionException if a cyclic gets detected when provisioning - * @return mixed - */ - public function make($name, array $args = array()) - { - list($className, $normalizedClass) = $this->resolveAlias($name); - - if (isset($this->inProgressMakes[$normalizedClass])) { - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_CYCLIC_DEPENDENCY, - $className - ), - self::E_CYCLIC_DEPENDENCY - ); - } - - $this->inProgressMakes[$normalizedClass] = count($this->inProgressMakes); - - // isset() is used specifically here because classes may be marked as "shared" before an - // instance is stored. In these cases the class is "shared," but it has a null value and - // instantiation is needed. - if (isset($this->shares[$normalizedClass])) { - unset($this->inProgressMakes[$normalizedClass]); - - return $this->shares[$normalizedClass]; - } - - try { - if (isset($this->delegates[$normalizedClass])) { - $executable = $this->buildExecutable($this->delegates[$normalizedClass]); - $reflectionFunction = $executable->getCallableReflection(); - $args = $this->provisionFuncArgs($reflectionFunction, $args, null, $className); - $obj = call_user_func_array(array($executable, '__invoke'), $args); - } elseif (isset($this->proxies[$normalizedClass])) { - if (isset($this->prepares[$normalizedClass])) { - $this->preparesProxy[$normalizedClass] = $this->prepares[$normalizedClass]; - } - $obj = $this->resolveProxy($className, $normalizedClass, $args); - unset($this->prepares[$normalizedClass]); - } else { - $obj = $this->provisionInstance($className, $normalizedClass, $args); - } - - $obj = $this->prepareInstance($obj, $normalizedClass); - - if (array_key_exists($normalizedClass, $this->shares)) { - $this->shares[$normalizedClass] = $obj; - } - - unset($this->inProgressMakes[$normalizedClass]); - } - catch (\Exception $exception) { - unset($this->inProgressMakes[$normalizedClass]); - throw $exception; - } - catch (\Throwable $exception) { - unset($this->inProgressMakes[$normalizedClass]); - throw $exception; - } - - return $obj; - } - - private function resolveProxy(string $className, string $normalizedClass, array $args) - { - $callback = function () use ($className, $normalizedClass, $args) { - return $this->buildWrappedObject( $className, $normalizedClass, $args ); - }; - - $proxy = $this->proxies[$normalizedClass]; - - return $proxy( $className, $callback ); - } - - /** - * @param string $className - * @param string $normalizedClass - * @param array $args - * @return mixed|object - * @throws InjectionException - */ - private function buildWrappedObject( $className, $normalizedClass, array $args ) { - $wrappedObject = $this->provisionInstance( $className, $normalizedClass, $args ); - - if ( isset( $this->preparesProxy[ $normalizedClass ] ) ) { - $this->prepares[ $normalizedClass ] = $this->preparesProxy[ $normalizedClass ]; - } - - return $this->prepareInstance( $wrappedObject, $normalizedClass ); - } - - private function provisionInstance($className, $normalizedClass, array $definition) - { - try { - $ctor = $this->reflector->getCtor($className); - - if (!$ctor) { - $obj = $this->instantiateWithoutCtorParams($className); - } elseif (!$ctor->isPublic()) { - throw new InjectionException( - $this->inProgressMakes, - sprintf(self::M_NON_PUBLIC_CONSTRUCTOR, $className), - self::E_NON_PUBLIC_CONSTRUCTOR - ); - } elseif ($ctorParams = $this->reflector->getCtorParams($className)) { - $reflClass = $this->reflector->getClass($className); - $definition = isset($this->classDefinitions[$normalizedClass]) - ? array_replace($this->classDefinitions[$normalizedClass], $definition) - : $definition; - $args = $this->provisionFuncArgs($ctor, $definition, $ctorParams, $className); - $obj = $reflClass->newInstanceArgs($args); - } else { - $obj = $this->instantiateWithoutCtorParams($className); - } - - return $obj; - } catch (\ReflectionException $e) { - throw new InjectionException( - $this->inProgressMakes, - sprintf(self::M_MAKE_FAILURE, $className, $e->getMessage()), - self::E_MAKE_FAILURE, - $e - ); - } - } - - private function instantiateWithoutCtorParams($className) - { - $reflClass = $this->reflector->getClass($className); - - if (!$reflClass->isInstantiable()) { - $type = $reflClass->isInterface() ? 'interface' : 'abstract class'; - throw new InjectionException( - $this->inProgressMakes, - sprintf(self::M_NEEDS_DEFINITION, $type, $className), - self::E_NEEDS_DEFINITION - ); - } - - return new $className(); - } - - private function provisionFuncArgs(\ReflectionFunctionAbstract $reflFunc, array $definition, array $reflParams = null, $className = null) - { - $args = array(); - - // @TODO store this in ReflectionStorage - if (!isset($reflParams)) { - $reflParams = $reflFunc->getParameters(); - } - - foreach ($reflParams as $i => $reflParam) { - $name = $reflParam->name; - - if (isset($definition[$i]) || array_key_exists($i, $definition)) { - // indexed arguments take precedence over named parameters - $arg = $definition[$i]; - } elseif (isset($definition[$name]) || array_key_exists($name, $definition)) { - // interpret the param as a class name to be instantiated - $arg = $this->make($definition[$name]); - } elseif (($prefix = self::A_RAW . $name) && (isset($definition[$prefix]) || array_key_exists($prefix, $definition))) { - // interpret the param as a raw value to be injected - $arg = $definition[$prefix]; - } elseif (($prefix = self::A_DELEGATE . $name) && isset($definition[$prefix])) { - // interpret the param as an invokable delegate - $arg = $this->buildArgFromDelegate($name, $definition[$prefix]); - } elseif (($prefix = self::A_DEFINE . $name) && isset($definition[$prefix])) { - // interpret the param as a class definition - $arg = $this->buildArgFromParamDefineArr($definition[$prefix]); - } elseif (!$arg = $this->buildArgFromTypeHint($reflFunc, $reflParam)) { - $arg = $this->buildArgFromReflParam($reflParam, $className); - - if ($arg === null && PHP_VERSION_ID >= 50600 && $reflParam->isVariadic()) { - // buildArgFromReflParam might return null in case the parameter is optional - // in case of variadics, the parameter is optional, but null might not be allowed - continue; - } - } - - $args[] = $arg; - } - - return $args; - } - - private function buildArgFromParamDefineArr($definition) - { - if (!is_array($definition)) { - throw new InjectionException( - $this->inProgressMakes - // @TODO Add message - ); - } - - if (!isset($definition[0], $definition[1])) { - throw new InjectionException( - $this->inProgressMakes - // @TODO Add message - ); - } - - list($class, $definition) = $definition; - - return $this->make($class, $definition); - } - - private function buildArgFromDelegate($paramName, $callableOrMethodStr) - { - if ($this->isExecutable($callableOrMethodStr) === false) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr - ); - } - - $executable = $this->buildExecutable($callableOrMethodStr); - - return $executable($paramName, $this); - } - - private function buildArgFromTypeHint(\ReflectionFunctionAbstract $reflFunc, \ReflectionParameter $reflParam) - { - $typeHint = $this->reflector->getParamTypeHint($reflFunc, $reflParam); - - if (!$typeHint) { - $obj = null; - } elseif ($reflParam->isDefaultValueAvailable()) { - $normalizedName = $this->normalizeName($typeHint); - // Injector has been told explicitly how to make this type - if (isset($this->aliases[$normalizedName]) || - isset($this->delegates[$normalizedName]) || - isset($this->shares[$normalizedName])) { - $obj = $this->make($typeHint); - } else { - $obj = $reflParam->getDefaultValue(); - } - } else { - $obj = $this->make($typeHint); - } - - return $obj; - } - - private function buildArgFromReflParam(\ReflectionParameter $reflParam, $className = null) - { - if (array_key_exists($reflParam->name, $this->paramDefinitions)) { - $arg = $this->paramDefinitions[$reflParam->name]; - } elseif ($reflParam->isDefaultValueAvailable()) { - $arg = $reflParam->getDefaultValue(); - } elseif ($reflParam->isOptional()) { - // This branch is required to work around PHP bugs where a parameter is optional - // but has no default value available through reflection. Specifically, PDO exhibits - // this behavior. - $arg = null; - } else { - $reflFunc = $reflParam->getDeclaringFunction(); - $classDeclare = ($reflFunc instanceof \ReflectionMethod) - ? " declared in " . $reflFunc->getDeclaringClass()->name . "::" - : ""; - $classWord = ($reflFunc instanceof \ReflectionMethod) - ? $className . '::' - : ''; - $funcWord = $classWord . $reflFunc->name; - - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_UNDEFINED_PARAM, - $reflParam->name, - $reflParam->getPosition(), - $funcWord, - $classDeclare - ), - self::E_UNDEFINED_PARAM - ); - } - - return $arg; - } - - private function prepareInstance($obj, $normalizedClass) - { - if (isset($this->prepares[$normalizedClass])) { - $prepare = $this->prepares[$normalizedClass]; - $executable = $this->buildExecutable($prepare); - $result = $executable($obj, $this); - if ($result instanceof $normalizedClass) { - $obj = $result; - } - } - - $interfaces = @class_implements($obj); - - if ($interfaces === false) { - throw new InjectionException( - $this->inProgressMakes, - sprintf( - self::M_MAKING_FAILED, - $normalizedClass, - gettype($obj) - ), - self::E_MAKING_FAILED - ); - } - - if (empty($interfaces)) { - return $obj; - } - - $interfaces = array_flip(array_map(array($this, 'normalizeName'), $interfaces)); - $prepares = array_intersect_key($this->prepares, $interfaces); - foreach ($prepares as $interfaceName => $prepare) { - $executable = $this->buildExecutable($prepare); - $result = $executable($obj, $this); - if ($result instanceof $normalizedClass) { - $obj = $result; - } - } - - return $obj; - } - - /** - * Invoke the specified callable or class::method string, provisioning dependencies along the way - * - * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string - * @param array $args Optional array specifying params with which to invoke the provisioned callable - * @throws \Auryn\InjectionException - * @return mixed Returns the invocation result returned from calling the generated executable - */ - public function execute($callableOrMethodStr, array $args = array()) - { - list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr); - $executable = new Executable($reflFunc, $invocationObj); - $args = $this->provisionFuncArgs($reflFunc, $args, null, $invocationObj === null ? null : get_class($invocationObj)); - - return call_user_func_array(array($executable, '__invoke'), $args); - } - - /** - * Provision an Executable instance from any valid callable or class::method string - * - * @param mixed $callableOrMethodStr A valid PHP callable or a provisionable ClassName::methodName string - * @return \Auryn\Executable - */ - public function buildExecutable($callableOrMethodStr) - { - try { - list($reflFunc, $invocationObj) = $this->buildExecutableStruct($callableOrMethodStr); - } catch (\ReflectionException $e) { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr, - $e - ); - } - - return new Executable($reflFunc, $invocationObj); - } - - private function buildExecutableStruct($callableOrMethodStr) - { - if (is_string($callableOrMethodStr)) { - $executableStruct = $this->buildExecutableStructFromString($callableOrMethodStr); - } elseif ($callableOrMethodStr instanceof \Closure) { - $callableRefl = new \ReflectionFunction($callableOrMethodStr); - $executableStruct = array($callableRefl, null); - } elseif (is_object($callableOrMethodStr) && is_callable($callableOrMethodStr)) { - $invocationObj = $callableOrMethodStr; - $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke'); - $executableStruct = array($callableRefl, $invocationObj); - } elseif (is_array($callableOrMethodStr) - && isset($callableOrMethodStr[0], $callableOrMethodStr[1]) - && count($callableOrMethodStr) === 2 - ) { - $executableStruct = $this->buildExecutableStructFromArray($callableOrMethodStr); - } else { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $callableOrMethodStr - ); - } - - return $executableStruct; - } - - private function buildExecutableStructFromString($stringExecutable) - { - if (function_exists($stringExecutable)) { - $callableRefl = $this->reflector->getFunction($stringExecutable); - $executableStruct = array($callableRefl, null); - } elseif (method_exists($stringExecutable, '__invoke')) { - $invocationObj = $this->make($stringExecutable); - $callableRefl = $this->reflector->getMethod($invocationObj, '__invoke'); - $executableStruct = array($callableRefl, $invocationObj); - } elseif (strpos($stringExecutable, '::') !== false) { - list($class, $method) = explode('::', $stringExecutable, 2); - $executableStruct = $this->buildStringClassMethodCallable($class, $method); - } else { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $stringExecutable - ); - } - - return $executableStruct; - } - - private function buildStringClassMethodCallable($class, $method) - { - $relativeStaticMethodStartPos = strpos($method, 'parent::'); - - if ($relativeStaticMethodStartPos === 0) { - $childReflection = $this->reflector->getClass($class); - $class = $childReflection->getParentClass()->name; - $method = substr($method, $relativeStaticMethodStartPos + 8); - } - - list($className, $normalizedClass) = $this->resolveAlias($class); - $reflectionMethod = $this->reflector->getMethod($className, $method); - - if ($reflectionMethod->isStatic()) { - return array($reflectionMethod, null); - } - - $instance = $this->make($className); - // If the class was delegated, the instance may not be of the type - // $class but some other type. We need to get the reflection on the - // actual class to be able to call the method correctly. - $reflectionMethod = $this->reflector->getMethod($instance, $method); - - return array($reflectionMethod, $instance); - } - - private function buildExecutableStructFromArray($arrayExecutable) - { - list($classOrObj, $method) = $arrayExecutable; - - if (is_object($classOrObj) && method_exists($classOrObj, $method)) { - $callableRefl = $this->reflector->getMethod($classOrObj, $method); - $executableStruct = array($callableRefl, $classOrObj); - } elseif (is_string($classOrObj)) { - $executableStruct = $this->buildStringClassMethodCallable($classOrObj, $method); - } else { - throw InjectionException::fromInvalidCallable( - $this->inProgressMakes, - $arrayExecutable - ); - } - - return $executableStruct; - } - - /** - * @param $callableOrMethodStr - * - * @throws ConfigException - */ - private function generateInvalidCallableError($callableOrMethodStr) - { - $errorDetail = ''; - if (is_string($callableOrMethodStr)) { - $errorDetail = " but received '$callableOrMethodStr'"; - } elseif (is_array($callableOrMethodStr) && - count($callableOrMethodStr) === 2 && - array_key_exists(0, $callableOrMethodStr) && - array_key_exists(1, $callableOrMethodStr) - ) { - if (is_string($callableOrMethodStr[0]) && is_string($callableOrMethodStr[1])) { - $errorDetail = " but received ['" . $callableOrMethodStr[0] . "', '" . $callableOrMethodStr[1] . "']"; - } - } - throw new ConfigException( - sprintf(self::M_DELEGATE_ARGUMENT, __CLASS__, $errorDetail), - self::E_DELEGATE_ARGUMENT - ); - } -} diff --git a/codeception.dist.yml b/codeception.dist.yml index af68839..da6a54a 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -1,3 +1,5 @@ +namespace: ItalyStrap\Empress\Tests +support: Support paths: tests: tests output: tests/_output @@ -8,14 +10,6 @@ actor_suffix: Tester extensions: enabled: - Codeception\Extension\RunFailed - commands: - - Codeception\Command\GenerateWPUnit - - Codeception\Command\GenerateWPRestApi - - Codeception\Command\GenerateWPRestController - - Codeception\Command\GenerateWPRestPostTypeController - - Codeception\Command\GenerateWPAjax - - Codeception\Command\GenerateWPCanonical - - Codeception\Command\GenerateWPXMLRPC params: - .env coverage: diff --git a/composer.json b/composer.json index 09f2684..6b20ff2 100644 --- a/composer.json +++ b/composer.json @@ -11,16 +11,20 @@ "role": "Developer" } ], - "minimum-stability": "stable", + "minimum-stability": "dev", + "prefer-stable": true, "require": { "php" : ">=7.4", - "rdlowrey/auryn": "^1.4", - "italystrap/config": "^2.2", - "ocramius/proxy-manager": "~2.11.0" + "brick/varexporter": "^0.3.8", + "italystrap/config": "^2.11", + "overclokk/auryn": "dev-master", + "webimpress/safe-writer": "^2.2", + "psr/container": "^1.1 || ^2.0" }, "require-dev": { - "lucatume/wp-browser": "^3.0", + "lucatume/wp-browser": ">=3.2.3 <3.5", "lucatume/function-mocker-le": "^1.0.1", + "behat/gherkin": ">=4.4 <4.11", "codeception/module-asserts": "^1.0", "phpspec/prophecy-phpunit": "^2.0", @@ -28,7 +32,7 @@ "phpcompatibility/php-compatibility": "^9.3", "dealerdirect/phpcodesniffer-composer-installer": "^1.0", - "vimeo/psalm": "^5.6", + "phpstan/phpstan": "^1.12", "phpbench/phpbench": "^1.2", "phpmetrics/phpmetrics": "^2.8", @@ -36,49 +40,58 @@ "infection/infection": "^0.26.6", "infection/codeception-adapter": "^0.4.1", - "rector/rector": "^0.15.17", - "italystrap/debug": "^2.1" + "rector/rector": "^1.2", + "italystrap/debug": "dev-master", + "italystrap/finder": "dev-master", + "laminas/laminas-config-aggregator": "^1.9", + "crellbar/prophecy-extensions": "^1.1", + "rector/swiss-knife": "^2.3", + + "friendsofphp/proxy-manager-lts": "^1.0" }, "autoload": { "psr-4": { - "ItalyStrap\\Empress\\": "src/" + "ItalyStrap\\Empress\\": [ + "src/" + ] }, "files": [ - "namespace-bc-aliases.php", - "bridge/Injector.php" + "namespace-bc-aliases.php" ] }, "autoload-dev": { "psr-4": { - "ItalyStrap\\Tests\\": "tests/src/", - "ItalyStrap\\Tests\\Unit\\": "tests/unit/" - }, - "files": [ - "tests/_data/fixtures/fixtures.php", - "tests/_data/fixtures/fixtures_5_6.php" - ] + "ItalyStrap\\Empress\\Tests\\": [ + "tests/src/", + "tests/_data/fixtures/src/" + ], + "ItalyStrap\\Empress\\Tests\\Modules\\": "tests/_data/fixtures/modules/", + "ItalyStrap\\Empress\\Tests\\Unit\\": "tests/unit/" + } }, "suggest": { "elazar/auryn-container-interop": "Only if you want to add a psr/container adapter, not required for this package", - "northwoods/container": "Only if you want to add a psr/container adapter, not required for this package" + "northwoods/container": "Only if you want to add a psr/container adapter, not required for this package", + "friendsofphp/proxy-manager-lts": "Only if you want to add support for lazy loading classes" }, "scripts": { "cs": [ "@php vendor/bin/phpcs -p" ], "cs:fix": [ - "@php vendor/bin/phpcbf -p" + "echo Fixing the package", + "@php vendor/bin/phpcbf -p", + "echo Fixing example.php", + "@php vendor/bin/phpcbf example.php" ], - "psalm": [ - "@php ./vendor/bin/psalm --no-cache" + "stan": [ + "@php ./vendor/bin/phpstan analyse --no-progress" ], "unit": [ - "@php vendor/bin/codecept run unit", - "@clean" + "@php vendor/bin/codecept run unit" ], "unit:debug": [ - "@php vendor/bin/codecept run unit --debug", - "@clean" + "@php vendor/bin/codecept run unit --debug" ], "infection": [ "@php vendor/bin/infection --threads=4" @@ -97,6 +110,11 @@ ], "clean": [ "@php vendor/bin/codecept clean" + ], + "qa": [ + "@cs", + "@stan", + "@unit" ] }, "support" : { diff --git a/example.php b/example.php index 1ce893d..c50262b 100644 --- a/example.php +++ b/example.php @@ -1,193 +1,185 @@ class = $class; - $this->config = $config; - $this->param = $param; - } - - /** - * @return ConfigInterface - */ - public function getConfig(): ConfigInterface { - return $this->config; - } - - /** - * @return stdClass - */ - public function getClass(): stdClass { - return $this->class; - } - - public function execute( string $text ) { - return $text; - } +require_once __DIR__ . '/vendor/autoload.php'; // phpcs:ignore PSR1.Files.SideEffects + +class Example +{ + private stdClass $class; + private ConfigInterface $config; + private string $param; + + public function __construct(stdClass $class, ConfigInterface $config, string $param) + { + $this->class = $class; + $this->config = $config; + $this->param = $param; + } + + public function getConfig(): ConfigInterface + { + return $this->config; + } + + public function getClass(): stdClass + { + return $this->class; + } + + public function getParam(): string + { + return $this->param; + } + + public function execute(string $text): string + { + return $text; + } } /** * The better way to add keys to the array configuration is to use the - * AurynResolver:: + * AurynConfig:: * * Keys available are: * - * AurynResolver::PROXY = 'proxies'; - * AurynResolver::SHARING = 'sharing'; - * AurynResolver::ALIASES = 'aliases'; - * AurynResolver::DEFINITIONS = 'definitions'; - * AurynResolver::DEFINE_PARAM = 'define_param'; - * AurynResolver::DELEGATIONS = 'delegations'; - * AurynResolver::PREPARATIONS = 'preparations'; + * AurynConfig::PROXY = 'proxies'; + * AurynConfig::SHARING = 'sharing'; + * AurynConfig::ALIASES = 'aliases'; + * AurynConfig::DEFINITIONS = 'definitions'; + * AurynConfig::DEFINE_PARAM = 'define_param'; + * AurynConfig::DELEGATIONS = 'delegations'; + * AurynConfig::PREPARATIONS = 'preparations'; */ /** - * First off all we need a configuration + * First of all we need a configuration */ $config = [ - /** - * Example: - * class MyCLass( ConfigInterface $config ) {} - * You alias a `ConfigInterface::class` to `Config::class` - * $injector->make(MyCLass::class); will be injected with a Config object - * @see [Type-Hint Aliasing](https://github.com/rdlowrey/auryn#type-hint-aliasing) - */ - AurynConfig::ALIASES => [ - ConfigInterface::class => Config::class, - ], - - /** - * Example: - * class MyCLass( ConfigInterface $global_config, \stdClass $class ) {} - * class MyOtherCLass( ConfigInterface $global_config, \stdClass $class ) {} - * A new Config instance will be shared, think of it like a singleton but more better and OOP oriented (You can mock it ;-)) - * The same instance of Config will be injected to MyCLass and MyOtherCLass - * $injector->make(MyCLass::class); // Will have $global_config - * $injector->make(MyOtherCLass::class); // Will have $global_config - * @see [Instance Sharing](https://github.com/rdlowrey/auryn#instance-sharing) - */ - AurynConfig::SHARING => [ - stdClass::class, - ConfigInterface::class, - ], - - /** - * This is the new feature for the Auryn\Injector implemented in the bridge adapter - * You usually need a lazy value holder in cases where the following applies: - * * your object takes a lot of time and memory to be initialized (with all dependencies) - * * your object is not always used, and the instantiation overhead is avoidable - * - * Example: - * class HeavyComplexObject( ...HeavyDependency ){}; // Declared somewhere - * $object = $injector->make(HeavyComplexObject::class); - * add_{filter|action}( 'event_name', [ $object, 'doSomeStuff' ] ); - * - * You can proxies the `HeavyComplexObject::class` dependency - * $injector->proxy(HeavyDependency::class); - * Or the `HeavyComplexObject::class` - * $injector->proxy(HeavyComplexObject::class); - * - * It depends on your business logic. - * - * Let see for example if you have proxies the HeavyComplexObject::class - * - * $proxy = $injector->make(HeavyComplexObject::class); - * - * Now $proxy will be the lazy version of the object (as a proxy) and when the event call it - * add_{filter|action}( 'event_name', [ $proxy, 'doSomeStuff' ] ); - * ::doSomeStuff() will just work as before. - * - * @see [Lazy Loading Value Holder Proxy](https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md) - */ - AurynConfig::PROXY => [ - Config::class, - ], - - /** - * Define global parameter - * class SomeCLass(string $text) {} - * class SomeOtherCLass(string $text) {} - * $injector->make(SomeCLass::class); - * $injector->make(SomeOtherCLass::class); - * Now the `$text` param will be decorated with 'Some Text' - * @see [Global Parameter Definitions](https://github.com/rdlowrey/auryn#global-parameter-definitions) - */ - AurynConfig::DEFINE_PARAM => [ - 'text' => 'Some Text' - ], - - /** - * Definition for class specific - * Example: - * class Example(int $param) {} - * Now the `$param` will be decorated with 42 - * class OtherExample(int $param) {} - * This will not be decorated with 42 because you have defined only the Example::class parameter - * @see [Injection Definitions](https://github.com/rdlowrey/auryn#injection-definitions) - */ - AurynConfig::DEFINITIONS => [ - Example::class => [ - ':param' => 42, - ] - ], - - /** - * As soon as the instance is created you can prepare some action before use the new created instance - * This is the same as: - * $class = new \stdClass; - * $class->param = 42; - * echo $class->param; - * @see [Prepares and Setter Injection](https://github.com/rdlowrey/auryn#prepares-and-setter-injection) - */ - AurynConfig::PREPARATIONS => [ - stdClass::class => function ( stdClass $class, Injector $injector ) { - $class->param = 42; - }, - ], - - /** - * You can delegate the instantiation of an object to a some kind of callable factory - * This will be always used to get the instance of a class. - * @see [Instantiation Delegates](https://github.com/rdlowrey/auryn#instantiation-delegates) - */ - AurynConfig::DELEGATIONS => [ - ConfigInterface::class => [ ConfigFactory::class, 'make'] - ], + /** + * Example: + * class MyCLass(ConfigInterface $config) {} + * You alias a `ConfigInterface::class` to `Config::class` + * $injector->make(MyCLass::class); will be injected with a Config object + * @see [Type-Hint Aliasing](https://github.com/rdlowrey/auryn#type-hint-aliasing) + */ + AurynConfig::ALIASES => [ + ConfigInterface::class => Config::class, + ], + + /** + * Example: + * class MyCLass( ConfigInterface $global_config, \stdClass $class ) {} + * class MyOtherCLass( ConfigInterface $global_config, \stdClass $class ) {} + * A new Config instance will be shared, think of it like a singleton + * but better and OOP oriented (You can mock it ;-)) + * The same instance of Config will be injected to MyCLass and MyOtherCLass + * $injector->make(MyCLass::class); // Will have $global_config + * $injector->make(MyOtherCLass::class); // Will have $global_config + * @see [Instance Sharing](https://github.com/rdlowrey/auryn#instance-sharing) + */ + AurynConfig::SHARING => [ + stdClass::class, + ConfigInterface::class, + ], + + /** + * This is the new feature for the Auryn\Injector implemented in the bridge adapter + * You usually need a lazy value holder in cases where the following applies: + * * your object takes a lot of time and memory to be initialized (with all dependencies) + * * your object is not always used, and the instantiation overhead is avoidable + * + * Example: + * class HeavyComplexObject( ...HeavyDependency ){}; // Declared somewhere + * $object = $injector->make(HeavyComplexObject::class); + * add_{filter|action}( 'event_name', [ $object, 'doSomeStuff' ] ); + * + * You can proxies the `HeavyComplexObject::class` dependency + * $injector->proxy(HeavyDependency::class); + * Or the `HeavyComplexObject::class` + * $injector->proxy(HeavyComplexObject::class); + * + * It depends on your business logic. + * + * Let see for example if you have proxies the HeavyComplexObject::class + * + * $proxy = $injector->make(HeavyComplexObject::class); + * + * Now $proxy will be the lazy version of the object (as a proxy) and when the event call it + * add_{filter|action}( 'event_name', [ $proxy, 'doSomeStuff' ] ); + * ::doSomeStuff() will just work as before. + * + * @see https://github.com/Ocramius/ProxyManager/blob/master/docs/lazy-loading-value-holder.md + */ + AurynConfig::PROXY => [ + Config::class, + ], + + /** + * Define global parameter + * class SomeCLass(string $text) {} + * class SomeOtherCLass(string $text) {} + * $injector->make(SomeCLass::class); + * $injector->make(SomeOtherCLass::class); + * Now the `$text` param will be decorated with 'Some Text' + * @see [Global Parameter Definitions](https://github.com/rdlowrey/auryn#global-parameter-definitions) + */ + AurynConfig::DEFINE_PARAM => [ + 'text' => 'Some Text' + ], + + /** + * Definition for class specific + * Example: + * class Example(int $param) {} + * Now the `$param` will be decorated with 42 + * class OtherExample(int $param) {} + * This will not be decorated with 42 because you have defined only the Example::class parameter + * @see [Injection Definitions](https://github.com/rdlowrey/auryn#injection-definitions) + */ + AurynConfig::DEFINITIONS => [ + Example::class => [ + ':param' => 42, + ] + ], + + /** + * As soon as the instance is created you can prepare some action before use the new created instance + * This is the same as: + * $class = new \stdClass; + * $class->param = 42; + * echo $class->param; + * @see [Prepares and Setter Injection](https://github.com/rdlowrey/auryn#prepares-and-setter-injection) + */ + AurynConfig::PREPARATIONS => [ + stdClass::class => function (stdClass $class, Injector $injector) { + $class->param = 42; + }, + ], + + /** + * You can delegate the instantiation of an object to a some kind of callable factory + * This will always be used to get the instance of a class. + * @see [Instantiation Delegates](https://github.com/rdlowrey/auryn#instantiation-delegates) + */ + AurynConfig::DELEGATIONS => [ + ConfigInterface::class => [ ConfigFactory::class, 'make'] + ], ]; /** @@ -196,92 +188,143 @@ public function execute( string $text ) { $injector = new Injector(); /** - * Pass the $injector instance to the AurynResolver::class as first parameter and a + * Pass the $injector instance to the AurynConfig::class as first parameter and a * Config::class instance at the second parameters with the configuration array. */ -$app = new AurynConfig( $injector, ConfigFactory::make( $config ) ); +$app = new AurynConfig($injector, (new ConfigFactory())->make($config)); /** - * Call the AurynResolver::resolve() method to do the autowiring of the application + * Call the AurynConfig::$this->apply() method to do the autowiring of the application */ -$app->resolve(); +$app->apply(); /** - * Now that you have autoload your application dependency you can call $injector for instantiating objects + * Now that you have autoloaded your application dependency you can call $injector for instantiating objects * when you need them */ -$example = $injector->make( Example::class ); +$example = $injector->make(Example::class); // $example instanceof Example::class +\var_dump( + $example instanceof Example + ? 'Yes, $example is an instance of Example::class' + : 'No, $example is NOT an instance of Example::class' +); +echo $example->execute('Hello World!'); +echo PHP_EOL; + +/** + * ContainerBuilder usage + * + * This is the quickest way to aggregate providers, configure Auryn, + * and receive a PSR-11 container. + */ +$containerBuilder = new ContainerBuilder(); + +$containerBuilder + ->addProvider(static fn(): array => [ + AurynConfig::ALIASES => [ + ConfigInterface::class => Config::class, + ], + AurynConfig::SHARING => [ + stdClass::class, + ConfigInterface::class, + ], + AurynConfig::DEFINE_PARAM => [ + 'param' => 'Builder Text', + ], + AurynConfig::DELEGATIONS => [ + Example::class => static function (ContainerInterface $container): Example { + return new Example( + $container->get(stdClass::class), + $container->get(ConfigInterface::class), + 'Builder Text' + ); + }, + ], + ]) + ->extend(new class implements Extension { + public function name(): string + { + return 'container-builder-extension'; + } + + public function execute(AurynConfigInterface $application): void + { + // Add custom Auryn configuration logic here. + } + }); + +$container = $containerBuilder->build(); +$builderExample = $container->get(Example::class); + +\var_dump( + $builderExample instanceof Example + ? 'Yes, $builderExample is an instance of Example::class' + : 'No, $builderExample is NOT an instance of Example::class' +); //$example2 = $injector->make( Example::class ); // //$result = $injector->execute( [ $example, 'execute' ] ); -//d_footer( -// $app, -// $example, -// $example2, -// $example !== $example2, -// $result, -// $example->getConfig() -//); - /** * Advanced usage */ /** - * If you need more power you can extend the AurynResolver::class BEFORE calling the AurynResolver::resolve() method + * If you need more power you can extend the AurynConfig::class BEFORE calling the AurynConfig::$this->apply() method * Create your custom configuration like the follow: * $config = [ - * 'your-key' => [ - * 'Key' => 'Value', + * 'your-key' => [ + * 'Key' => 'Value', * ], * ]; * - * Now extend the AurynResolver: + * Now extend the AurynConfig: */ $app->extend( - new class implements Extension { - - /** @var string */ - const YOUR_KEY = 'your-key'; - - public function name(): string { - return (string) self::YOUR_KEY; - } - - /** - * Called inside the AurynResolver instance - * @param AurynConfigInterface $application - */ - public function execute( AurynConfigInterface $application ) { - - /** - * ::walk() accept: - * self::YOUR_KEY will be a key to search against the config array - * [ $this, 'doSomeStuff' ] will be a valid callable to do the work you need. - */ - $application->walk( (string) self::YOUR_KEY, [$this, 'doSomeStuff'] ); - } - - /** - * @param string $array_value Array value from yous configuration - * @param int|string $array_key Array key from your configuration - * @param Injector $injector An instance of the Injector::class - */ - public function doSomeStuff( string $array_value, $array_key, Injector $injector ) { - // Do your logic here - } - } + new class implements Extension { + /** @var string */ + public const YOUR_KEY = 'your-key'; + + public function name(): string + { + return (string) self::YOUR_KEY; + } + + /** + * Called inside the AurynConfig instance + * @param AurynConfigInterface $application + */ + public function execute(AurynConfigInterface $application) + { + + /** + * ::walk() accept: + * self::YOUR_KEY will be a key to search against the config array + * [ $this, 'doSomeStuff' ] will be a valid callable to do the work you need. + */ + $application->walk((string) self::YOUR_KEY, [$this, 'doSomeStuff']); + } + + /** + * @param string $array_value Array value from yous configuration + * @param int|string $array_key Array key from your configuration + * @param Injector $injector An instance of the Injector::class + */ + public function doSomeStuff(string $array_value, $array_key, Injector $injector) + { + // Do your logic here + } + } ); /** * You can add as many extensions as you need - * Now you can call the ::resolve() method + * Now you can call the ::$this->apply() method */ -$app->resolve(); +$app->apply(); // Do the rest of your stuff diff --git a/index.php b/index.php deleted file mode 100644 index aefbe26..0000000 --- a/index.php +++ /dev/null @@ -1,43 +0,0 @@ -./src/ ./tests/ + namespace-bc-aliases.php */vendor/* */tests/_support/* + */tests/_output/* diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..13fd118 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +parameters: + level: 9 + phpVersion: 70400 + paths: + - src + tmpDir: tests/_output/phpstan + parallel: + maximumNumberOfProcesses: 1 diff --git a/psalm.xml b/psalm.xml deleted file mode 100644 index eef60d0..0000000 --- a/psalm.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - diff --git a/src/AurynConfig.php b/src/AurynConfig.php index 1def70c..68a1f12 100644 --- a/src/AurynConfig.php +++ b/src/AurynConfig.php @@ -4,15 +4,13 @@ namespace ItalyStrap\Empress; +use Auryn\Injector; use Auryn\ConfigException; use Auryn\InjectionException; use ItalyStrap\Config\ConfigInterface as Config; use function array_walk; -/** - * @psalm-api - */ class AurynConfig implements AurynConfigInterface { public const PROXY = 'proxies'; @@ -21,6 +19,7 @@ class AurynConfig implements AurynConfigInterface public const DEFINITIONS = 'definitions'; public const DEFINE_PARAM = 'define_param'; public const DELEGATIONS = 'delegations'; + public const FACTORIES = 'factories'; public const PREPARATIONS = 'preparations'; private const METHODS = [ @@ -30,35 +29,43 @@ class AurynConfig implements AurynConfigInterface self::DEFINITIONS => 'define', self::DEFINE_PARAM => 'defineParam', self::DELEGATIONS => 'delegate', + self::FACTORIES => 'delegate', self::PREPARATIONS => 'prepare', ]; private Injector $injector; + /** + * @var Config + */ private Config $dependencies; + private ?ProxyFactoryInterface $proxy_factory; + /** - * @var array + * @var array */ private array $extensions = []; - private ProxyFactoryInterface $proxy_factory; + /** + * @var array + */ + private array $extensionsClasses = []; /** - * @param Config $dependencies - * @param Injector $injector + * @param Config $dependencies */ public function __construct( Injector $injector, Config $dependencies, - ProxyFactoryInterface $proxyFactory = null + ?ProxyFactoryInterface $proxyFactory = null ) { $this->injector = $injector; $this->dependencies = $dependencies; - $this->proxy_factory = $proxyFactory ?? new ProxyFactory(); + $this->proxy_factory = $proxyFactory; } - public function resolve(): void + public function apply(): void { /** @@ -71,15 +78,46 @@ public function resolve(): void $this->walk($key, $callback); } + foreach ($this->extensionsClasses as $extensionClass) { + /** @var Extension $extension */ + $extension = $this->injector->share($extensionClass)->make($extensionClass); + $this->extensions[$extension->name()] = $extension; + } + foreach ($this->extensions as $extension) { $extension->execute($this); } } - public function extend(Extension ...$extensions): void + /** + * @deprecated Use apply() instead. + */ + public function resolve(): void + { + $this->apply(); + } + + public function extend(...$extensions): void { foreach ($extensions as $extension) { - $this->extensions[$extension->name()] = $extension; + if ( + \is_string($extension) + && \class_exists($extension) + && \is_subclass_of($extension, Extension::class) + ) { + $this->extensionsClasses[] = $extension; + continue; + } + + if ($extension instanceof Extension) { + $this->extensions[$extension->name()] = $extension; + continue; + } + + throw new \InvalidArgumentException(\sprintf( + 'Invalid extension type, given: %s', + \gettype($extension) + )); } } @@ -94,9 +132,7 @@ public function walk(string $key, callable $callback): void /** * @param mixed $nameOrInstance - * @param int $index * @throws ConfigException - * @psalm-suppress PossiblyUnusedParam */ protected function share($nameOrInstance, int $index): void { @@ -104,29 +140,34 @@ protected function share($nameOrInstance, int $index): void } /** - * @param string $name - * @param int $index * @throws ConfigException - * @psalm-suppress PossiblyUnusedParam */ protected function proxy(string $name, int $index): void { + if ($name !== '' && \class_exists($name) && $this->proxy_factory === null) { + throw new ConfigException(\sprintf( + 'Proxy factory is required for proxying %s', + $name + )); + } + + if ($this->proxy_factory === null) { + return; + } + $this->injector->proxy($name, $this->proxy_factory); } /** - * @param string $implementation - * @param string $interface * @throws ConfigException */ - protected function alias(string $implementation, string $interface): void + protected function alias(string $alias, string $typeHint): void { - $this->injector->alias($interface, $implementation); + $this->injector->alias($typeHint, $alias); } /** - * @param array $class_args - * @param string $class_name + * @param array $class_args */ protected function define(array $class_args, string $class_name): void { @@ -135,7 +176,6 @@ protected function define(array $class_args, string $class_name): void /** * @param mixed $param_args - * @param string $param_name */ protected function defineParam($param_args, string $param_name): void { @@ -144,7 +184,6 @@ protected function defineParam($param_args, string $param_name): void /** * @param string $callableOrMethodStr - * @param string $name * @throws ConfigException */ protected function delegate($callableOrMethodStr, string $name): void @@ -154,7 +193,6 @@ protected function delegate($callableOrMethodStr, string $name): void /** * @param mixed $callableOrMethodStr - * @param string $name * @throws InjectionException */ protected function prepare($callableOrMethodStr, string $name): void diff --git a/src/AurynConfigInterface.php b/src/AurynConfigInterface.php index a027536..16a4e58 100644 --- a/src/AurynConfigInterface.php +++ b/src/AurynConfigInterface.php @@ -4,25 +4,20 @@ namespace ItalyStrap\Empress; -/** - * @psalm-api - */ interface AurynConfigInterface { /** * @return void */ - public function resolve(); + public function apply(); /** - * @param Extension ...$extensions + * @param class-string|Extension ...$extensions * @return void */ - public function extend(Extension ...$extensions); + public function extend(...$extensions); /** - * @param string $key - * @param callable $callback * @return void */ public function walk(string $key, callable $callback); diff --git a/src/ConfigReplacementTrait.php b/src/ConfigReplacementTrait.php new file mode 100644 index 0000000..76d07f6 --- /dev/null +++ b/src/ConfigReplacementTrait.php @@ -0,0 +1,22 @@ + $config + * @param array $values + */ + private function replaceConfig(ConfigInterface $config, array $values): void + { + // @phpstan-ignore-next-line Config supports exchangeArray(), but ConfigInterface does not expose it yet. + $config->exchangeArray($values); + } +} diff --git a/src/Container.php b/src/Container.php new file mode 100644 index 0000000..bffc6c8 --- /dev/null +++ b/src/Container.php @@ -0,0 +1,51 @@ +injector = $injector; + } + + public function get(string $id) + { + if (!$this->has($id)) { + throw new NotFoundException(\sprintf("Service '%s' not found", $id)); + } + + try { + return $this->injector->make($id); + } catch (\Throwable $throwable) { + throw new ContainerException( + \sprintf("Error while retrieving service '%s'", $id), + 0, + $throwable + ); + } + } + + public function has(string $id): bool + { + if (\class_exists($id)) { + return true; + } + + return $this->injectorHas($id); + } + + private function injectorHas(string $id): bool + { + $details = $this->injector->inspect($id, 31); + return (bool) \array_filter($details); + } +} diff --git a/src/ContainerBuilder.php b/src/ContainerBuilder.php new file mode 100644 index 0000000..9f11360 --- /dev/null +++ b/src/ContainerBuilder.php @@ -0,0 +1,105 @@ +&NodeManipulationInterface + */ + private ConfigInterface $config; + + private ?ProvidersCacheInterface $cache; + + private ?ProxyFactoryInterface $proxyFactory; + + /** + * @var array + */ + private array $providers = []; + + /** + * @var array + */ + private array $extensions = []; + + /** + * @param ConfigInterface|null $config + */ + public function __construct( + ?Injector $injector = null, + ?ConfigInterface $config = null, + ?ProvidersCacheInterface $cache = null, + ?ProxyFactoryInterface $proxyFactory = null + ) { + if ($config instanceof ConfigInterface && !$config instanceof NodeManipulationInterface) { + throw new \InvalidArgumentException(\sprintf( + '$config must implement %s', + NodeManipulationInterface::class + )); + } + + $this->injector = $injector ?: new Injector(); + /** @var ConfigInterface&NodeManipulationInterface $configInstance */ + $configInstance = $config ?: new Config(); + $this->config = $configInstance; + $this->cache = $cache; + $this->proxyFactory = $proxyFactory; + } + + /** + * @param callable|class-string|array{0: class-string|object, 1: non-empty-string}|object $provider + */ + public function addProvider($provider): self + { + $this->providers[] = $provider; + + return $this; + } + + /** + * @param callable|class-string|array{0: class-string|object, 1: non-empty-string}|object $module + */ + public function addModule($module): self + { + return $this->addProvider($module); + } + + /** + * @param class-string|Extension ...$extensions + */ + public function extend(...$extensions): self + { + $this->extensions = $extensions; + return $this; + } + + public function build(): ContainerInterface + { + $injector = $this->injector; + $injector->share($injector); + + $container = new Container($injector); + $injector->alias(ContainerInterface::class, \get_class($container)); + $injector->share($container); + + $providersCollection = new ProvidersCollection($injector, $this->config, $this->cache, $this->providers); + $providersCollection->aggregate(); + + $injectorConfig = new AurynConfig($injector, $this->config, $this->proxyFactory); + $injectorConfig->extend(...$this->extensions); + $injectorConfig->apply(); + + return $container; + } +} diff --git a/src/ContainerException.php b/src/ContainerException.php new file mode 100644 index 0000000..2c81822 --- /dev/null +++ b/src/ContainerException.php @@ -0,0 +1,11 @@ + + */ + public function __invoke(): iterable; +} diff --git a/src/NotFoundException.php b/src/NotFoundException.php new file mode 100644 index 0000000..2b23782 --- /dev/null +++ b/src/NotFoundException.php @@ -0,0 +1,11 @@ +pattern = $pattern; + $this->finder = $finder; + } + + public function __invoke(): \Generator + { + $this->finder->names([$this->pattern]); + /** + * @var \SplFileInfo $file + */ + foreach ($this->finder as $file) { + yield include $file; + } + } +} diff --git a/src/ProvidersCache.php b/src/ProvidersCache.php new file mode 100644 index 0000000..22e5629 --- /dev/null +++ b/src/ProvidersCache.php @@ -0,0 +1,115 @@ +file = $file; + $this->fileMode = $fileMode; + $this->enabled = $enabled; + } + + /** + * @param ConfigInterface $config + */ + public function read(ConfigInterface $config): bool + { + if (!$this->enabled) { + return false; + } + + if ( + $this->file === '' + || !file_exists($this->file) + || !is_file($this->file) + || !is_readable($this->file) + ) { + return false; + } + + $this->replaceConfig($config, $this->loadCacheFile($this->file)); + return true; + } + + /** + * @param ConfigInterface $config + */ + public function write(ConfigInterface $config): void + { + if (!$this->enabled || $this->file === '') { + return; + } + + try { + $contents = sprintf( + self::CACHE_TEMPLATE, + self::class, + // Write an alternative to date('c') + (new DateTimeImmutable('now'))->format('c'), + VarExporter::export( + $config->toArray(), + VarExporter::ADD_RETURN | VarExporter::CLOSURE_SNAPSHOT_USES + ) + ); + } catch (ExportException $e) { + throw new \ErrorException('Configuration cannot be cached', 0, 1, __FILE__, __LINE__, $e); + } + + $this->writeCache($this->file, $contents, $this->fileMode); + } + + private function writeCache(string $cachedConfigFile, string $contents, int $mode): void + { + try { + FileWriter::writeFile($cachedConfigFile, $contents, $mode); + } catch (FileWriterException $e) { + throw new \ErrorException('Configuration cache cannot be written', 0, 1, __FILE__, __LINE__, $e); + } + } + + /** + * @return array + */ + private function loadCacheFile(string $cachedConfigFile): array + { + try { + $config = require $cachedConfigFile; + } catch (\Throwable $e) { + throw new \ErrorException('Configuration cache cannot be read', 0, 1, __FILE__, __LINE__, $e); + } + + if (!\is_array($config)) { + throw new \ErrorException('Configuration cache must return an array'); + } + + return $config; + } +} diff --git a/src/ProvidersCacheInterface.php b/src/ProvidersCacheInterface.php new file mode 100644 index 0000000..cef3ad0 --- /dev/null +++ b/src/ProvidersCacheInterface.php @@ -0,0 +1,20 @@ + $config + */ + public function read(ConfigInterface $config): bool; + + /** + * @param ConfigInterface $config + */ + public function write(ConfigInterface $config): void; +} diff --git a/src/ProvidersCollection.php b/src/ProvidersCollection.php new file mode 100644 index 0000000..359c4f0 --- /dev/null +++ b/src/ProvidersCollection.php @@ -0,0 +1,213 @@ + + */ +final class ProvidersCollection +{ + use ConfigReplacementTrait; + + private Injector $injector; + /** + * @var ConfigInterface&NodeManipulationInterface + */ + private ConfigInterface $config; + private ProvidersCacheInterface $cache; + /** + * @var iterable + */ + private iterable $providers; + + /** + * @param ConfigInterface&NodeManipulationInterface $config + * @param iterable $providers + */ + public function __construct( + Injector $injector, + ConfigInterface $config, + ?ProvidersCacheInterface $cache = null, + iterable $providers = [] + ) { + if (!$config instanceof NodeManipulationInterface) { + throw new \InvalidArgumentException(\sprintf( + '$config must implement %s', + NodeManipulationInterface::class + )); + } + + $this->injector = $injector; + $this->config = $config; + $this->cache = $cache ?: new ProvidersCache(); + $this->providers = $providers; + } + + public function aggregate(): void + { + if ($this->cache->read($this->config)) { + return; + } + + $result = $this->config->toArray(); + foreach ($this->loadConfigurationsFromProviders() as $configuration) { + $this->mergeConfiguration($configuration, $result); + } + + $this->replaceConfig($this->config, $result); + + $this->cache->write($this->config); + } + + /** + * @param Configuration $configuration + * @param array $result + */ + private function mergeConfiguration( + array $configuration, + array &$result + ): void { + $result = $this->mergeValue($result, $configuration); + } + + /** + * @param mixed $current + * @param mixed $incoming + * @return mixed + */ + private function mergeValue($current, $incoming) + { + if (!\is_array($incoming)) { + return $incoming; + } + + $current = \is_array($current) ? $current : []; + + foreach ($incoming as $key => $value) { + if (!\is_int($key)) { + $current[$key] = $this->mergeValue($current[$key] ?? null, $value); + continue; + } + + if (!$this->hasListValue($current, $value)) { + $current[] = $value; + } + } + + return $current; + } + + /** + * @param array $values + * @param mixed $valueToFind + */ + private function hasListValue(array $values, $valueToFind): bool + { + foreach ($values as $key => $value) { + if (\is_int($key) && $value === $valueToFind) { + return true; + } + } + + return false; + } + + /** + * @return \Generator + */ + private function loadConfigurationsFromProviders(): \Generator + { + foreach ($this->providers as $provider) { + try { + $result = $this->injector->execute($provider); + } catch (\Throwable $e) { + throw new \ErrorException( + \sprintf( + 'An error occurred when executing %s: %s', + $this->providerName($provider), + $e->getMessage() + ), + 0, + 1, + __FILE__, + __LINE__, + $e + ); + } + + if (\is_array($result)) { + yield $result; + continue; + } + + if ($result instanceof \Traversable) { + yield from $this->validateIterableConfiguration($result, $provider); + continue; + } + + throw new \RuntimeException( + \sprintf( + 'The provider %s must return an array or iterable of arrays, %s given', + $this->providerName($provider), + \gettype($result) + ) + ); + } + } + + /** + * @param \Traversable $configuration + * @param mixed $provider + * @return \Generator + */ + private function validateIterableConfiguration(\Traversable $configuration, $provider): \Generator + { + foreach ($configuration as $item) { + if (!\is_array($item)) { + throw new \RuntimeException( + \sprintf( + 'The provider %s yielded %s, expected array', + $this->providerName($provider), + \gettype($item) + ) + ); + } + + yield $item; + } + } + + /** + * @param mixed $provider + */ + private function providerName($provider): string + { + if (\is_object($provider)) { + return \get_class($provider); + } + + if (\is_array($provider)) { + $target = $provider[0] ?? 'unknown'; + $method = $provider[1] ?? 'unknown'; + + if (\is_object($target)) { + $target = \get_class($target); + } + + return \sprintf('%s::%s', (string)$target, (string)$method); + } + + if (\is_string($provider)) { + return $provider; + } + + return \gettype($provider); + } +} diff --git a/src/ProxyFactory.php b/src/ProxyFactory.php index 49137c8..67bd3e4 100644 --- a/src/ProxyFactory.php +++ b/src/ProxyFactory.php @@ -8,24 +8,25 @@ use ProxyManager\Factory\LazyLoadingValueHolderFactory; use ProxyManager\Proxy\VirtualProxyInterface; +/** + * @infection-ignore-all + */ class ProxyFactory implements ProxyFactoryInterface { /** - * @psalm-suppress ArgumentTypeCoercion + * @param class-string $className */ public function __invoke(string $className, callable $callback): VirtualProxyInterface { - /** @psalm-suppress MixedArgumentTypeCoercion */ return (new LazyLoadingValueHolderFactory())->createProxy( $className, static function ( - ?object &$object, - ?object $proxy, - string $method, - array $parameters, - ?Closure &$initializer + ?object &$object = null, + ?object $proxy = null, + string $method = '', + array $parameters = [], + ?Closure &$initializer = null ) use ($callback): bool { - /** @psalm-suppress MixedAssignment */ $object = $callback(); $initializer = null; return true; diff --git a/src/ProxyFactoryInterface.php b/src/ProxyFactoryInterface.php index 34067f8..8f265ce 100644 --- a/src/ProxyFactoryInterface.php +++ b/src/ProxyFactoryInterface.php @@ -6,5 +6,8 @@ interface ProxyFactoryInterface { + /** + * @param class-string $className + */ public function __invoke(string $className, callable $callback): object; } diff --git a/tests/_data/fixtures/config/autoload/config.global.php b/tests/_data/fixtures/config/autoload/config.global.php new file mode 100644 index 0000000..977905f --- /dev/null +++ b/tests/_data/fixtures/config/autoload/config.global.php @@ -0,0 +1,14 @@ + [ + ProvidersCollectionIntegrationTest::CONFIG_KEY_1 => 'global config', + ], + AurynConfig::SHARING => [ + ], +]; diff --git a/tests/_data/fixtures/config/autoload/config.local.php b/tests/_data/fixtures/config/autoload/config.local.php new file mode 100644 index 0000000..b5b4ce2 --- /dev/null +++ b/tests/_data/fixtures/config/autoload/config.local.php @@ -0,0 +1,12 @@ + [ + ProvidersCollectionIntegrationTest::CONFIG_KEY_1 => 'local config', + ], +]; diff --git a/tests/_data/fixtures/config/test.global.php b/tests/_data/fixtures/config/test.global.php new file mode 100644 index 0000000..d978331 --- /dev/null +++ b/tests/_data/fixtures/config/test.global.php @@ -0,0 +1,12 @@ + [ + ProvidersCollectionIntegrationTest::CONFIG_KEY_3 => 'test.global.php', + ], +]; diff --git a/tests/_data/fixtures/fixtures.php b/tests/_data/fixtures/fixtures.php deleted file mode 100644 index 0319156..0000000 --- a/tests/_data/fixtures/fixtures.php +++ /dev/null @@ -1,756 +0,0 @@ -foo = $foo; - } -} - -class RequiresDependencyWithDefinedParam -{ - public $obj; - public function __construct(DependencyWithDefinedParam $obj) - { - $this->obj = $obj; - } -} - - -class ClassWithAliasAsParameter -{ - public $sharedClass; - - public function __construct(SharedClass $sharedClass) - { - $this->sharedClass = $sharedClass; - } -} - -class ConcreteClass1 -{ -} - -class ConcreteClass2 -{ -} - -class ClassWithoutMagicInvoke -{ -} - -class TestNoConstructor -{ -} - -class TestDependency -{ - public $testProp = 'testVal'; -} - -class TestDependency2 extends TestDependency -{ - public $testProp = 'testVal2'; -} - -class SpecdTestDependency extends TestDependency -{ - public $testProp = 'testVal'; -} - -class TestNeedsDep -{ - public function __construct(TestDependency $testDep) - { - $this->testDep = $testDep; - } -} - -class TestClassWithNoCtorTypehints -{ - public function __construct($val = 42) - { - $this->test = $val; - } -} - -class TestMultiDepsNeeded -{ - public function __construct(TestDependency $val1, TestDependency2 $val2) - { - $this->testDep = $val1; - $this->testDep = $val2; - } -} - - -class TestMultiDepsWithCtor -{ - public function __construct(TestDependency $val1, TestNeedsDep $val2) - { - $this->testDep = $val1; - $this->testDep = $val2; - } -} - -class NoTypehintNullDefaultConstructorClass -{ - public $testParam = 1; - public function __construct(TestDependency $val1, $arg = 42) - { - $this->testParam = $arg; - } -} - -class NoTypehintNoDefaultConstructorClass -{ - public $testParam = 1; - public function __construct(TestDependency $val1, $arg = null) - { - $this->testParam = $arg; - } -} - -interface DepInterface -{ -} -interface SomeInterface -{ -} -class SomeImplementation implements SomeInterface -{ -} -class PreparesImplementationTest implements SomeInterface -{ - public $testProp = 0; -} - -class DepImplementation implements DepInterface -{ - public $testProp = 'something'; -} - -class RequiresInterface -{ - public $dep; - public function __construct(DepInterface $dep) - { - $this->testDep = $dep; - } -} - -class ClassInnerA -{ - public $dep; - public function __construct(ClassInnerB $dep) - { - $this->dep = $dep; - } -} -class ClassInnerB -{ - public function __construct() - { - } -} -class ClassOuter -{ - public $dep; - public function __construct(ClassInnerA $dep) - { - $this->dep = $dep; - } -} - -class ProvTestNoDefinitionNullDefaultClass -{ - public function __construct($arg = null) - { - $this->arg = $arg; - } -} - -interface TestNoExplicitDefine -{ -} - -class InjectorTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine -{ - public $val = 42; - public function __construct($val) - { - $this->val = $val; - } -} - -class InjectorTestCtorParamWithNoTypehintOrDefaultDependent -{ - private $param; - public function __construct(TestNoExplicitDefine $param) - { - $this->param = $param; - } -} - -class InjectorTestRawCtorParams -{ - public $string; - public $obj; - public $int; - public $array; - public $float; - public $bool; - public $null; - - public function __construct($string, $obj, $int, $array, $float, $bool, $null) - { - $this->string = $string; - $this->obj = $obj; - $this->int = $int; - $this->array = $array; - $this->float = $float; - $this->bool = $bool; - $this->null = $null; - } -} - -class InjectorTestParentClass -{ - public function __construct($arg1) - { - $this->arg1 = $arg1; - } -} - -class InjectorTestChildClass extends InjectorTestParentClass -{ - public function __construct($arg1, $arg2) - { - parent::__construct($arg1); - $this->arg2 = $arg2; - } -} - -class CallableMock -{ - public function __invoke() - { - } -} - -class ProviderTestCtorParamWithNoTypehintOrDefault implements TestNoExplicitDefine -{ - public $val = 42; - public function __construct($val) - { - $this->val = $val; - } -} - -class ProviderTestCtorParamWithNoTypehintOrDefaultDependent -{ - private $param; - public function __construct(TestNoExplicitDefine $param) - { - $this->param = $param; - } -} - -class StringStdClassDelegateMock -{ - public function __invoke() - { - return $this->make(); - } - private function make() - { - $obj = new \StdClass(); - $obj->test = 42; - return $obj; - } -} - -class StringDelegateWithNoInvokeMethod -{ -} - -class ExecuteClassNoDeps -{ - public function execute() - { - return 42; - } -} - -class ExecuteClassDeps -{ - public function __construct(TestDependency $testDep) - { - } - public function execute() - { - return 42; - } -} - -class ExecuteClassDepsWithMethodDeps -{ - public function __construct(TestDependency $testDep) - { - } - public function execute(TestDependency $dep, $arg = null) - { - return isset($arg) ? $arg : 42; - } -} - -class ExecuteClassStaticMethod -{ - public static function execute() - { - return 42; - } -} - -class ExecuteClassRelativeStaticMethod extends ExecuteClassStaticMethod -{ - public static function execute() - { - return 'this should NEVER be seen since we are testing against parent::execute()'; - } -} - -class ExecuteClassInvokable -{ - public function __invoke() - { - return 42; - } -} - -function hasArrayDependency(array $parameter) -{ - return 42; -} - -function testExecuteFunction() -{ - return 42; -} - -function testExecuteFunctionWithArg(ConcreteClass1 $foo) -{ - return 42; -} - -class MadeByDelegate -{ -} - -class CallableDelegateClassTest -{ - public function __invoke() - { - return new MadeByDelegate(); - } -} - -interface DelegatableInterface -{ - public function foo(); -} - -class ImplementsInterface implements DelegatableInterface -{ - public function foo() - { - } -} - -class ImplementsInterfaceFactory -{ - public function __invoke() - { - return new ImplementsInterface(); - } -} - -class RequiresDelegatedInterface -{ - private $interface; - - public function __construct(DelegatableInterface $interface) - { - $this->interface = $interface; - } - public function foo() - { - $this->interface->foo(); - } -} - -class TestMissingDependency -{ - public function __construct(TypoInTypehint $class) - { - } -} - -class NonConcreteDependencyWithDefaultValue -{ - public $interface; - public function __construct(DelegatableInterface $i = null) - { - $this->interface = $i; - } -} - - -class ConcreteDependencyWithDefaultValue -{ - public $dependency; - public function __construct(\StdClass $instance = null) - { - $this->dependency = $instance; - } -} - -class TypelessParameterDependency -{ - public $thumbnailSize; - - public function __construct($thumbnailSize) - { - $this->thumbnailSize = $thumbnailSize; - } -} - -class RequiresDependencyWithTypelessParameters -{ - public $dependency; - - public function __construct(TypelessParameterDependency $dependency) - { - $this->dependency = $dependency; - } - - public function getThumbnailSize() - { - return $this->dependency->thumbnailSize; - } -} - -class HasNonPublicConstructor -{ - protected function __construct() - { - } -} - -class HasNonPublicConstructorWithArgs -{ - protected function __construct($arg1, $arg2, $arg3) - { - } -} - -class ClassWithCtor -{ - public function __construct() - { - } -} - -class TestDependencyWithProtectedConstructor -{ - protected function __construct() - { - } - - public static function create() - { - return new self(); - } -} - -class TestNeedsDepWithProtCons -{ - public function __construct(TestDependencyWithProtectedConstructor $dep) - { - $this->dep = $dep; - } -} - -class SimpleNoTypehintClass -{ - public $testParam = 1; - - public function __construct($arg) - { - $this->testParam = $arg; - } -} - -class SomeClassName -{ -} - -class TestDelegationSimple -{ - public $delgateCalled = false; -} - -class TestDelegationDependency -{ - public $delgateCalled = false; - public function __construct(TestDelegationSimple $testDelegationSimple) - { - } -} - -function createTestDelegationSimple() -{ - $instance = new TestDelegationSimple(); - $instance->delegateCalled = true; - - return $instance; -} - -function createTestDelegationDependency(TestDelegationSimple $testDelegationSimple) -{ - $instance = new TestDelegationDependency($testDelegationSimple); - $instance->delegateCalled = true; - - return $instance; -} - - -class BaseExecutableClass -{ - public function foo() - { - return 'This is the BaseExecutableClass'; - } - public static function bar() - { - return 'This is the BaseExecutableClass'; - } -} - -class ExtendsExecutableClass extends BaseExecutableClass -{ - public function foo() - { - return 'This is the ExtendsExecutableClass'; - } - public static function bar() - { - return 'This is the ExtendsExecutableClass'; - } -} - -class ReturnsCallable -{ - private $value = 'original'; - - public function __construct($value) - { - $this->value = $value; - } - - public function getCallable() - { - $callable = function () { - return $this->value; - }; - - return $callable; - } -} - -class DelegateClosureInGlobalScope -{ -} - -function getDelegateClosureInGlobalScope() -{ - return function () { - return new DelegateClosureInGlobalScope(); - }; -} - -class CloneTest -{ - public $injector; - public function __construct(\Auryn\Injector $injector) - { - $this->injector = clone $injector; - } -} - -abstract class AbstractExecuteTest -{ - public function process() - { - return "Abstract"; - } -} - -class ConcreteExexcuteTest extends AbstractExecuteTest -{ - public function process() - { - return "Concrete"; - } -} - -class DependencyChainTest -{ - public function __construct(DepInterface $dep) - { - } -} - -class ParentWithConstructor -{ - public $foo; - function __construct($foo) - { - $this->foo = $foo; - } -} - -class ChildWithoutConstructor extends ParentWithConstructor -{ -} - -class DelegateA -{ -} -class DelegatingInstanceA -{ - public function __construct(DelegateA $a) - { - $this->a = $a; - } -} - -class DelegateB -{ -} -class DelegatingInstanceB -{ - public function __construct(DelegateB $b) - { - $this->b = $b; - } -} - -class ThrowsExceptionInConstructor -{ - public function __construct() - { - throw new \Exception('Exception in constructor'); - } -} diff --git a/tests/_data/fixtures/fixtures_5_6.php b/tests/_data/fixtures/fixtures_5_6.php deleted file mode 100644 index 6888047..0000000 --- a/tests/_data/fixtures/fixtures_5_6.php +++ /dev/null @@ -1,24 +0,0 @@ -testParam = $arg; - } -} - -class TypehintNoDefaultConstructorVariadicClass -{ - public $testParam = 1; - public function __construct(TestDependency ...$arg) - { - $this->testParam = $arg; - } -} diff --git a/tests/_data/fixtures/modules/ModuleStub1.php b/tests/_data/fixtures/modules/ModuleStub1.php new file mode 100644 index 0000000..0101352 --- /dev/null +++ b/tests/_data/fixtures/modules/ModuleStub1.php @@ -0,0 +1,24 @@ + [ + SharedAliasedInterface::class => SharedClass::class, + ], + AurynConfig::SHARING => [ + ], + AurynConfig::DEFINITIONS => [ + ], + ]; + } +} diff --git a/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php b/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php new file mode 100644 index 0000000..45ebcef --- /dev/null +++ b/tests/_data/fixtures/src/ConcreteNeedsSomeInterface.php @@ -0,0 +1,23 @@ +someInterface = $someInterface; + } + + public function someInterface(): SomeInterface + { + return $this->someInterface; + } +} diff --git a/tests/_data/fixtures/src/ContainerBuilderExtensionStub.php b/tests/_data/fixtures/src/ContainerBuilderExtensionStub.php new file mode 100644 index 0000000..887961e --- /dev/null +++ b/tests/_data/fixtures/src/ContainerBuilderExtensionStub.php @@ -0,0 +1,27 @@ +walk( + 'container_builder_test_aliases', + static function (string $alias, string $typeHint, Injector $injector): void { + $injector->alias($typeHint, $alias); + } + ); + } +} diff --git a/tests/_data/fixtures/src/SomeConcrete.php b/tests/_data/fixtures/src/SomeConcrete.php new file mode 100644 index 0000000..9398f50 --- /dev/null +++ b/tests/_data/fixtures/src/SomeConcrete.php @@ -0,0 +1,13 @@ +name(); + } +} diff --git a/tests/_data/fixtures/src/SomeInterface.php b/tests/_data/fixtures/src/SomeInterface.php new file mode 100644 index 0000000..f040b05 --- /dev/null +++ b/tests/_data/fixtures/src/SomeInterface.php @@ -0,0 +1,10 @@ +make([ AurynConfig::SHARING => [ stdClass::class, ], ]); $resolver = new AurynConfig($injector, $config); - $resolver->resolve(); + $resolver->apply(); - $class = $injector->make(stdClass::class); + $injector->make(stdClass::class); } /** @@ -36,10 +36,10 @@ public function benchResolver() * @Revs(1000) * @Iterations(5) */ - public function benchResolverP() + public function benchResolverP(): void { $injector = new Injector(); $injector->share(stdClass::class); - $class = $injector->make(stdClass::class); + $injector->make(stdClass::class); } } diff --git a/tests/src/UnitTestCase.php b/tests/src/UnitTestCase.php index 49de625..0e7ee23 100644 --- a/tests/src/UnitTestCase.php +++ b/tests/src/UnitTestCase.php @@ -2,33 +2,89 @@ declare(strict_types=1); -namespace ItalyStrap\Tests; +namespace ItalyStrap\Empress\Tests; +use Auryn\Injector; use Codeception\Test\Unit; -use ItalyStrap\Empress\Injector; +use ItalyStrap\Config\Config; +use ItalyStrap\Config\ConfigInterface; +use ItalyStrap\Empress\ProxyFactory; +use ItalyStrap\Empress\ProxyFactoryInterface; +use ItalyStrap\Finder\FinderInterface; +use Prophecy\PhpUnit\ProphecyTrait; use Prophecy\Prophecy\ObjectProphecy; -use Prophecy\Prophet; -use UnitTester; class UnitTestCase extends Unit { + use ProphecyTrait; + protected UnitTester $tester; + + protected Injector $realInjector; + + protected function makeRealInjector(): Injector + { + return $this->realInjector; + } + protected ObjectProphecy $injector; - protected Prophet $prophet; protected function makeInjector(): Injector { return $this->injector->reveal(); } + protected ProxyFactoryInterface $realProxyFactory; + + protected ?ObjectProphecy $proxyFactory = null; + + protected function makeProxyFactory(): ProxyFactoryInterface + { + return $this->proxyFactory->reveal(); + } + + protected ObjectProphecy $config; + + protected function makeConfig(): ConfigInterface + { + return $this->config->reveal(); + } + + protected ConfigInterface $configReal; + + protected function makeConfigReal(): ConfigInterface + { + return $this->configReal; + } + + protected ObjectProphecy $finder; + + protected function makeFinder(): FinderInterface + { + return $this->finder->reveal(); + } + + protected string $cachedConfigFile; + // phpcs:ignore -- Method from Codeception protected function _before(): void { - $this->prophet = new Prophet(); - $this->injector = $this->prophet->prophesize(Injector::class); + $this->realInjector = new Injector(); + $this->realProxyFactory = new ProxyFactory(); + $this->configReal = new Config(); + $this->injector = $this->prophesize(Injector::class); + $this->proxyFactory = $this->prophesize(ProxyFactoryInterface::class); + $this->config = $this->prophesize(Config::class); + $this->finder = $this->prophesize(FinderInterface::class); + + $this->cachedConfigFile = codecept_output_dir('config-cache.php'); } // phpcs:ignore -- Method from Codeception protected function _after(): void { $this->prophet->checkPredictions(); + unset($this->configReal); + unset($this->realInjector); + unset($this->realProxyFactory); + \file_exists($this->cachedConfigFile) and unlink($this->cachedConfigFile); } } diff --git a/tests/unit.suite.yml b/tests/unit.suite.yml index 00565f6..7ccc1a6 100644 --- a/tests/unit.suite.yml +++ b/tests/unit.suite.yml @@ -1,10 +1,4 @@ -# Codeception Test Suite Configuration -# -# Suite for unit or integration tests. - actor: UnitTester modules: enabled: - - Asserts - - \Helper\Unit - step_decorators: ~ \ No newline at end of file + - \ItalyStrap\Empress\Tests\Helper\Unit \ No newline at end of file diff --git a/tests/unit/AurynConfigIntegrationTest.php b/tests/unit/AurynConfigIntegrationTest.php new file mode 100644 index 0000000..21d8be7 --- /dev/null +++ b/tests/unit/AurynConfigIntegrationTest.php @@ -0,0 +1,102 @@ +realInjector, (new ConfigFactory())->make($config), $this->realProxyFactory); + } + + public function testItShouldAlias(): void + { + $aurynConfig = $this->makeInstance( + [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ] + ); + + $aurynConfig->apply(); + + $this->assertInstanceOf(SomeConcrete::class, $this->realInjector->make(SomeInterface::class)); + $this->assertInstanceOf(SomeConcrete::class, $this->realInjector->make(SomeConcrete::class)); + $object = $this->realInjector->make(ConcreteNeedsSomeInterface::class); + $actual = $object->someInterface(); + $this->assertInstanceOf(SomeConcrete::class, $actual); + $this->assertSame('SomeConcrete', $actual->render()); + } + + public function testItShouldShare(): void + { + $aurynConfig = $this->makeInstance( + [ + AurynConfig::SHARING => [ + SomeConcrete::class, + ], + ] + ); + + $aurynConfig->apply(); + + $shared = $this->realInjector->make(SomeConcrete::class); + $this->assertSame($shared, $this->realInjector->make(SomeConcrete::class)); + } + + public function testItShouldProxy(): void + { + $this->proxyFactory->__invoke(Argument::type('string'), Argument::type('callable')) + ->willReturn(new class + { + public function render(): string + { + return 'DifferentConcrete'; + } + }) + ->shouldBeCalledTimes(1); + + $this->realProxyFactory = $this->proxyFactory->reveal(); + $sut = $this->makeInstance( + [ + AurynConfig::PROXY => [ + SomeConcrete::class + ], + ] + ); + + $sut->apply(); + + /** @var SomeConcrete $concrete */ + $concrete = $this->realInjector->make(SomeConcrete::class); + $this->assertSame('DifferentConcrete', $concrete->render()); + } + + public function testItShouldThrowWhenProxyFactoryIsMissingForRealClassProxyConfiguration(): void + { + $sut = new AurynConfig( + $this->realInjector, + (new ConfigFactory())->make([ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]) + ); + + $this->expectException(ConfigException::class); + + $sut->apply(); + } +} diff --git a/tests/unit/AurynConfigTest.php b/tests/unit/AurynConfigTest.php index 77ad747..fb07649 100644 --- a/tests/unit/AurynConfigTest.php +++ b/tests/unit/AurynConfigTest.php @@ -2,25 +2,105 @@ declare(strict_types=1); -namespace ItalyStrap\Tests\Unit; +namespace ItalyStrap\Empress\Tests\Unit; +use Auryn\Injector; +use Auryn\ConfigException; use ItalyStrap\Config\ConfigFactory; -use ItalyStrap\Empress\Injector; use ItalyStrap\Empress\AurynConfig; use ItalyStrap\Empress\AurynConfigInterface; use ItalyStrap\Empress\Extension; -use ItalyStrap\Tests\UnitTestCase; +use ItalyStrap\Empress\Tests\SomeConcrete; +use ItalyStrap\Empress\Tests\SomeExtension; +use ItalyStrap\Empress\Tests\UnitTestCase; use PHPUnit\Framework\Assert; use Prophecy\Argument; -class AurynConfigTest extends UnitTestCase +final class AurynConfigTest extends UnitTestCase { protected function makeInstance(array $config = []): AurynConfig { - return new AurynConfig($this->makeInjector(), ConfigFactory::make($config)); + return new AurynConfig($this->makeInjector(), (new ConfigFactory())->make($config), $this->makeProxyFactory()); } - public function shareProvider() +// public function testItShouldProxy(): void +// { +// $mockProxyFactory = $this->prophesize(ProxyFactoryInterface::class); +// $mockProxyFactory->__invoke(Argument::type('string'), Argument::type('callable')) +// ->shouldBeCalledTimes(1); +// +// $this->proxyFactory = $mockProxyFactory->reveal(); +// $sut = $this->makeInstance( +// [ +// AurynConfig::PROXY => [ +// SomeConcrete::class +// ], +// ] +// ); +// +// $sut->$this->apply(); +// +// $concrete = $this->realInjector->make(SomeConcrete::class); +// } + + public function testItShouldProxy01(): void + { + + $expected = 'SomeClassProxies'; + + $this->injector->proxy( + Argument::type('string'), + Argument::type('callable') + )->will(function ($args) use ($expected): void { + Assert::assertEquals($expected, $args[0], ''); + }); + + $sut = $this->makeInstance( + [ + AurynConfig::PROXY => [ + $expected, + ], + ] + ); + + $sut->apply(); + } + + public function testItShouldSkipProxyConfigurationWhenProxyFactoryIsMissing(): void + { + $this->injector + ->proxy(Argument::any(), Argument::any()) + ->shouldNotBeCalled(); + + $sut = new AurynConfig( + $this->makeInjector(), + (new ConfigFactory())->make([ + AurynConfig::PROXY => [ + 'SomeClassProxies', + ], + ]) + ); + + $sut->apply(); + } + + public function testItShouldThrowWhenProxyFactoryIsMissingForRealClassProxyConfiguration(): void + { + $sut = new AurynConfig( + $this->makeInjector(), + (new ConfigFactory())->make([ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]) + ); + + $this->expectException(ConfigException::class); + + $sut->apply(); + } + + public function shareProvider(): iterable { return [ 'ClassName' => [ @@ -39,7 +119,7 @@ public function shareProvider() public function testItShouldShare($expected): void { - $this->injector->share(Argument::any())->will(function ($args) use ($expected) { + $this->injector->share(Argument::any())->will(function ($args) use ($expected): void { Assert::assertEquals($expected, $args[0], ''); }); @@ -51,30 +131,7 @@ public function testItShouldShare($expected): void ] ); - $sut->resolve(); - } - - public function testItShouldProxy(): void - { - - $expected = 'SomeClassProxies'; - - $this->injector->proxy( - Argument::type('string'), - Argument::type('callable') - )->will(function ($args) use ($expected) { - Assert::assertEquals($expected, $args[0], ''); - }); - - $sut = $this->makeInstance( - [ - AurynConfig::PROXY => [ - $expected, - ], - ] - ); - - $sut->resolve(); + $sut->apply(); } public function testItShouldAlias(): void @@ -82,7 +139,7 @@ public function testItShouldAlias(): void $this->injector ->alias(Argument::type('string'), Argument::type('string')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertEquals('InterfaceName', $args[0], ''); Assert::assertEquals('ClassName', $args[1], ''); }); @@ -95,7 +152,7 @@ public function testItShouldAlias(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldDefine(): void @@ -103,7 +160,7 @@ public function testItShouldDefine(): void $this->injector ->define(Argument::type('string'), Argument::type('array')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertEquals('ClassName', $args[0], ''); Assert::assertArrayHasKey(':config', $args[1], ''); }); @@ -119,7 +176,7 @@ public function testItShouldDefine(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldDefineParam(): void @@ -130,7 +187,7 @@ public function testItShouldDefineParam(): void $this->injector ->defineParam(Argument::type('string'), Argument::any()) - ->will(function ($args) use ($param_expected) { + ->will(function ($args) use ($param_expected): void { Assert::assertEquals(':config', $args[0], ''); Assert::assertEquals($param_expected, $args[1], ''); }); @@ -143,20 +200,18 @@ public function testItShouldDefineParam(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldDelegate(): void { - $factory_delegation = function () { - return new class { - }; + $factory_delegation = fn(): object => new class { }; $this->injector ->delegate(Argument::type('string'), Argument::any()) - ->will(function ($args) use ($factory_delegation) { + ->will(function ($args) use ($factory_delegation): void { Assert::assertEquals(':config', $args[0], ''); Assert::assertEquals($factory_delegation, $args[1], ''); Assert::assertIsCallable($args[1], ''); @@ -170,22 +225,22 @@ public function testItShouldDelegate(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldPrepare(): void { - $preparation_callback = function ($class, $injector) { + $preparation_callback = function ($class, $injector): void { Assert::assertEquals('ClassName', $class, ''); Assert::assertInstanceOf(Injector::class, $injector, ''); }; - $test = $this->prophet; + $test = $this; $this->injector ->prepare(Argument::type('string'), Argument::any()) - ->will(function ($args) use ($preparation_callback, $test) { + ->will(function ($args) use ($preparation_callback, $test): void { Assert::assertEquals('ClassName', $args[0], ''); Assert::assertEquals($preparation_callback, $args[1], ''); Assert::assertIsCallable($args[1], ''); @@ -204,7 +259,7 @@ public function testItShouldPrepare(): void ] ); - $sut->resolve(); + $sut->apply(); } public function testItShouldWalk(): void @@ -217,7 +272,7 @@ public function testItShouldWalk(): void ] ); - $sut->walk('Test', function (string $value, $key) { + $sut->walk('Test', function (string $value, $key): void { Assert::assertStringContainsString($value, 'ClassName', ''); Assert::assertStringContainsString($key, 'Key', ''); }); @@ -229,12 +284,12 @@ public function testItShouldExtendFakeClass(): void $this ->injector ->share(Argument::type('string'), Argument::any()) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); $this->injector->make(Argument::type('string'), Argument::type('array')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); @@ -247,28 +302,28 @@ public function testItShouldExtendFakeClass(): void ] ); - $extension = $this->prophet->prophesize(Extension::class); + $extension = $this->prophesize(Extension::class); $extension->name()->willReturn('ExtensionName'); - $extension->execute(Argument::exact($sut))->will(function ($args) { + $extension->execute(Argument::exact($sut))->will(function ($args): void { }); $sut->extend($extension->reveal()); - $sut->resolve(); + $sut->apply(); } public function testItShouldExtendRealClass(): void { $this->injector->share(Argument::type('string'), Argument::any()) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); $this->injector->make(Argument::type('string'), Argument::type('array')) - ->will(function ($args) { + ->will(function ($args): void { Assert::assertStringContainsString('ClassName', $args[0], ''); }); @@ -290,27 +345,35 @@ public function name(): string return self::SUBSCRIBERS; } - public function execute(AurynConfigInterface $application) + public function execute(AurynConfigInterface $application): void { - $application->walk(self::SUBSCRIBERS, [ $this, 'method' ]); + $application->walk(self::SUBSCRIBERS, $this); } - public function method(string $class, $index_or_optionName, Injector $injector) + public function __invoke(string $class, $index_or_optionName, Injector $injector): void { Assert::assertStringContainsString($class, 'ClassName', ''); $injector->share($class); $injector->make($class, []); - -// if ( empty( $config->get( $index_or_optionName, '' ) ) ) { -// return; -// } -// -// $event_manager = $injector->make( EventManager::class ); -// $event_manager->add_subscriber( $injector->share( $class )->make( $class ) ); } }); - $sut->resolve(); + $sut->apply(); + } + + public function testItShouldExtendClassString(): void + { + $sut = new AurynConfig(new Injector(), (new ConfigFactory())->make()); + $sut->extend(SomeExtension::class); + $this->expectOutputString(SomeExtension::class); + $sut->apply(); + } + + public function testItShouldNotExtend(): void + { + $sut = new AurynConfig(new Injector(), (new ConfigFactory())->make()); + $this->expectException(\InvalidArgumentException::class); + $sut->extend('SomeGenericClass'); } public function testOldClassNameShouldBeAliasedCorrectly(): void @@ -318,7 +381,7 @@ public function testOldClassNameShouldBeAliasedCorrectly(): void /** * New name is AurynConfig::class */ - $auryn_config = new \ItalyStrap\Empress\AurynResolver(new Injector(), ConfigFactory::make([])); - $auryn_config->resolve(); + $auryn_config = new \ItalyStrap\Empress\AurynResolver(new Injector(), (new ConfigFactory())->make([])); + $auryn_config->apply(); } } diff --git a/tests/unit/ContainerBuilderTest.php b/tests/unit/ContainerBuilderTest.php new file mode 100644 index 0000000..c3666ac --- /dev/null +++ b/tests/unit/ContainerBuilderTest.php @@ -0,0 +1,308 @@ +addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + AurynConfig::SHARING => [ + SomeConcrete::class, + ], + ]; + } + }); + + $this->assertSame($builder, $result); + + $container = $builder->build(); + + $this->assertInstanceOf(ContainerInterface::class, $container); + $this->assertSame($container, $container->get(ContainerInterface::class)); + + $service = $container->get(SomeInterface::class); + $this->assertInstanceOf(SomeConcrete::class, $service); + $this->assertSame('SomeConcrete', $service->render()); + } + + public function testAddModuleIsFluentAliasForAddProvider(): void + { + $builder = new ContainerBuilder(); + + $result = $builder->addModule(static fn(): array => [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]); + + $this->assertSame($builder, $result); + $this->assertInstanceOf(SomeConcrete::class, $builder->build()->get(SomeInterface::class)); + } + + public function testExtendIsFluentAndAppliesExtensionInstance(): void + { + $extension = new class implements Extension { + public bool $executed = false; + + public function name(): string + { + return self::class; + } + + public function execute(AurynConfigInterface $application): void + { + $this->executed = true; + } + }; + $builder = new ContainerBuilder(); + + $result = $builder->extend($extension); + $builder->build(); + + $this->assertSame($builder, $result); + $this->assertTrue($extension->executed); + } + + public function testExtendAcceptsExtensionClassString(): void + { + $config = new Config([ + 'container_builder_test_aliases' => [ + SomeInterface::class => SomeConcrete::class, + ], + ]); + $builder = new ContainerBuilder(null, $config); + + $builder->extend(ContainerBuilderExtensionStub::class); + $container = $builder->build(); + + $this->assertInstanceOf(SomeConcrete::class, $container->get(SomeInterface::class)); + } + + public function testBuildMergesMultipleModulesAndSharesAliasedService(): void + { + $builder = new ContainerBuilder(); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]; + } + }); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::SHARING => [ + SomeConcrete::class, + ], + ]; + } + }); + + $container = $builder->build(); + + $first = $container->get(SomeInterface::class); + $second = $container->get(SomeConcrete::class); + + $this->assertInstanceOf(SomeConcrete::class, $first); + $this->assertSame($first, $second, 'Aliased service should be shared across resolutions'); + } + + public function testBuildDelegationsCanRequestContainerInterface(): void + { + $builder = new ContainerBuilder(); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]; + } + }); + + $builder->addProvider(new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::DELEGATIONS => [ + ConcreteNeedsSomeInterface::class + => static function (ContainerInterface $container): ConcreteNeedsSomeInterface { + $some = $container->get(SomeInterface::class); + return new ConcreteNeedsSomeInterface($some); + } + ], + ]; + } + }); + + $container = $builder->build(); + + $service = $container->get(ConcreteNeedsSomeInterface::class); + $this->assertInstanceOf( + ConcreteNeedsSomeInterface::class, + $service, + 'Service should be an instance of ConcreteNeedsSomeInterface' + ); + $this->assertInstanceOf( + SomeConcrete::class, + $service->someInterface(), + 'Dependency should be an instance of SomeConcrete' + ); + } + + public function testBuildUsesCustomInjectorAndSharesIt(): void + { + $injector = new Injector(); + $builder = new ContainerBuilder($injector); + + $container = $builder->build(); + + $this->assertSame($injector, $injector->make(Injector::class)); + $this->assertSame($container, $injector->make(ContainerInterface::class)); + } + + public function testBuildPopulatesCustomConfig(): void + { + $config = new Config(); + $builder = new ContainerBuilder(null, $config); + + $builder->addProvider(static fn(): array => [ + 'custom' => [ + 'key' => 'value', + ], + ]); + + $builder->build(); + + $this->assertSame('value', $config->get('custom.key')); + } + + public function testBuildPassesCustomCacheToProvidersCollection(): void + { + $cache = new class implements ProvidersCacheInterface { + public bool $written = false; + + public function read(ConfigInterface $config): bool + { + $config->merge([ + AurynConfig::ALIASES => [ + SomeInterface::class => SomeConcrete::class, + ], + ]); + + return true; + } + + public function write(ConfigInterface $config): void + { + $this->written = true; + } + }; + $builder = new ContainerBuilder(null, null, $cache); + $builder->addProvider(static function (): array { + throw new \RuntimeException('Provider should not be executed when cache is warm'); + }); + + $container = $builder->build(); + + $this->assertInstanceOf(SomeConcrete::class, $container->get(SomeInterface::class)); + $this->assertFalse($cache->written); + } + + public function testBuildPassesCustomProxyFactoryToAurynConfig(): void + { + $factory = new class implements ProxyFactoryInterface { + public bool $called = false; + + public function __invoke(string $className, callable $callback): object + { + $this->called = true; + + return new class extends SomeConcrete { + public function render(): string + { + return 'ProxiedConcrete'; + } + }; + } + }; + $builder = new ContainerBuilder(null, null, null, $factory); + $builder->addProvider(static fn(): array => [ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]); + + /** @var SomeConcrete $service */ + $service = $builder->build()->get(SomeConcrete::class); + + $this->assertTrue($factory->called); + $this->assertSame('ProxiedConcrete', $service->render()); + } + + public function testBuildIgnoresProxyConfigurationWhenProxyFactoryIsMissing(): void + { + $builder = new ContainerBuilder(); + $builder->addProvider(static fn(): array => [ + AurynConfig::PROXY => [ + 'SomeClassProxies', + ], + ]); + + /** @var SomeConcrete $service */ + $service = $builder->build()->get(SomeConcrete::class); + + $this->assertSame('SomeConcrete', $service->render()); + } + + public function testBuildThrowsWhenProxyFactoryIsMissingForRealClassProxyConfiguration(): void + { + $builder = new ContainerBuilder(); + $builder->addProvider(static fn(): array => [ + AurynConfig::PROXY => [ + SomeConcrete::class, + ], + ]); + + $this->expectException(ConfigException::class); + + $builder->build(); + } +} diff --git a/tests/unit/ContainerTest.php b/tests/unit/ContainerTest.php new file mode 100644 index 0000000..968f1e7 --- /dev/null +++ b/tests/unit/ContainerTest.php @@ -0,0 +1,78 @@ +makeRealInjector()); + } + + public function testHasReturnsTrueForExistingClass(): void + { + $sut = $this->makeInstance(); + $this->assertTrue($sut->has(SomeConcrete::class)); + } + + public function testHasReturnsFalseForNonExistingService(): void + { + $sut = $this->makeInstance(); + $this->assertFalse($sut->has('non.existing.service')); + } + + public function testGetThrowsNotFoundExceptionForNonExistingService(): void + { + $sut = $this->makeInstance(); + + $this->expectException(NotFoundExceptionInterface::class); + + $sut->get('non.existing.service'); + } + + public function testGetNotFoundExceptionIsAlsoContainerException(): void + { + $sut = $this->makeInstance(); + + try { + $sut->get('non.existing.service'); + $this->fail('Expected a not found exception'); + } catch (NotFoundExceptionInterface $exception) { + $this->assertInstanceOf(ContainerExceptionInterface::class, $exception); + $this->assertSame("Service 'non.existing.service' not found", $exception->getMessage()); + } + } + + public function testGetReturnsInstanceForExistingClass(): void + { + $sut = $this->makeInstance(); + $instance = $sut->get(SomeConcrete::class); + $this->assertInstanceOf(SomeConcrete::class, $instance); + } + + public function testGetWrapsAurynResolutionErrorsInContainerException(): void + { + $sut = $this->makeInstance(); + + try { + $sut->get(ConcreteNeedsSomeInterface::class); + $this->fail('Expected a container exception'); + } catch (ContainerExceptionInterface $exception) { + $this->assertNotInstanceOf(NotFoundExceptionInterface::class, $exception); + $this->assertStringContainsString( + ConcreteNeedsSomeInterface::class, + $exception->getMessage() + ); + $this->assertNotNull($exception->getPrevious()); + } + } +} diff --git a/tests/unit/PhpFileProviderTest.php b/tests/unit/PhpFileProviderTest.php new file mode 100644 index 0000000..9af6995 --- /dev/null +++ b/tests/unit/PhpFileProviderTest.php @@ -0,0 +1,38 @@ +makeFinder()); + } + + public function testShouldBeInvokable(): void + { + $file = \codecept_data_dir('fixtures/config/autoload/config.global.php'); + + $this->finder->names(['pattern'])->will(function ($args): void { + Assert::assertSame('pattern', $args[0][0], 'Should be the same pattern'); + }); + + $this->finder->getIterator()->willReturn(new \ArrayIterator([ + $file, + ])); + + $expected = require $file; + + $sut = $this->makeInstance(); + foreach ($sut() as $actual) { + $this->assertEquals($expected, $actual, 'Should be expected file'); + break; // Only do on first iteration + } + } +} diff --git a/tests/unit/ProvidersCacheTest.php b/tests/unit/ProvidersCacheTest.php new file mode 100644 index 0000000..092313c --- /dev/null +++ b/tests/unit/ProvidersCacheTest.php @@ -0,0 +1,177 @@ + + */ + private array $pathsToRemove = []; + + // phpcs:ignore -- Method from Codeception + protected function _after(): void { + foreach (\array_reverse($this->pathsToRemove) as $path) { + if (\is_file($path)) { + \unlink($path); + continue; + } + + if (\is_dir($path)) { + \rmdir($path); + } + } + + parent::_after(); + } + + public function testReadReturnsFalseWhenNoCachePathIsConfigured(): void + { + $sut = new ProvidersCache(); + + $this->assertFalse($sut->read(new Config())); + } + + public function testReadReturnsFalseWhenCachePathDoesNotExist(): void + { + $sut = new ProvidersCache($this->cacheFile('missing-cache.php'), 0666, true); + + $this->assertFalse($sut->read(new Config())); + } + + public function testReadReturnsFalseWhenCachePathIsNotAFile(): void + { + $directory = $this->cacheDirectory('cache-directory'); + $sut = new ProvidersCache($directory, 0666, true); + + $this->assertFalse($sut->read(new Config())); + } + + public function testReadMergesArrayReturnedByCacheFile(): void + { + $file = $this->writeCacheFile('read-cache.php', <<<'PHP' + 'value', + 'nested' => [ + 'key' => 'nested-value', + ], +]; +PHP); + $config = new Config(); + $sut = new ProvidersCache($file, 0666, true); + + $this->assertTrue($sut->read($config)); + $this->assertSame('value', $config->get('key')); + $this->assertSame('nested-value', $config->get('nested.key')); + } + + public function testReadThrowsWhenCacheFileDoesNotReturnArray(): void + { + $file = $this->writeCacheFile('non-array-cache.php', <<<'PHP' +expectException(\ErrorException::class); + $this->expectExceptionMessage('Configuration cache must return an array'); + + $sut->read(new Config()); + } + + public function testReadWrapsErrorsThrownByCacheFile(): void + { + $file = $this->writeCacheFile('throwing-cache.php', <<<'PHP' +read(new Config()); + $this->fail('Expected cache read failure'); + } catch (\ErrorException $exception) { + $this->assertSame('Configuration cache cannot be read', $exception->getMessage()); + $this->assertInstanceOf(\RuntimeException::class, $exception->getPrevious()); + $this->assertSame('Broken cache file', $exception->getPrevious()->getMessage()); + } + } + + public function testWriteReturnsWhenCacheIsDisabled(): void + { + $file = $this->cacheFile('disabled-write-cache.php'); + $sut = new ProvidersCache($file); + + $sut->write(new Config([ + 'key' => 'value', + ])); + + $this->assertFalse(\is_file($file)); + } + + public function testWriteCreatesReadableCacheFile(): void + { + $file = $this->cacheFile('written-cache.php'); + $this->pathsToRemove[] = $file; + $config = new Config([ + 'key' => 'value', + ]); + $sut = new ProvidersCache($file, 0666, true); + + $sut->write($config); + + $this->assertFileExists($file); + $this->assertFileIsReadable($file); + $cachedConfig = require $file; + + $this->assertIsArray($cachedConfig); + $this->assertSame('value', $cachedConfig['key']); + } + + public function testWriteThrowsWhenCacheFileCannotBeWritten(): void + { + $config = new Config(); + $sut = new ProvidersCache($this->cacheFile('missing-directory/cache.php'), 0666, true); + + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage('Configuration cache cannot be written'); + + $sut->write($config); + } + + private function cacheFile(string $name): string + { + return \codecept_output_dir($name); + } + + private function cacheDirectory(string $name): string + { + $directory = \codecept_output_dir($name); + + if (!\is_dir($directory)) { + \mkdir($directory); + } + + $this->pathsToRemove[] = $directory; + return $directory; + } + + private function writeCacheFile(string $name, string $contents): string + { + $file = $this->cacheFile($name); + \file_put_contents($file, $contents); + $this->pathsToRemove[] = $file; + + return $file; + } +} diff --git a/tests/unit/ProvidersCollectionIntegrationTest.php b/tests/unit/ProvidersCollectionIntegrationTest.php new file mode 100644 index 0000000..d43bdb2 --- /dev/null +++ b/tests/unit/ProvidersCollectionIntegrationTest.php @@ -0,0 +1,134 @@ +makeConfigReal(); + return new ProvidersCollection( + new Injector(), + $config, + new ProvidersCache($this->cachedConfigFile, 0666, true), + [ + new PhpFileProvider( + '/config/autoload/{{,*.}global,{,*.}local}.php', + (new FinderFactory()) + ->make() + ->in(codecept_data_dir('fixtures')) + ), + static fn(): array => [ + AurynConfig::ALIASES => [ + self::CONFIG_KEY_2 => 'array config', + ], + AurynConfig::SHARING => [ + ], + ], + static function (): iterable { + yield [ + AurynConfig::ALIASES => [ + self::CONFIG_KEY_2 => 'iterable config', + ], + ]; + }, + static fn(): array => [ + AurynConfig::ALIASES => [ + 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\GlobalDispatcher", + 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\SubscriberRegister", + 'ItalyStrap\View\ViewInterface' => "ItalyStrap\View\View", + 15 => 'value', + ], + ], + static fn(): array => [ + AurynConfig::ALIASES => [ + 'ItalyStrap\Event\GlobalDispatcherInterface' => "ItalyStrap\Event\DifferentDispatcher", + 'talyStrap\Event\SubscriberRegisterInterface ' => "ItalyStrap\Event\DifferentRegister", + 'ItalyStrap\HTML\TagInterface' => "ItalyStrap\HTML\Tag", + 15 => 'newValue', + ], + ], + ModuleStub1::class, + [ModuleStub1::class, '__invoke'], + new class implements ModuleInterface { + public function __invoke(): iterable + { + return [ + AurynConfig::DELEGATIONS => [ + ConcreteNeedsSomeInterface::class + => static function (ContainerInterface $container): ConcreteNeedsSomeInterface { + $some = $container->get(SomeInterface::class); + return new ConcreteNeedsSomeInterface($some); + } + ], + ]; + } + }, + fn(): array => require \codecept_data_dir('fixtures/config/test.global.php'), + ], + ); + } + + public function testIntegration(): void + { + $sut = $this->makeInstance(); + $sut->aggregate(); + $config = $this->makeConfigReal(); + + $this->assertSame( + 'local config', + $config->get([ + AurynConfig::ALIASES, + self::CONFIG_KEY_1, + ]) + ); + + $this->assertSame( + 'iterable config', + $config->get([ + AurynConfig::ALIASES, + self::CONFIG_KEY_2, + ]) + ); + + $this->assertSame( + 'test.global.php', + $config->get([ + AurynConfig::ALIASES, + self::CONFIG_KEY_3, + ]) + ); + + $this->assertFileExists($this->cachedConfigFile); + $this->assertFileIsReadable($this->cachedConfigFile); + + $file = require $this->cachedConfigFile; + $this->assertIsArray($file); + + $aliases = $config->get(AurynConfig::ALIASES); + + $this->assertSame('value', $aliases[0]); + $this->assertSame('newValue', $aliases[1]); + $this->assertCount(10, $aliases, 'Should be 10'); + } +} diff --git a/tests/unit/ProvidersCollectionTest.php b/tests/unit/ProvidersCollectionTest.php new file mode 100644 index 0000000..5a02781 --- /dev/null +++ b/tests/unit/ProvidersCollectionTest.php @@ -0,0 +1,375 @@ +makeInstance([ + static fn(): array => [ + AurynConfig::ALIASES => [ + 'InterfaceName' => 'ClassName', + ], + ], + ], null, $config); + + $sut->aggregate(); + + $this->assertSame('ClassName', $config->get(AurynConfig::ALIASES . '.InterfaceName')); + } + + public function testConstructorRequiresConfigWithNodeManipulationSupport(): void + { + $config = $this->prophesize(ConfigInterface::class)->reveal(); + + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage(NodeManipulationInterface::class); + + new ProvidersCollection(new Injector(), $config); + } + + public function testAggregateAppendsListValuesAndReplacesMapValuesRecursively(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + static fn(): array => [ + AurynConfig::PROXY => [ + 'FirstProxy', + 'SecondProxy', + ], + AurynConfig::SHARING => [ + 'FirstShared', + ], + AurynConfig::ALIASES => [ + 'InterfaceName' => 'FirstClass', + ], + AurynConfig::DEFINITIONS => [ + 'ServiceName' => [ + ':overridden' => 'first', + ':kept' => 'kept', + ], + ], + ], + static fn(): array => [ + AurynConfig::PROXY => [ + 'SecondProxy', + 'FirstProxy', + ], + AurynConfig::SHARING => [ + 'SecondShared', + ], + AurynConfig::ALIASES => [ + 'InterfaceName' => 'SecondClass', + ], + AurynConfig::DEFINITIONS => [ + 'ServiceName' => [ + ':overridden' => 'second', + ], + ], + ], + ], null, $config); + + $sut->aggregate(); + + $this->assertSame([ + 'FirstProxy', + 'SecondProxy', + ], $config->get(AurynConfig::PROXY)); + $this->assertSame([ + 'FirstShared', + 'SecondShared', + ], $config->get(AurynConfig::SHARING)); + $this->assertSame([ + 'InterfaceName' => 'SecondClass', + ], $config->get(AurynConfig::ALIASES)); + $this->assertSame( + [ + ':overridden' => 'second', + ':kept' => 'kept', + ], + $config->get(AurynConfig::DEFINITIONS . '.ServiceName') + ); + } + + public function testAggregateMergesMixedExternalSectionsByArrayShape(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + static fn(): array => [ + 'external_section' => [ + 'FirstService', + 'feature_flag' => 'ConditionalService', + 'nested' => [ + 'kept' => 'kept', + 'overridden' => 'first', + ], + ], + ], + static fn(): array => [ + 'external_section' => [ + 'FirstService', + 'SecondService', + 'feature_flag' => 'UpdatedConditionalService', + 'nested' => [ + 'overridden' => 'second', + 'added' => 'added', + ], + ], + ], + ], null, $config); + + $sut->aggregate(); + + $externalSection = $config->get('external_section'); + + $this->assertSame('FirstService', $externalSection[0]); + $this->assertSame('SecondService', $externalSection[1]); + $this->assertSame('UpdatedConditionalService', $externalSection['feature_flag']); + $this->assertSame([ + 'kept' => 'kept', + 'overridden' => 'second', + 'added' => 'added', + ], $externalSection['nested']); + } + + public function testAggregateAppendsProviderListsToPreloadedConfig(): void + { + $config = new Config([ + 'external_section' => [ + 'PreloadedService', + 'feature_flag' => 'PreloadedConditionalService', + ], + ]); + $sut = $this->makeInstance([ + static fn(): array => [ + 'external_section' => [ + 'ProviderService', + 'feature_flag' => 'ProviderConditionalService', + ], + ], + ], null, $config); + + $sut->aggregate(); + + $externalSection = $config->get('external_section'); + + $this->assertSame('PreloadedService', $externalSection[0]); + $this->assertSame('ProviderService', $externalSection[1]); + $this->assertSame('ProviderConditionalService', $externalSection['feature_flag']); + } + + public function testAggregateAcceptsTraversableProvidersYieldingConfigurationArrays(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + static function (): \Generator { + yield [ + AurynConfig::SHARING => [ + 'FirstShared', + ], + ]; + + yield [ + AurynConfig::SHARING => [ + 'SecondShared', + ], + ]; + }, + ], null, $config); + + $sut->aggregate(); + + $this->assertSame([ + 'FirstShared', + 'SecondShared', + ], $config->get(AurynConfig::SHARING)); + } + + public function testAggregateAcceptsInvokableObjectProviders(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + new class { + public function __invoke(): array + { + return [ + 'custom' => [ + 'key' => 'value', + ], + ]; + } + }, + ], null, $config); + + $sut->aggregate(); + + $this->assertSame('value', $config->get('custom.key')); + } + + public function testAggregateAcceptsClassStringAndArrayCallableProviders(): void + { + $config = new Config(); + $sut = $this->makeInstance([ + ModuleStub1::class, + [ModuleStub1::class, '__invoke'], + ], null, $config); + + $sut->aggregate(); + + $this->assertSame( + SharedClass::class, + $config->get([AurynConfig::ALIASES, SharedAliasedInterface::class]) + ); + } + + public function testAggregateDoesNotExecuteProvidersWhenCacheReadSucceeds(): void + { + $cache = new class implements ProvidersCacheInterface { + public bool $written = false; + + public function read(ConfigInterface $config): bool + { + $config->merge([ + 'from_cache' => true, + ]); + + return true; + } + + public function write(ConfigInterface $config): void + { + $this->written = true; + } + }; + + $config = new Config(); + $sut = $this->makeInstance([ + static function (): array { + throw new \RuntimeException('Provider should not be executed'); + }, + ], $cache, $config); + + $sut->aggregate(); + + $this->assertTrue($config->get('from_cache')); + $this->assertFalse($cache->written); + } + + public function testAggregateWritesCacheWhenCacheIsProvided(): void + { + $cache = new class implements ProvidersCacheInterface { + /** + * @var array + */ + public array $writtenConfig = []; + + public function read(ConfigInterface $config): bool + { + return false; + } + + public function write(ConfigInterface $config): void + { + $this->writtenConfig = $config->toArray(); + } + }; + + $sut = $this->makeInstance([ + static fn(): array => [ + 'key' => 'value', + ], + ], $cache); + + $sut->aggregate(); + + $this->assertSame('value', $cache->writtenConfig['key']); + } + + public function testAggregateDoesNotWriteCacheWhenCacheIsDisabled(): void + { + $file = \codecept_output_dir('disabled-providers-cache.php'); + if (\is_file($file)) { + \unlink($file); + } + + $sut = $this->makeInstance([ + static fn(): array => [ + 'key' => 'value', + ], + ], new ProvidersCache($file)); + + $sut->aggregate(); + + $this->assertFalse(\is_file($file)); + } + + public function testAggregateThrowsWhenProviderReturnsInvalidResult(): void + { + $sut = $this->makeInstance([ + static fn(): string => 'invalid', + ]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('must return an array or iterable of arrays'); + + $sut->aggregate(); + } + + public function testAggregateThrowsWhenProviderYieldsInvalidResult(): void + { + $sut = $this->makeInstance([ + static function (): \Generator { + yield 'invalid'; + }, + ]); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('yielded string, expected array'); + + $sut->aggregate(); + } + + public function testAggregateWrapsProviderExecutionErrors(): void + { + $sut = $this->makeInstance([ + static function (): array { + throw new \RuntimeException('Provider failed'); + }, + ]); + + $this->expectException(\ErrorException::class); + $this->expectExceptionMessage('Provider failed'); + + $sut->aggregate(); + } +} diff --git a/tests/unit/ProxyInjectorTest.php b/tests/unit/ProxyInjectorTest.php deleted file mode 100644 index ebd5857..0000000 --- a/tests/unit/ProxyInjectorTest.php +++ /dev/null @@ -1,133 +0,0 @@ -expectException(\Auryn\ConfigException::class); - $injector->proxy('1', 'string'); - } - - public function testInstanceProxy() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\TestDependency', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $class = $injector->make('Auryn\Test\TestDependency'); - - $this->assertInstanceOf('Auryn\Test\TestDependency', $class, ''); - $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $class, ''); - $this->assertEquals('testVal', $class->testProp, ''); - } - - public function testMakeInstanceInjectsSimpleConcreteDependencyProxy() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\TestDependency', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $need_dep = $injector->make('Auryn\Test\TestNeedsDep'); - - $this->assertInstanceOf('Auryn\Test\TestNeedsDep', $need_dep, ''); - } - - public function testShareInstanceProxy() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\TestDependency', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $injector->share('Auryn\Test\TestDependency'); - $class = $injector->make('Auryn\Test\TestDependency'); - $class2 = $injector->make('Auryn\Test\TestDependency'); - - $this->assertEquals($class, $class2, ''); - } - - public function testProxyMakeInstanceReturnsAliasInstanceOnNonConcreteTypehint() - { - $injector = new Injector(); - $injector->alias('Auryn\Test\DepInterface', 'Auryn\Test\DepImplementation'); - $injector->proxy( - 'Auryn\Test\DepInterface', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $object = $injector->make('Auryn\Test\DepInterface'); - - $this->assertInstanceOf('Auryn\Test\DepInterface', $object, ''); - $this->assertInstanceOf('Auryn\Test\DepImplementation', $object, ''); - $this->assertInstanceOf('ProxyManager\Proxy\LazyLoadingInterface', $object, ''); - } - - public function testProxyPrepare() - { - $injector = new Injector(); - $injector->proxy( - 'Auryn\Test\PreparesImplementationTest', - static function (string $className, callable $callback) { - return (new LazyLoadingValueHolderFactory())->createProxy( - $className, - static function (&$object, $proxy, $method, $parameters, &$initializer) use ($callback) { - $object = $callback(); - $initializer = null; - } - ); - } - ); - $injector->prepare( - 'Auryn\Test\PreparesImplementationTest', - function (PreparesImplementationTest $obj, $injector) { - $obj->testProp = 42; - } - ); - $obj = $injector->make('Auryn\Test\PreparesImplementationTest'); - - $this->assertSame(42, $obj->testProp); - } -}