diff --git a/composer.json b/composer.json index b92ebaa90..7adce2019 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,8 @@ { - "name": "php-di/php-di", + "name": "ovos/php-di", + "replace": { + "php-di/php-di": "^6.0.8" + }, "type": "library", "description": "The dependency injection container for humans", "keywords": ["di", "dependency injection", "container", "ioc", "psr-11", "psr11", "container-interop"], @@ -45,5 +48,10 @@ "suggest": { "doctrine/annotations": "Install it if you want to use annotations (version ~1.2)", "ocramius/proxy-manager": "Install it if you want to use lazy injection (version ~2.0)" + }, + "extra": { + "branch-alias": { + "dev-mod": "6.x-dev" + } } } diff --git a/phpstan.neon b/phpstan.neon index ec549c629..23d107525 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,5 +10,6 @@ parameters: ignoreErrors: - '#Access to undefined constant DI\\CompiledContainer::METHOD_MAPPING.#' - '#Function apcu_.* not found.#' + - '#PHPDoc tag @throws with type Psr\\Container\\ContainerExceptionInterface is not subtype of Throwable#' reportUnmatchedIgnoredErrors: false inferPrivatePropertyTypeFromConstructor: true diff --git a/src/CompiledContainer.php b/src/CompiledContainer.php index 88438d736..15b7e6635 100644 --- a/src/CompiledContainer.php +++ b/src/CompiledContainer.php @@ -16,6 +16,7 @@ use Invoker\ParameterResolver\DefaultValueResolver; use Invoker\ParameterResolver\NumericArrayResolver; use Invoker\ParameterResolver\ResolverChain; +use Psr\Container\NotFoundExceptionInterface; /** * Compiled version of the dependency injection container. @@ -120,4 +121,42 @@ protected function resolveFactory($callable, $entryName, array $extraParameters throw new InvalidDefinition("Entry \"$entryName\" cannot be resolved: " . $e->getMessage()); } } + + /** + * Resolve a placeholder in string definition + * - wrap possible NotFound exception to conform to the one from StringDefinition::resolveExpression. + */ + protected function resolveStringPlaceholder($placeholder, $entryName) + { + try { + return $this->delegateContainer->get($placeholder); + } catch (NotFoundExceptionInterface $e) { + throw new DependencyException(sprintf( + "Error while parsing string expression for entry '%s': %s", + $entryName, + $e->getMessage() + ), 0, $e); + } + } + + /** + * Resolve ServiceLocator for given subscriber class (based on \DI\Definition\ServiceLocatorDefinition::resolve). + * + * @param string $requestingName class name of a subscriber, implementing ServiceSubscriberInterface + * @param string $repositoryClass ServiceLocatorRepository + * @return ServiceLocator + * @throws ServiceSubscriberException + */ + protected function resolveServiceLocator($requestingName, $repositoryClass) + { + if (!method_exists($requestingName, 'getSubscribedServices')) { + throw new ServiceSubscriberException(sprintf('The class %s does not implement ServiceSubscriberInterface.', $requestingName)); + } + + /** @var ServiceLocatorRepository $repository */ + $repository = $this->delegateContainer->get($repositoryClass); + $services = $requestingName::getSubscribedServices(); + + return $repository->create($requestingName, $services); + } } diff --git a/src/Compiler/Compiler.php b/src/Compiler/Compiler.php index f85b1fb8a..7e6b05240 100644 --- a/src/Compiler/Compiler.php +++ b/src/Compiler/Compiler.php @@ -5,6 +5,7 @@ namespace DI\Compiler; use function chmod; +use DI\Container; use DI\Definition\ArrayDefinition; use DI\Definition\DecoratorDefinition; use DI\Definition\Definition; @@ -22,6 +23,8 @@ use function file_put_contents; use InvalidArgumentException; use Opis\Closure\SerializableClosure; +use ReflectionFunction; +use ReflectionFunctionAbstract; use function rename; use function sprintf; use function tempnam; @@ -227,6 +230,13 @@ private function compileDefinition(string $entryName, Definition $definition) : $code = 'return ' . $this->compileValue($value) . ';'; break; case $definition instanceof Reference: + if ($definition->isServiceLocatorEntry()) { + $requestingEntry = $definition->getRequestingName(); + $serviceLocatorDefinition = $definition->getServiceLocatorDefinition(); + $code = 'return $this->resolveServiceLocator(' . $this->compileValue($requestingEntry) . ', ' . $this->compileValue($serviceLocatorDefinition::$serviceLocatorRepositoryClass) . ');'; + break; + } + $targetEntryName = $definition->getTargetEntryName(); $code = 'return $this->delegateContainer->get(' . $this->compileValue($targetEntryName) . ');'; // If this method is not yet compiled we store it for compilation @@ -235,9 +245,12 @@ private function compileDefinition(string $entryName, Definition $definition) : } break; case $definition instanceof StringDefinition: - $entryName = $this->compileValue($definition->getName()); - $expression = $this->compileValue($definition->getExpression()); - $code = 'return \DI\Definition\StringDefinition::resolveExpression(' . $entryName . ', ' . $expression . ', $this->delegateContainer);'; + $expression = $definition->getExpression(); + $callback = function (array $matches) use ($definition) { + return '\'.$this->resolveStringPlaceholder(' . $this->compileValue($matches[1]) . ', ' . $this->compileValue($definition->getName()) . ').\''; + }; + $value = preg_replace_callback('#\{([^\{\}]+)\}#', $callback, $expression); + $code = 'return \'' . $value . '\';'; break; case $definition instanceof EnvironmentVariableDefinition: $variableName = $this->compileValue($definition->getVariableName()); @@ -297,6 +310,35 @@ private function compileDefinition(string $entryName, Definition $definition) : )); } + if ($value instanceof \Closure) { + $reflection = new ReflectionFunction($value); + $requestedEntry = new RequestedEntryHolder($entryName); + $parametersByClassName = [ + 'DI\Factory\RequestedEntry' => $requestedEntry, + ]; + // default non-typehinted parameters + $defaultParameters = [new Reference(Container::class), $requestedEntry]; + + $resolvedParameters = $this->resolveFactoryParameters( + $reflection, + $definition->getParameters(), + $parametersByClassName, + $defaultParameters + ); + + $definitionParameters = array_map(function ($value) { + return $this->compileValue($value); + }, $resolvedParameters); + + $code = sprintf( + 'return (%s)(%s);', + $this->compileValue($value), + implode(', ', $definitionParameters) + ); + break; + } + + // todo optimize other (non-closure) factories $definitionParameters = ''; if (!empty($definition->getParameters())) { $definitionParameters = ', ' . $this->compileValue($definition->getParameters()); @@ -328,6 +370,11 @@ public function compileValue($value) : string throw new InvalidDefinition($errorMessage); } + // one step ahead to skip CompiledContainer->resolveFactory + if ($value instanceof RequestedEntryHolder) { + return 'new DI\Compiler\RequestedEntryHolder(\'' . $value->getName() . '\')'; + } + if ($value instanceof Definition) { // Give it an arbitrary unique name $subEntryName = 'subEntry' . (++$this->subEntryCounter); @@ -386,6 +433,10 @@ private function isCompilable($value) if ($value instanceof \Closure) { return true; } + // added for skipping CompiledContainer->resolveFactory - there is a special case for this in compileValue method + if ($value instanceof RequestedEntryHolder) { + return true; + } if (is_object($value)) { return 'An object was found but objects cannot be compiled'; } @@ -420,4 +471,39 @@ private function compileClosure(\Closure $closure) : string return $code; } + + public function resolveFactoryParameters( + ReflectionFunctionAbstract $reflection, + array $definitionParameters = [], + array $parametersByClassName = [], + array $defaultParameters = [] + ) { + $resolvedParameters = []; + $parameters = $reflection->getParameters(); + + foreach ($parameters as $index => $parameter) { + $name = $parameter->getName(); + if (array_key_exists($name, $definitionParameters)) { + $resolvedParameters[$index] = $definitionParameters[$name]; + continue; + } + + $parameterClass = $parameter->getClass(); + if (!$parameterClass) { + if (array_key_exists($index, $defaultParameters)) { + // take default parameters, when no typehint + $resolvedParameters[$index] = $defaultParameters[$index]; + } + continue; + } + + if (isset($parametersByClassName[$parameterClass->name])) { + $resolvedParameters[$index] = $parametersByClassName[$parameterClass->name]; + } else { + $resolvedParameters[$index] = new Reference($parameterClass->name); + } + } + + return $resolvedParameters; + } } diff --git a/src/ContainerBuilder.php b/src/ContainerBuilder.php index 3ee747f43..95e4e6fd8 100644 --- a/src/ContainerBuilder.php +++ b/src/ContainerBuilder.php @@ -8,6 +8,7 @@ use DI\Definition\Source\AnnotationBasedAutowiring; use DI\Definition\Source\DefinitionArray; use DI\Definition\Source\DefinitionFile; +use DI\Definition\Source\DefinitionGlob; use DI\Definition\Source\DefinitionSource; use DI\Definition\Source\NoAutowiring; use DI\Definition\Source\ReflectionBasedAutowiring; @@ -56,6 +57,11 @@ class ContainerBuilder */ private $useAnnotations = false; + /** + * @var int + */ + private $annotationsFlags = 0; + /** * @var bool */ @@ -131,7 +137,7 @@ public function build() $sources = array_reverse($this->definitionSources); if ($this->useAnnotations) { - $autowiring = new AnnotationBasedAutowiring($this->ignorePhpDocErrors); + $autowiring = new AnnotationBasedAutowiring($this->ignorePhpDocErrors, $this->annotationsFlags); $sources[] = $autowiring; } elseif ($this->useAutowiring) { $autowiring = new ReflectionBasedAutowiring; @@ -146,6 +152,8 @@ public function build() return new DefinitionFile($definitions, $autowiring); } elseif (is_array($definitions)) { return new DefinitionArray($definitions, $autowiring); + } elseif ($definitions instanceof DefinitionGlob) { + $definitions->setAutowiring($autowiring); } return $definitions; @@ -155,10 +163,8 @@ public function build() // Mutable definition source $source->setMutableDefinitionSource(new DefinitionArray([], $autowiring)); - if ($this->sourceCache) { - if (!SourceCache::isSupported()) { - throw new \Exception('APCu is not enabled, PHP-DI cannot use it as a cache'); - } + // use cache if isSupported check passes, otherwise proceed without cache and do not throw an exception + if ($this->sourceCache && SourceCache::isSupported()) { // Wrap the source with the cache decorator $source = new SourceCache($source, $this->sourceCacheNamespace); } @@ -244,11 +250,12 @@ public function useAutowiring(bool $bool) : self * * @return $this */ - public function useAnnotations(bool $bool) : self + public function useAnnotations(bool $bool, int $flags = 0) : self { $this->ensureNotLocked(); $this->useAnnotations = $bool; + $this->annotationsFlags = $flags; return $this; } diff --git a/src/Definition/ArrayDefinition.php b/src/Definition/ArrayDefinition.php index 8b06fed6d..ef77100ca 100644 --- a/src/Definition/ArrayDefinition.php +++ b/src/Definition/ArrayDefinition.php @@ -50,7 +50,7 @@ public function replaceNestedDefinitions(callable $replacer) public function __toString() { - $str = '[' . PHP_EOL; + $str = '[' . \PHP_EOL; foreach ($this->values as $key => $value) { if (is_string($key)) { @@ -60,12 +60,12 @@ public function __toString() $str .= ' ' . $key . ' => '; if ($value instanceof Definition) { - $str .= str_replace(PHP_EOL, PHP_EOL . ' ', (string) $value); + $str .= str_replace(\PHP_EOL, \PHP_EOL . ' ', (string) $value); } else { $str .= var_export($value, true); } - $str .= ',' . PHP_EOL; + $str .= ',' . \PHP_EOL; } return $str . ']'; diff --git a/src/Definition/AutowireDefinition.php b/src/Definition/AutowireDefinition.php index 25dc3ecbc..4d7728612 100644 --- a/src/Definition/AutowireDefinition.php +++ b/src/Definition/AutowireDefinition.php @@ -9,4 +9,25 @@ */ class AutowireDefinition extends ObjectDefinition { + /** + * @var bool|null + */ + protected $useAnnotations; + + /** + * Enable/disable reading annotations for this definition, regardless of a container configuration. + */ + public function useAnnotations(bool $flag = true) + { + $this->useAnnotations = $flag; + } + + /** + * Returns boolean if the useAnnotation flag was explicitly set, otherwise null. + * @return bool|null + */ + public function isUsingAnnotations() + { + return $this->useAnnotations; + } } diff --git a/src/Definition/Dumper/ObjectDefinitionDumper.php b/src/Definition/Dumper/ObjectDefinitionDumper.php index 7e1999961..b99695ff2 100644 --- a/src/Definition/Dumper/ObjectDefinitionDumper.php +++ b/src/Definition/Dumper/ObjectDefinitionDumper.php @@ -35,7 +35,7 @@ public function dump(ObjectDefinition $definition) : string $str = sprintf(' class = %s%s', $warning, $className); // Lazy - $str .= PHP_EOL . ' lazy = ' . var_export($definition->isLazy(), true); + $str .= \PHP_EOL . ' lazy = ' . var_export($definition->isLazy(), true); if ($classExist) { // Constructor @@ -48,7 +48,7 @@ public function dump(ObjectDefinition $definition) : string $str .= $this->dumpMethods($className, $definition); } - return sprintf('Object (' . PHP_EOL . '%s' . PHP_EOL . ')', $str); + return sprintf('Object (' . \PHP_EOL . '%s' . \PHP_EOL . ')', $str); } private function dumpConstructor(string $className, ObjectDefinition $definition) : string @@ -60,7 +60,7 @@ private function dumpConstructor(string $className, ObjectDefinition $definition if ($constructorInjection !== null) { $parameters = $this->dumpMethodParameters($className, $constructorInjection); - $str .= sprintf(PHP_EOL . ' __construct(' . PHP_EOL . ' %s' . PHP_EOL . ' )', $parameters); + $str .= sprintf(\PHP_EOL . ' __construct(' . \PHP_EOL . ' %s' . \PHP_EOL . ' )', $parameters); } return $str; @@ -74,7 +74,7 @@ private function dumpProperties(ObjectDefinition $definition) : string $value = $propertyInjection->getValue(); $valueStr = $value instanceof Definition ? (string) $value : var_export($value, true); - $str .= sprintf(PHP_EOL . ' $%s = %s', $propertyInjection->getPropertyName(), $valueStr); + $str .= sprintf(\PHP_EOL . ' $%s = %s', $propertyInjection->getPropertyName(), $valueStr); } return $str; @@ -87,7 +87,7 @@ private function dumpMethods(string $className, ObjectDefinition $definition) : foreach ($definition->getMethodInjections() as $methodInjection) { $parameters = $this->dumpMethodParameters($className, $methodInjection); - $str .= sprintf(PHP_EOL . ' %s(' . PHP_EOL . ' %s' . PHP_EOL . ' )', $methodInjection->getMethodName(), $parameters); + $str .= sprintf(\PHP_EOL . ' %s(' . \PHP_EOL . ' %s' . \PHP_EOL . ' )', $methodInjection->getMethodName(), $parameters); } return $str; @@ -130,6 +130,6 @@ private function dumpMethodParameters(string $className, MethodInjection $method $args[] = sprintf('$%s = #UNDEFINED#', $parameter->getName()); } - return implode(PHP_EOL . ' ', $args); + return implode(\PHP_EOL . ' ', $args); } } diff --git a/src/Definition/EnvironmentVariableDefinition.php b/src/Definition/EnvironmentVariableDefinition.php index 73e0e2979..b2a80c3e4 100644 --- a/src/Definition/EnvironmentVariableDefinition.php +++ b/src/Definition/EnvironmentVariableDefinition.php @@ -93,20 +93,20 @@ public function replaceNestedDefinitions(callable $replacer) public function __toString() { - $str = ' variable = ' . $this->variableName . PHP_EOL + $str = ' variable = ' . $this->variableName . \PHP_EOL . ' optional = ' . ($this->isOptional ? 'yes' : 'no'); if ($this->isOptional) { if ($this->defaultValue instanceof Definition) { $nestedDefinition = (string) $this->defaultValue; - $defaultValueStr = str_replace(PHP_EOL, PHP_EOL . ' ', $nestedDefinition); + $defaultValueStr = str_replace(\PHP_EOL, \PHP_EOL . ' ', $nestedDefinition); } else { $defaultValueStr = var_export($this->defaultValue, true); } - $str .= PHP_EOL . ' default = ' . $defaultValueStr; + $str .= \PHP_EOL . ' default = ' . $defaultValueStr; } - return sprintf('Environment variable (' . PHP_EOL . '%s' . PHP_EOL . ')', $str); + return sprintf('Environment variable (' . \PHP_EOL . '%s' . \PHP_EOL . ')', $str); } } diff --git a/src/Definition/Exception/InvalidDefinition.php b/src/Definition/Exception/InvalidDefinition.php index 54e602b74..682f1e78c 100644 --- a/src/Definition/Exception/InvalidDefinition.php +++ b/src/Definition/Exception/InvalidDefinition.php @@ -16,7 +16,7 @@ class InvalidDefinition extends \Exception public static function create(Definition $definition, string $message, \Exception $previous = null) : self { return new self(sprintf( - '%s' . PHP_EOL . 'Full definition:' . PHP_EOL . '%s', + '%s' . \PHP_EOL . 'Full definition:' . \PHP_EOL . '%s', $message, (string) $definition ), 0, $previous); diff --git a/src/Definition/Helper/AutowireDefinitionHelper.php b/src/Definition/Helper/AutowireDefinitionHelper.php index 8dfa4e2d5..9b9a278ee 100644 --- a/src/Definition/Helper/AutowireDefinitionHelper.php +++ b/src/Definition/Helper/AutowireDefinitionHelper.php @@ -5,6 +5,7 @@ namespace DI\Definition\Helper; use DI\Definition\AutowireDefinition; +use DI\Definition\Definition; /** * Helps defining how to create an instance of a class using autowiring. @@ -15,6 +16,8 @@ class AutowireDefinitionHelper extends CreateDefinitionHelper { const DEFINITION_CLASS = AutowireDefinition::class; + protected $useAnnotations; + /** * Defines a value for a specific argument of the constructor. * @@ -69,4 +72,30 @@ public function methodParameter(string $method, $parameter, $value) return $this; } + + /** + * Define if entry should use annotation reader for reading dependencies. + * This is turned off by default if autowire() helper is used, and turned on if entry is not defined explicitly in the di config. + * @return $this + */ + public function useAnnotations(bool $useAnnotations = true) + { + $this->useAnnotations = $useAnnotations; + + return $this; + } + + /** + * @return AutowireDefinition + */ + public function getDefinition(string $entryName) : Definition + { + /** @var AutowireDefinition $definition */ + $definition = parent::getDefinition($entryName); + if ($this->useAnnotations !== null) { + $definition->useAnnotations($this->useAnnotations); + } + + return $definition; + } } diff --git a/src/Definition/Reference.php b/src/Definition/Reference.php index 5b4597a82..13bc62583 100644 --- a/src/Definition/Reference.php +++ b/src/Definition/Reference.php @@ -4,6 +4,8 @@ namespace DI\Definition; +use DI\Definition\Exception\InvalidDefinition; +use DI\ServiceLocator; use Psr\Container\ContainerInterface; /** @@ -13,6 +15,8 @@ */ class Reference implements Definition, SelfResolvingDefinition { + public static $serviceLocatorClass = ServiceLocator::class; + /** * Entry name. * @var string @@ -25,12 +29,30 @@ class Reference implements Definition, SelfResolvingDefinition */ private $targetEntryName; + /** + * @var string + */ + private $requestingName; + + /** + * @var bool + */ + private $isServiceLocatorEntry; + + /** + * @var ServiceLocatorDefinition|null + */ + private $serviceLocatorDefinition; + /** * @param string $targetEntryName Name of the target entry + * @param string $requestingName name of an entry - holder of a definition requesting this entry */ - public function __construct(string $targetEntryName) + public function __construct(string $targetEntryName, $requestingName = null) { $this->targetEntryName = $targetEntryName; + $this->requestingName = $requestingName; + $this->isServiceLocatorEntry = $targetEntryName === self::$serviceLocatorClass; } public function getName() : string @@ -48,13 +70,50 @@ public function getTargetEntryName() : string return $this->targetEntryName; } + /** + * Returns the name of the entity requesting this entry. + */ + public function getRequestingName() : string + { + return $this->requestingName; + } + + public function isServiceLocatorEntry() : bool + { + return $this->isServiceLocatorEntry; + } + + public function getServiceLocatorDefinition() : ServiceLocatorDefinition + { + if (!$this->isServiceLocatorEntry || $this->requestingName === null) { + throw new InvalidDefinition(sprintf( + "Invalid service locator definition ('%s' for '%s')", + $this->targetEntryName, + $this->requestingName + )); + } + if (!$this->serviceLocatorDefinition) { + $this->serviceLocatorDefinition = new ServiceLocatorDefinition($this->getTargetEntryName(), $this->requestingName); + } + + return $this->serviceLocatorDefinition; + } + public function resolve(ContainerInterface $container) { + if ($this->isServiceLocatorEntry) { + return $this->getServiceLocatorDefinition()->resolve($container); + } + return $container->get($this->getTargetEntryName()); } public function isResolvable(ContainerInterface $container) : bool { + if ($this->isServiceLocatorEntry) { + return $this->getServiceLocatorDefinition()->isResolvable($container); + } + return $container->has($this->getTargetEntryName()); } diff --git a/src/Definition/ServiceLocatorDefinition.php b/src/Definition/ServiceLocatorDefinition.php new file mode 100644 index 000000000..ebaf2c453 --- /dev/null +++ b/src/Definition/ServiceLocatorDefinition.php @@ -0,0 +1,100 @@ +name = $name; + $this->requestingName = $requestingName; + } + + /** + * Returns the name of the entry in the container. + */ + public function getName() : string + { + return $this->name; + } + + public function setName(string $name) + { + $this->name = $name; + } + + /** + * Returns the name of the holder of the definition requesting service locator. + */ + public function getRequestingName() : string + { + return $this->requestingName; + } + + /** + * Resolve the definition and return the resulting value. + * + * @return ServiceLocator + * @throws ServiceSubscriberException + */ + public function resolve(ContainerInterface $container) + { + if (!method_exists($this->requestingName, 'getSubscribedServices')) { + throw new ServiceSubscriberException(sprintf('The class %s does not implement ServiceSubscriberInterface.', $this->requestingName)); + } + + /** @var ServiceLocatorRepository $repository */ + $repository = $container->get(self::$serviceLocatorRepositoryClass); + $services = $this->requestingName::getSubscribedServices(); + + return $repository->create($this->requestingName, $services); + } + + /** + * Check if a definition can be resolved. + */ + public function isResolvable(ContainerInterface $container) : bool + { + return method_exists($this->requestingName, 'getSubscribedServices'); + } + + public function replaceNestedDefinitions(callable $replacer) + { + // no nested definitions + } + + /** + * Definitions can be cast to string for debugging information. + */ + public function __toString() + { + return sprintf( + 'get(%s) for \'%s\'', + $this->name, + $this->requestingName + ); + } +} diff --git a/src/Definition/Source/AnnotationBasedAutowiring.php b/src/Definition/Source/AnnotationBasedAutowiring.php index 508e00745..8f28efe0c 100644 --- a/src/Definition/Source/AnnotationBasedAutowiring.php +++ b/src/Definition/Source/AnnotationBasedAutowiring.php @@ -6,6 +6,7 @@ use DI\Annotation\Inject; use DI\Annotation\Injectable; +use DI\Definition\AutowireDefinition; use DI\Definition\Exception\InvalidAnnotation; use DI\Definition\ObjectDefinition; use DI\Definition\ObjectDefinition\MethodInjection; @@ -33,6 +34,25 @@ */ class AnnotationBasedAutowiring implements DefinitionSource, Autowiring { + // Annotations configuration flags: + // enable on implicit definitions + const IMPLICIT = 1; + // enable on all autowire definitions (which are written in DI config) by default + const EXPLICIT = 2; + // read @Injectable annotations for classes + const INJECTABLE = 4; + // read @Inject annotations for properties + const PROPERTIES = 8; + // read @Inject annotations for methods' parameters + const METHODS = 16; + // all options enabled + const ALL = 31; + + /** + * @var int + */ + private $flags; + /** * @var Reader */ @@ -48,9 +68,10 @@ class AnnotationBasedAutowiring implements DefinitionSource, Autowiring */ private $ignorePhpDocErrors; - public function __construct($ignorePhpDocErrors = false) + public function __construct($ignorePhpDocErrors = false, int $flags = 0) { $this->ignorePhpDocErrors = (bool) $ignorePhpDocErrors; + $this->flags = $flags > 0 ? $flags : self::ALL; // all flags turned on by default } public function autowire(string $name, ObjectDefinition $definition = null) @@ -62,16 +83,35 @@ public function autowire(string $name, ObjectDefinition $definition = null) } $definition = $definition ?: new ObjectDefinition($name); + $useAnnotations = $definition instanceof AutowireDefinition + ? ($definition->isUsingAnnotations() ?? ($this->flags & self::EXPLICIT)) + : ($this->flags & self::IMPLICIT); + + $class = null; + if ($useAnnotations && $this->flags >= self::INJECTABLE) { + $class = new ReflectionClass($className); - $class = new ReflectionClass($className); + if ($this->flags & self::INJECTABLE) { + $this->readInjectableAnnotation($class, $definition); + } - $this->readInjectableAnnotation($class, $definition); + // Browse the class properties looking for annotated properties + if ($this->flags & self::PROPERTIES) { + $this->readProperties($class, $definition); + } - // Browse the class properties looking for annotated properties - $this->readProperties($class, $definition); + // Browse the object's methods looking for annotated methods + if ($this->flags & self::METHODS) { + $this->readMethods($class, $definition); + } + } - // Browse the object's methods looking for annotated methods - $this->readMethods($class, $definition); + // constructor parameters should always be read, even if annotations are disabled (completely or i.a. for methods) + // so that it behaves at least as ReflectionBasedAutowiring + if (!$useAnnotations || !($this->flags & self::METHODS)) { + $class = $class ?? new ReflectionClass($className); + $this->readConstructor($class, $definition); + } return $definition; } @@ -147,7 +187,7 @@ private function readProperty(ReflectionProperty $property, ObjectDefinition $de } $definition->addPropertyInjection( - new PropertyInjection($property->getName(), new Reference($entryName), $classname) + new PropertyInjection($property->getName(), new Reference($entryName, $classname), $classname) ); } @@ -176,6 +216,28 @@ private function readMethods(ReflectionClass $class, ObjectDefinition $objectDef } } + /** + * Browse the object's constructor parameters and inject dependencies. + */ + private function readConstructor(ReflectionClass $class, ObjectDefinition $definition) + { + if (!($constructor = $class->getConstructor()) || !$constructor->isPublic()) { + return; + } + + $parameters = []; + foreach ($constructor->getParameters() as $index => $parameter) { + $entryName = $this->getMethodParameter($index, $parameter, []); + + if ($entryName !== null) { + $parameters[$index] = new Reference($entryName, $class->getName()); + } + } + + $constructorInjection = MethodInjection::constructor($parameters); + $definition->completeConstructorInjection($constructorInjection); + } + /** * @return MethodInjection|null */ diff --git a/src/Definition/Source/DefinitionGlob.php b/src/Definition/Source/DefinitionGlob.php new file mode 100644 index 000000000..7c3f46698 --- /dev/null +++ b/src/Definition/Source/DefinitionGlob.php @@ -0,0 +1,77 @@ +pattern = $pattern; + } + + public function setAutowiring(Autowiring $autowiring) + { + $this->autowiring = $autowiring; + } + + public function getDefinition(string $name, int $startIndex = 0) + { + $this->initialize(); + + return $this->sourceChain->getDefinition($name, $startIndex); + } + + public function getDefinitions() : array + { + $this->initialize(); + + return $this->sourceChain->getDefinitions(); + } + + /** + * Lazy-loading of the definitions. + */ + private function initialize() + { + if ($this->initialized === true) { + return; + } + + $paths = glob($this->pattern, \GLOB_BRACE); + $sources = array_map(function ($path) { + return new DefinitionFile($path, $this->autowiring); + }, $paths); + $this->sourceChain = new SourceChain($sources); + + $this->initialized = true; + } +} diff --git a/src/Definition/Source/ReflectionBasedAutowiring.php b/src/Definition/Source/ReflectionBasedAutowiring.php index 0ad4cf71f..460db0445 100644 --- a/src/Definition/Source/ReflectionBasedAutowiring.php +++ b/src/Definition/Source/ReflectionBasedAutowiring.php @@ -30,7 +30,7 @@ public function autowire(string $name, ObjectDefinition $definition = null) $class = new \ReflectionClass($className); $constructor = $class->getConstructor(); if ($constructor && $constructor->isPublic()) { - $constructorInjection = MethodInjection::constructor($this->getParametersDefinition($constructor)); + $constructorInjection = MethodInjection::constructor($this->getParametersDefinition($constructor, $class->getName())); $definition->completeConstructorInjection($constructorInjection); } @@ -53,7 +53,7 @@ public function getDefinitions() : array /** * Read the type-hinting from the parameters of the function. */ - private function getParametersDefinition(\ReflectionFunctionAbstract $constructor) : array + private function getParametersDefinition(\ReflectionFunctionAbstract $constructor, string $className) : array { $parameters = []; @@ -77,7 +77,7 @@ private function getParametersDefinition(\ReflectionFunctionAbstract $constructo continue; } - $parameters[$index] = new Reference($parameterType->getName()); + $parameters[$index] = new Reference($parameterType->getName(), $className); } return $parameters; diff --git a/src/ServiceLocator.php b/src/ServiceLocator.php new file mode 100644 index 000000000..727f4352e --- /dev/null +++ b/src/ServiceLocator.php @@ -0,0 +1,110 @@ +container = $container; + $this->subscriber = $subscriber; + $this->setServices($services); + } + + protected function setServices(array $services) + { + foreach ($services as $key => $value) { + if (is_numeric($key)) { + $key = $value; + } + $this->services[$key] = $value; + } + } + + /** + * Get defined services. + */ + public function getServices() : array + { + return $this->services; + } + + /** + * Get name of a class to which this service locator instance belongs to. + */ + public function getSubscriber() : string + { + return $this->subscriber; + } + + /** + * Finds a service by its identifier. + * + * @param string $id Identifier of the entry to look for. + * + * @throws NotFoundExceptionInterface No entry was found for **this** identifier. + * @throws ContainerExceptionInterface Error while retrieving the entry. + * + * @return mixed Entry. + */ + public function get($id) + { + if (!isset($this->services[$id])) { + throw new NotFoundException("Service '$id' is not defined."); + } + + return $this->container->get($this->services[$id]); + } + + /** + * Returns true if the container can return an entry for the given identifier. + * Returns false otherwise. + * + * `has($id)` returning true does not mean that `get($id)` will not throw an exception. + * It does however mean that `get($id)` will not throw a `NotFoundExceptionInterface`. + * + * @param string $id Identifier of the entry to look for. + * + * @return bool + */ + public function has($id) + { + if (!isset($this->services[$id])) { + return false; + } + + return $this->container->has($this->services[$id]); + } +} diff --git a/src/ServiceLocatorRepository.php b/src/ServiceLocatorRepository.php new file mode 100644 index 000000000..6c04d6a17 --- /dev/null +++ b/src/ServiceLocatorRepository.php @@ -0,0 +1,109 @@ +container = $container; + } + + /** + * Create or modify service locator. + */ + public function create(string $entry, array $services = []) : ServiceLocator + { + if (isset($this->overrides[$entry])) { + $services = array_merge($services, $this->overrides[$entry]); + } + if (!isset($this->locators[$entry])) { + $this->locators[$entry] = new ServiceLocator($this->container, $services, $entry); + } else { + // the service locator cannot be re-created - the existing locator may be returned only if expected services are identical + // compare passed services and those in the already created ServiceLocator + $locatorServices = $this->locators[$entry]->getServices(); + foreach ($services as $key => $value) { + if (is_numeric($key)) { + $key = $value; + } + if (!array_key_exists($key, $locatorServices) || $locatorServices[$key] !== $value) { + throw new \LogicException(sprintf( + "ServiceLocator for '%s' cannot be recreated with different services.", + $entry + )); + } + } + } + + return $this->locators[$entry]; + } + + /** + * Override a single service for a service locator. + * This can be only used before the service locator for the given entry is created. + * + * @return $this + */ + public function override(string $entry, string $serviceId, string $serviceEntry = null) + { + if (isset($this->locators[$entry])) { + throw new \LogicException(sprintf( + "Service '%s' for '%s' cannot be overridden - ServiceLocator is already created.", + $serviceId, + $entry + )); + } + + $serviceEntry = $serviceEntry ?? $serviceId; + $this->overrides[$entry][$serviceId] = $serviceEntry; + + return $this; + } + + /** + * Get a service locator for an entry. + * @param string $entry + * @throws NotFoundException + */ + public function get($entry) : ServiceLocator + { + if (!isset($this->locators[$entry])) { + throw new NotFoundException("Service locator for entry '$entry' is not initialized."); + } + + return $this->locators[$entry]; + } + + /** + * @param string $entry + * @return bool + */ + public function has($entry) + { + return isset($this->locators[$entry]); + } +} diff --git a/src/ServiceSubscriberException.php b/src/ServiceSubscriberException.php new file mode 100644 index 000000000..d207d7c11 --- /dev/null +++ b/src/ServiceSubscriberException.php @@ -0,0 +1,11 @@ +>> Suggested as a lightweight alternative for heavyweight proxies from ocramius/proxy-manager. + * + * The getSubscribedServices method returns an array of service types required by such instances, + * optionally keyed by the service names used internally. + * + * The injected service locators SHOULD NOT allow access to any other services not specified by the method. + * + * It is expected that ServiceSubscriber instances consume PSR-11-based service locators internally. + * This interface does not dictate any injection method for these service locators, although constructor + * injection is recommended. + */ +interface ServiceSubscriberInterface +{ + /** + * Lazy instantiate heavy dependencies on-demand + * Returns an array of service types required by such instances, optionally keyed by the service names used internally. + * + * * ['logger' => Psr\Log\LoggerInterface::class] means the objects use the "logger" name + * internally to fetch a service which must implement Psr\Log\LoggerInterface. + * * ['Psr\Log\LoggerInterface'] is a shortcut for + * * ['Psr\Log\LoggerInterface' => 'Psr\Log\LoggerInterface'] + * + * @return array The required service types, optionally keyed by service names + */ + public static function getSubscribedServices() : array; +} diff --git a/tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php b/tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php new file mode 100644 index 000000000..b431c210f --- /dev/null +++ b/tests/IntegrationTest/Definitions/ServiceLocatorDefinitionTest.php @@ -0,0 +1,54 @@ +addDefinitions([ + ServiceLocatorDefinitionTest\TestClass::class => autowire() + ]); + $container = $builder->build(); + + self::assertEntryIsCompiled($container, ServiceLocatorDefinitionTest\TestClass::class); + + $instance = $container->get(ServiceLocatorDefinitionTest\TestClass::class); + $this->assertInstanceOf(ServiceLocator::class, $instance->serviceLocator); + $this->assertEquals(ServiceLocatorDefinitionTest\TestClass::class, $instance->serviceLocator->getSubscriber()); + $this->assertEquals(['foo' => 'foo'], $instance->serviceLocator->getServices()); + } +} + +namespace DI\Test\IntegrationTest\Definitions\ServiceLocatorDefinitionTest; + +use DI\ServiceLocator; +use DI\ServiceSubscriberInterface; + +class TestClass implements ServiceSubscriberInterface +{ + public $serviceLocator; + + public function __construct(ServiceLocator $serviceLocator) + { + $this->serviceLocator = $serviceLocator; + } + + public static function getSubscribedServices(): array + { + return ['foo']; + } +} \ No newline at end of file diff --git a/tests/IntegrationTest/ServiceLocatorTest.php b/tests/IntegrationTest/ServiceLocatorTest.php new file mode 100644 index 000000000..d4d271fb8 --- /dev/null +++ b/tests/IntegrationTest/ServiceLocatorTest.php @@ -0,0 +1,202 @@ +addDefinitions([ + 'foo' => 'value of foo', + 'baz' => 'baz', + ]); + + $container = $builder->build(); + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertEquals('value of foo', $instance->getFoo()); + $this->assertEquals('baz', $instance->getBar()); + $this->assertInstanceOf(ServiceLocatorTest\SomeService::class, $instance->getClass()); + } + + /** + * @dataProvider provideContainer + */ + public function testServiceLocatorThrowsForInvalidService(ContainerBuilder $builder) + { + $this->expectException(\DI\NotFoundException::class); + $this->expectExceptionMessage('Service \'baz\' is not defined.'); + + $builder->addDefinitions([ + 'baz' => 'baz', + ]); + + $container = $builder->build(); + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $instance->getInvalid(); + } + + /** + * @dataProvider provideContainer + */ + public function testServicesLazyResolve(ContainerBuilder $builder) + { + $container = $builder->build(); + + // services should not be resolved on instantiation of a subscriber class + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertNotContains(ServiceLocatorTest\SomeService::class, $container->getKnownEntryNames()); + + // resolve on demand + $instance->getClass(); + $this->assertContains(ServiceLocatorTest\SomeService::class, $container->getKnownEntryNames()); + } + + /** + * @dataProvider provideContainer + */ + public function testOverrideService(ContainerBuilder $builder) + { + $builder->addDefinitions([ + 'foo' => 'foo', + 'baz' => 'baz', + 'anotherFoo' => 'overridden foo', + ]); + $container = $builder->build(); + $repository = $container->get(ServiceLocatorRepository::class); + $repository->override(ServiceLocatorTest\ServiceSubscriber::class, 'foo', 'anotherFoo'); + + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertEquals('overridden foo', $instance->getFoo()); + } + + /** + * @dataProvider provideContainer + */ + public function testOverrideServiceInRepositoryDefinition(ContainerBuilder $builder) + { + $builder->addDefinitions([ + ServiceLocatorRepository::class => autowire() + ->method('override', ServiceLocatorTest\ServiceSubscriber::class, 'foo', 'anotherFoo'), + 'anotherFoo' => 'overridden foo', + ]); + + $container = $builder->build(); + + $instance = $container->get(ServiceLocatorTest\ServiceSubscriber::class); + $this->assertEquals('overridden foo', $instance->getFoo()); + } + + /** + * @dataProvider provideContainer + */ + public function testCannotOverrideServiceForAlreadyInstantiatedSubscriber(ContainerBuilder $builder) + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('Service \'foo\' for \'DI\Test\IntegrationTest\ServiceLocatorTest\ServiceSubscriber\' cannot be overridden - ServiceLocator is already created.'); + + $container = $builder->build(); + + $container->get(ServiceLocatorTest\ServiceSubscriber::class); + + $repository = $container->get(ServiceLocatorRepository::class); + $repository->override(ServiceLocatorTest\ServiceSubscriber::class, 'foo', 'anotherFoo'); + } + + /** + * @dataProvider provideContainer + */ + public function testMultipleSubscriberInstances(ContainerBuilder $builder) + { + $container = $builder->build(); + $instance1 = $container->make(ServiceLocatorTest\ServiceSubscriber::class); + $instance2 = $container->make(ServiceLocatorTest\ServiceSubscriber::class); + + // different instances + $this->assertNotSame($instance1, $instance2); + // but the same service locator instance + $this->assertSame($instance1->getServiceLocator(), $instance2->getServiceLocator()); + // and an instance of a service should be shared too + $this->assertSame($instance1->getClass(), $instance2->getClass()); + } + +} + +namespace DI\Test\IntegrationTest\ServiceLocatorTest; + +use DI\ServiceLocator; +use DI\ServiceSubscriberInterface; + +/** + * Fixture class for testing service locators + */ +class ServiceSubscriber implements ServiceSubscriberInterface +{ + /** + * @var ServiceLocator + */ + protected $serviceLocator; + + /** + * @param ServiceLocator $serviceLocator + */ + public function __construct(ServiceLocator $serviceLocator) + { + $this->serviceLocator = $serviceLocator; + } + + /** + * Lazy instantiate heavy dependencies on-demand + */ + public static function getSubscribedServices(): array + { + return [ + 'foo', + 'bar' => 'baz', + SomeService::class, + ]; + } + + public function getFoo() + { + return $this->serviceLocator->get('foo'); + } + + public function getBar() + { + return $this->serviceLocator->get('bar'); + } + + public function getClass() + { + return $this->serviceLocator->get(SomeService::class); + } + + /** + * @throws \DI\NotFoundException + */ + public function getInvalid() + { + return $this->serviceLocator->get('baz'); + } + + public function getServiceLocator() + { + return $this->serviceLocator; + } +} + +class SomeService +{ +} diff --git a/tests/UnitTest/Definition/ReferenceTest.php b/tests/UnitTest/Definition/ReferenceTest.php index 9b59d6960..72b3484f7 100644 --- a/tests/UnitTest/Definition/ReferenceTest.php +++ b/tests/UnitTest/Definition/ReferenceTest.php @@ -72,4 +72,55 @@ public function should_cast_to_string() { $this->assertEquals('get(bar)', (string) new Reference('bar')); } + + /** + * @test + */ + public function should_have_a_requesting_name() + { + $definition = new Reference('bar', 'foo'); + $this->assertEquals('foo', $definition->getRequestingName()); + } + + /** + * @test + */ + public function should_be_a_service_locator_entry() + { + $definition = new Reference(Reference::$serviceLocatorClass, 'foo'); + $this->assertTrue($definition->isServiceLocatorEntry()); + } + + /** + * @test + */ + public function should_not_be_a_service_locator_entry() + { + $definition = new Reference('bar', 'foo'); + $this->assertFalse($definition->isServiceLocatorEntry()); + } + + /** + * @test + */ + public function should_throw_on_invalid_service_locator_entry() + { + $this->expectException(\DI\Definition\Exception\InvalidDefinition::class); + $this->expectExceptionMessage('Invalid service locator definition (\'bar\' for \'foo\')'); + + $definition = new Reference('bar', 'foo'); + $definition->getServiceLocatorDefinition(); + } + + /** + * @test + */ + public function should_throw_on_invalid_service_locator_entry2() + { + $this->expectException(\DI\Definition\Exception\InvalidDefinition::class); + $this->expectExceptionMessage('Invalid service locator definition (\'DI\ServiceLocator\' for \'\')'); + + $definition = new Reference(Reference::$serviceLocatorClass); + $definition->getServiceLocatorDefinition(); + } } diff --git a/tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php b/tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php new file mode 100644 index 000000000..a984fd65e --- /dev/null +++ b/tests/UnitTest/Definition/ServiceLocatorDefinitionTest.php @@ -0,0 +1,57 @@ +assertEquals('ServiceLocator', $definition->getName()); + $definition->setName('foo'); + $this->assertEquals('foo', $definition->getName()); + + $this->assertEquals('subscriber', $definition->getRequestingName()); + } + + /** + * @test + */ + public function cannot_resolve_without_proper_subscriber() + { + $this->expectException(\DI\ServiceSubscriberException::class); + $this->expectExceptionMessage('The class DI\Test\UnitTest\Fixtures\Singleton does not implement ServiceSubscriberInterface.'); + + $container = $this->easyMock(ContainerInterface::class); + $definition = new ServiceLocatorDefinition(ServiceLocator::class, Singleton::class); + + $this->assertFalse($definition->isResolvable($container)); + $definition->resolve($container); + } + + /** + * @test + */ + public function should_cast_to_string() + { + $definition = new ServiceLocatorDefinition('bar', 'subscriber'); + $this->assertEquals("get(bar) for 'subscriber'", (string) $definition); + } +} diff --git a/tests/UnitTest/Definition/Source/DefinitionGlobTest.php b/tests/UnitTest/Definition/Source/DefinitionGlobTest.php new file mode 100644 index 000000000..1d1385e99 --- /dev/null +++ b/tests/UnitTest/Definition/Source/DefinitionGlobTest.php @@ -0,0 +1,53 @@ +getProperty('sourceChain'); + $property->setAccessible(true); + $sourceChain = $property->getValue($source); + // sources are not initialized (and files are not read) before getting definitions + $this->assertNull($sourceChain); + + $definitions = $source->getDefinitions(); + $this->assertCount(2, $definitions); + + /** @var ValueDefinition $definition */ + $definition = $definitions['foo']; + $this->assertInstanceOf(ValueDefinition::class, $definition); + $this->assertEquals('bar', $definition->getValue()); + $this->assertIsString($definition->getValue()); + } + + /** + * @test + */ + public function empty_definitions_for_pattern_not_matching_any_files() + { + $pattern = __DIR__ . '/*/no-definitions-here.php'; + $source = new DefinitionGlob($pattern); + + $definitions = $source->getDefinitions(); + $this->assertCount(0, $definitions); + } +} diff --git a/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php b/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php index 9ae98b9a2..3123df644 100644 --- a/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php +++ b/tests/UnitTest/Definition/Source/ReflectionBasedAutowiringTest.php @@ -35,7 +35,7 @@ public function testConstructor() $this->assertCount(1, $parameters); $param1 = $parameters[0]; - $this->assertEquals(new Reference(AutowiringFixture::class), $param1); + $this->assertEquals(new Reference(AutowiringFixture::class, AutowiringFixture::class), $param1); } public function testConstructorInParentClass() @@ -50,6 +50,6 @@ public function testConstructorInParentClass() $this->assertCount(1, $parameters); $param1 = $parameters[0]; - $this->assertEquals(new Reference(AutowiringFixture::class), $param1); + $this->assertEquals(new Reference(AutowiringFixture::class, AutowiringFixtureChild::class), $param1); } } diff --git a/tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php b/tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php new file mode 100644 index 000000000..34fd8432f --- /dev/null +++ b/tests/UnitTest/ServiceLocator/ServiceLocatorRepositoryTest.php @@ -0,0 +1,107 @@ + 'SomeServiceClass']; + + $serviceLocator = $repository->create('test', $services); + + $this->assertEquals('test', $serviceLocator->getSubscriber()); + $this->assertEquals($expectedServices, $serviceLocator->getServices()); + } + + public function testServiceLocatorNotCreated() + { + $this->expectException(\DI\NotFoundException::class); + $this->expectExceptionMessage('Service locator for entry \'something\' is not initialized.'); + + $container = ContainerBuilder::buildDevContainer(); + $repository = new ServiceLocatorRepository($container); + $repository->get('something'); + } + + public function testGetServiceLocator() + { + $container = ContainerBuilder::buildDevContainer(); + $repository = new ServiceLocatorRepository($container); + $repository->create('test'); + + $this->assertInstanceOf(ServiceLocator::class, $repository->get('test')); + } + + public function testHasServiceLocator() + { + $container = ContainerBuilder::buildDevContainer(); + $repository = new ServiceLocatorRepository($container); + $repository->create('test'); + + $this->assertTrue($repository->has('test')); + $this->assertFalse($repository->has('something-else')); + } + + public function testOverrideService() + { + $container = ContainerBuilder::buildDevContainer(); + $repository = new ServiceLocatorRepository($container); + $repository->override('test', 'foo'); + $repository->override('test', 'bar', 'baz'); + + $locator = $repository->create('test'); + $this->assertEquals(['foo' => 'foo', 'bar' => 'baz'], $locator->getServices()); + } + + public function testCanCreateMultipleWithSameServices() + { + $container = ContainerBuilder::buildDevContainer(); + $repository = new ServiceLocatorRepository($container); + $locator1 = $repository->create('test', ['foo']); + $locator2 = $repository->create('test', ['foo']); + + // same instance + $this->assertSame($locator1, $locator2); + + $repository->override('test2', 'bar', 'baz'); + $locator3 = $repository->create('test2'); + $locator4 = $repository->create('test2'); + $this->assertSame($locator3, $locator4); + + // still same services, because that matches the initial override + $locator5 = $repository->create('test2', ['bar' => 'baz']); + $this->assertSame($locator3, $locator5); + } + + public function testCannotCreateMultipleWithDifferentServices() + { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('ServiceLocator for \'test\' cannot be recreated with different services.'); + + $container = ContainerBuilder::buildDevContainer(); + $repository = new ServiceLocatorRepository($container); + + $repository->create('test', ['foo']); + $repository->create('test', ['foo2']); + } +} diff --git a/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php b/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php new file mode 100644 index 000000000..af7e748c5 --- /dev/null +++ b/tests/UnitTest/ServiceLocator/ServiceLocatorTest.php @@ -0,0 +1,84 @@ + 'bar', + 'baz', + ]; + $serviceLocator = new ServiceLocator($container, $services, 'test'); + + $this->assertEquals([ + 'foo' => 'bar', + 'baz' => 'baz', + ], $serviceLocator->getServices()); + $this->assertEquals('test', $serviceLocator->getSubscriber()); + } + + public function testServiceNotDefined() + { + $this->expectException(\DI\NotFoundException::class); + $this->expectExceptionMessage('Service \'something\' is not defined.'); + + $container = ContainerBuilder::buildDevContainer(); + $serviceLocator = new ServiceLocator($container, [], 'test'); + $serviceLocator->get('something'); + } + + public function testGetService() + { + $services = [ + 'stdClass', + 'service' => Singleton::class, + ]; + $services2 = [ + Singleton::class, + ]; + + $container = ContainerBuilder::buildDevContainer(); + $serviceLocator = new ServiceLocator($container, $services, 'test'); + $serviceLocator2 = new ServiceLocator($container, $services2, 'test2'); + + $this->assertInstanceOf('stdClass', $serviceLocator->get('stdClass')); + + $service1 = $serviceLocator->get('service'); + $this->assertInstanceOf(Singleton::class, $service1); + + $service2 = $serviceLocator2->get(Singleton::class); + $this->assertInstanceOf(Singleton::class, $service2); + + // it should be the same instances shared from the container + $this->assertSame($service1, $service2); + } + + public function testHasService() + { + $services = [ + 'service' => Singleton::class, + ]; + + $container = ContainerBuilder::buildDevContainer(); + $serviceLocator = new ServiceLocator($container, $services, 'test'); + + $this->assertTrue($serviceLocator->has('service')); + $this->assertFalse($serviceLocator->has(Singleton::class)); + } +}