├── tools ├── labrador-cs │ └── composer.json ├── psalm │ └── composer.json └── phpunit │ └── composer.json ├── src ├── Cli │ ├── OutputWithHelpers.php │ ├── Exception │ │ ├── OptionNotFound.php │ │ ├── CommandNotFound.php │ │ ├── PotentialConfigurationOverwrite.php │ │ ├── CliException.php │ │ ├── ConfigurationNotFound.php │ │ ├── CacheDirNotFound.php │ │ ├── ComposerConfigurationNotFound.php │ │ ├── CacheDirConfigurationNotFound.php │ │ └── InvalidOptionType.php │ ├── Output.php │ ├── Command.php │ ├── Stderr.php │ ├── Stdout.php │ ├── ResourceOutput.php │ ├── Input.php │ ├── Command │ │ ├── HelpCommand.php │ │ └── CacheClearCommand.php │ ├── InputParser.php │ ├── CommandExecutor.php │ └── TerminalOutput.php ├── ContainerFactory │ ├── ContainerFactoryState.php │ ├── AliasResolution │ │ ├── AliasResolutionReason.php │ │ ├── AliasDefinitionResolution.php │ │ ├── AliasDefinitionResolver.php │ │ └── StandardAliasDefinitionResolver.php │ ├── ListOf.php │ ├── ContainerReference.php │ ├── ServiceCollectorReference.php │ ├── HasServicePrepareState.php │ ├── ContainerFactoryOptions.php │ ├── EnvironmentParameterStore.php │ ├── ListOfAsArray.php │ ├── HasPropertyInjectState.php │ ├── ContainerFactory.php │ ├── ParameterStore.php │ ├── HasMethodInjectState.php │ ├── ContainerFactoryOptionsBuilder.php │ ├── AurynContainerFactoryState.php │ ├── IlluminateContainerFactoryState.php │ └── PhpDiContainerFactoryState.php ├── Bootstrap │ ├── ThirdPartyInitializerProvider.php │ ├── DefinitionProviderFactory.php │ ├── ServiceFromServiceDefinition.php │ ├── ParameterStoreFactory.php │ ├── ContainerAnalytics.php │ ├── ObserverFactory.php │ ├── BootstrappingDirectoryResolver.php │ ├── ServiceGatherer.php │ ├── ContainerAnalyticsObserver.php │ ├── PreAnalysisObserver.php │ ├── ThirdPartyInitializer.php │ ├── PostAnalysisObserver.php │ ├── ContainerCreatedObserver.php │ ├── DefaultParameterStoreFactory.php │ ├── DelegatedParameterStoreFactory.php │ ├── RootDirectoryBootstrappingDirectoryResolver.php │ ├── BootstrappingConfiguration.php │ └── ComposerJsonScanningThirdPartyInitializerProvider.php ├── Exception │ ├── UnsupportedOperation.php │ ├── InvalidServicePrepareDefinition.php │ ├── InvalidLogFile.php │ ├── InvalidServiceDelegateDefinition.php │ ├── AutowireParameterNotFound.php │ ├── BackingContainerNotFound.php │ ├── Exception.php │ ├── InvalidCache.php │ ├── EnvironmentVarNotFound.php │ ├── ContainerException.php │ ├── ComposerAutoloadNotFound.php │ ├── InvalidServicePrepare.php │ ├── ServiceNotFound.php │ ├── ParameterStoreNotFound.php │ ├── InvalidScanDirectories.php │ ├── InvalidAlias.php │ ├── InvalidAutowireParameter.php │ ├── InvalidParameterStore.php │ ├── InvalidInjectDefinition.php │ ├── InvalidBootstrapConfiguration.php │ └── InvalidServiceDelegate.php ├── AnnotatedContainerVersion.php ├── LogicalConstraint │ ├── LogicalConstraintViolationType.php │ ├── LogicalConstraint.php │ ├── LogicalConstraintViolation.php │ ├── LogicalConstraintValidator.php │ ├── Check │ │ ├── NonPublicServicePrepare.php │ │ ├── NonPublicServiceDelegate.php │ │ ├── DuplicateServiceName.php │ │ ├── DuplicateServiceType.php │ │ ├── DuplicateServicePrepare.php │ │ ├── DuplicateServiceDelegate.php │ │ └── MultiplePrimaryForAbstractService.php │ └── LogicalConstraintViolationCollection.php ├── AnnotatedContainer.php ├── Autowire │ ├── AutowireableInvoker.php │ ├── AutowireableFactory.php │ ├── AutowireableParameter.php │ └── AutowireableParameterSet.php ├── StaticAnalysis │ ├── DefinitionProvider.php │ ├── CallableDefinitionProvider.php │ ├── ContainerDefinitionAnalyzer.php │ ├── CompositeDefinitionProvider.php │ ├── DefinitionProviderContext.php │ ├── ContainerDefinitionAnalysisOptions.php │ ├── ContainerDefinitionAnalyzerBuilder.php │ ├── ContainerDefinitionAnalysisOptionsBuilder.php │ └── CacheAwareContainerDefinitionAnalyzer.php ├── Definition │ ├── InjectTargetIdentifier.php │ ├── AliasDefinition.php │ ├── ServicePrepareDefinition.php │ ├── ServiceDelegateDefinition.php │ ├── ConfigurationDefinition.php │ ├── InjectDefinition.php │ ├── ServiceDefinition.php │ ├── ServicePrepareDefinitionBuilder.php │ ├── AliasDefinitionBuilder.php │ ├── ConfigurationDefinitionBuilder.php │ ├── ServiceDelegateDefinitionBuilder.php │ ├── ContainerDefinition.php │ ├── ProfilesAwareContainerDefinition.php │ └── ServiceDefinitionBuilder.php ├── ArchitecturalDecisions │ └── Initializer.php ├── Internal │ ├── PropertyInjectTargetIdentifier.php │ ├── MethodParameterInjectTargetIdentifier.php │ ├── AttributeType.php │ ├── StdoutLogger.php │ ├── FileLogger.php │ ├── SerializerInjectValueParser.php │ └── CompositeLogger.php └── Profiles │ ├── ActiveProfiles.php │ ├── ActiveProfilesParser.php │ ├── CsvActiveProfilesParser.php │ └── ActiveProfilesBuilder.php ├── .gitattributes ├── Justfile ├── SECURITY.md ├── LICENSE ├── docs ├── README.md ├── references │ ├── 01-attributes-list.md │ └── 02-functional-api.md ├── tutorials │ ├── 06-injecting-scalar-values.md │ ├── 10-annotated-container-observers.md │ ├── 09-autowire-aware-invoker.md │ ├── 08-autowire-aware-factory.md │ ├── 05-calling-post-construct-methods.md │ └── 07-using-configuration-objects.md └── how-to │ └── 01-add-third-party-services.md ├── bin └── annotated-container ├── composer.json └── annotated-container.xsd /tools/labrador-cs/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "cspray/labrador-coding-standard": "^0.2.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /tools/psalm/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "vimeo/psalm": "^5.24", 4 | "cspray/phinal": "^2.0" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /tools/phpunit/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "phpunit/phpunit": "^11", 4 | "mikey179/vfsstream": "^1.6" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Cli/OutputWithHelpers.php: -------------------------------------------------------------------------------- 1 | > 9 | */ 10 | public function getThirdPartyInitializers() : array; 11 | } 12 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | .github export-ignore 2 | .gitignore export-ignore 3 | known-issues.xml export-ignore 4 | phpunit.xml export-ignore 5 | psalm.xml export-ignore 6 | CODE_OF_CONDUCT.md export-ignore 7 | CONTRIBUTING.md export-ignore 8 | fixture_src export-ignore 9 | test export-ignore -------------------------------------------------------------------------------- /src/Bootstrap/DefinitionProviderFactory.php: -------------------------------------------------------------------------------- 1 | $identifier 11 | * @return ParameterStore 12 | */ 13 | public function createParameterStore(string $identifier) : ParameterStore; 14 | } 15 | -------------------------------------------------------------------------------- /src/Exception/ContainerException.php: -------------------------------------------------------------------------------- 1 | getMessage(), previous: $throwable); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/Cli/Stderr.php: -------------------------------------------------------------------------------- 1 | output = new ResourceOutput(STDERR); 13 | } 14 | 15 | public function write(string|Stringable $msg, bool $appendNewLine = true) : void { 16 | $this->output->write($msg, $appendNewLine); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Cli/Stdout.php: -------------------------------------------------------------------------------- 1 | output = new ResourceOutput(STDOUT); 13 | } 14 | 15 | public function write(string|Stringable $msg, bool $appendNewLine = true) : void { 16 | $this->output->write($msg, $appendNewLine); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/ContainerFactory/ListOf.php: -------------------------------------------------------------------------------- 1 | $servicesOfType 16 | * @return CollectionType 17 | */ 18 | public function toCollection(array $servicesOfType) : mixed; 19 | } 20 | -------------------------------------------------------------------------------- /src/Cli/Exception/ComposerConfigurationNotFound.php: -------------------------------------------------------------------------------- 1 | resource, (string) $msg); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Bootstrap/ObserverFactory.php: -------------------------------------------------------------------------------- 1 | $type 12 | * @return ServiceFromServiceDefinition[] 13 | */ 14 | public function getServicesForType(string $type) : array; 15 | 16 | /** 17 | * @param class-string $attributeType 18 | * @return ServiceFromServiceDefinition[] 19 | */ 20 | public function getServicesWithAttribute(string $attributeType) : array; 21 | } 22 | -------------------------------------------------------------------------------- /src/Cli/Exception/CacheDirConfigurationNotFound.php: -------------------------------------------------------------------------------- 1 | > 9 | */ 10 | private array $servicePrepares = []; 11 | 12 | public function addServicePrepare(string $class, string $method) : void { 13 | $this->servicePrepares[$class] ??= []; 14 | $this->servicePrepares[$class][] = $method; 15 | } 16 | 17 | public function getServicePrepares() : array { 18 | return $this->servicePrepares; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/StaticAnalysis/DefinitionProvider.php: -------------------------------------------------------------------------------- 1 | getBuilder(), then pass the new builder instance to $context->setBuilder(). 13 | * 14 | * @param DefinitionProviderContext $context 15 | */ 16 | public function consume(DefinitionProviderContext $context) : void; 17 | } 18 | -------------------------------------------------------------------------------- /src/Bootstrap/ThirdPartyInitializer.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | abstract public function getRelativeScanDirectories() : array; 16 | 17 | /** 18 | * @return list 19 | * @deprecated 20 | */ 21 | abstract public function getObserverClasses() : array; 22 | 23 | abstract public function getDefinitionProviderClass() : ?string; 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/InvalidScanDirectories.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function getActiveProfiles() : array; 20 | 21 | /** 22 | * @deprecated 23 | */ 24 | public function getLogger() : ?LoggerInterface; 25 | } 26 | -------------------------------------------------------------------------------- /src/Exception/InvalidAlias.php: -------------------------------------------------------------------------------- 1 | |string|bool> 11 | */ 12 | public function getOptions() : array; 13 | 14 | /** 15 | * @return list 16 | */ 17 | public function getArguments() : array; 18 | 19 | /** 20 | * @return list|string|bool|null 21 | */ 22 | public function getOption(string $opt) : array|string|bool|null; 23 | 24 | /** 25 | * @return list|string|bool 26 | * @throws OptionNotFound 27 | */ 28 | public function requireOption(string $opt) : array|string|bool; 29 | } 30 | -------------------------------------------------------------------------------- /src/StaticAnalysis/CallableDefinitionProvider.php: -------------------------------------------------------------------------------- 1 | callable = $callable; 15 | } 16 | 17 | /** 18 | * @param DefinitionProviderContext $context 19 | * @return void 20 | */ 21 | public function consume(DefinitionProviderContext $context) : void { 22 | ($this->callable)($context); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/InvalidAutowireParameter.php: -------------------------------------------------------------------------------- 1 | > 11 | */ 12 | final class ListOfAsArray implements ListOf { 13 | 14 | /** 15 | * @param class-string $type 16 | */ 17 | public function __construct( 18 | private readonly string $type 19 | ) { 20 | } 21 | 22 | public function type() : ObjectType { 23 | return objectType($this->type); 24 | } 25 | 26 | /** 27 | * @param list $servicesOfType 28 | * @return list 29 | */ 30 | public function toCollection(array $servicesOfType) : array { 31 | return $servicesOfType; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Definition/ServicePrepareDefinition.php: -------------------------------------------------------------------------------- 1 | > 12 | */ 13 | private array $propertyInject = []; 14 | 15 | public function addPropertyInject(string $class, string $property, mixed $value) : void { 16 | $this->propertyInject[$class] ??= []; 17 | $this->propertyInject[$class][$property] = $value; 18 | } 19 | 20 | public function propertiesToInject(string $class) : array { 21 | return $this->propertyInject[$class] ?? []; 22 | } 23 | 24 | /** 25 | * @return array> 26 | */ 27 | public function getPropertyInject() : array { 28 | return $this->propertyInject; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env just --justfile 2 | 3 | _default: 4 | just --list --unsorted 5 | 6 | # Install all dependencies necesesary to run Annotated Container tools 7 | install: _install_ac 8 | 9 | _install_ac: 10 | composer install 11 | 12 | _install_labrador_cs: 13 | cd tools/labrador-cs 14 | composer install 15 | 16 | _install_phpunit: 17 | cd tools/phpunit 18 | composer install 19 | 20 | _install_psalm: 21 | cd tools/psalm 22 | composer install 23 | 24 | # Run unit tests 25 | test: 26 | @XDEBUG_MODE=coverage ./tools/phpunit/vendor/bin/phpunit 27 | 28 | # Run static analysis checks on src and test 29 | static-analysis: 30 | @./tools/psalm/vendor/bin/psalm 31 | 32 | # Run code-linting tools on src and test 33 | code-lint: 34 | @./tools/labrador-cs/vendor/bin/phpcs -p --standard=./tools/labrador-cs/vendor/cspray/labrador-coding-standard/ruleset.xml --exclude=Generic.Files.LineLength src test 35 | -------------------------------------------------------------------------------- /src/Bootstrap/DefaultParameterStoreFactory.php: -------------------------------------------------------------------------------- 1 | $identifier 12 | * @return ParameterStore 13 | */ 14 | public function createParameterStore(string $identifier) : ParameterStore { 15 | if (!class_exists($identifier)) { 16 | throw InvalidParameterStore::fromParameterStoreIdentifierNotClass($identifier); 17 | } 18 | 19 | if (!is_a($identifier, ParameterStore::class, true)) { 20 | throw InvalidParameterStore::fromIdentifierNotParameterStore($identifier); 21 | } 22 | 23 | return new $identifier(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Annotated Container Security 2 | 3 | This document details which versions of Annotated Container are being supported by security fixes and how you can report a security vulnerability. 4 | 5 | ## Supported Versions 6 | 7 | As of 2024-06-04 the only supported series is the **2.x** release. All versions in this release series will receive security fixes. Using versions prior to this is not advised and efforts should be made to update the library. 8 | 9 | It is planned for the 2.x series to continue receiving security fixes when the 3.x release goes live. 10 | 11 | ## Reporting a Vulnerability 12 | 13 | Unlike other bugs or issues I recommend not creating a public Issue for security vulnerabilities. Instead, please reach out to me at [annotated-container-security@me.cspray.io](annotated-container-security@me.cspray.io) with details of your discovery. It would be beneficial if you created a **private** GitHub repository with relevant code and tests describing the flaw. -------------------------------------------------------------------------------- /src/Internal/PropertyInjectTargetIdentifier.php: -------------------------------------------------------------------------------- 1 | name; 28 | } 29 | 30 | public function getClass() : ObjectType { 31 | return $this->class; 32 | } 33 | 34 | public function getMethodName() : ?string { 35 | return null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Exception/InvalidInjectDefinition.php: -------------------------------------------------------------------------------- 1 | $profiles 19 | * @return LogicalConstraintViolationCollection 20 | */ 21 | public function getConstraintViolations( 22 | ContainerDefinition $containerDefinition, 23 | array $profiles 24 | ) : LogicalConstraintViolationCollection; 25 | } 26 | -------------------------------------------------------------------------------- /src/LogicalConstraint/LogicalConstraintViolation.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | public function getProfiles() : array; 24 | 25 | /** 26 | * Determine whether $profile is included in the list of active profiles. 27 | * 28 | * @param string $profile 29 | * @return bool 30 | */ 31 | public function isActive(string $profile) : bool; 32 | } 33 | -------------------------------------------------------------------------------- /src/Bootstrap/DelegatedParameterStoreFactory.php: -------------------------------------------------------------------------------- 1 | , ParameterStoreFactory> 11 | */ 12 | private array $mappedParameterStores; 13 | 14 | public function __construct( 15 | private readonly ParameterStoreFactory $defaultFactory 16 | ) { 17 | $this->mappedParameterStores = []; 18 | } 19 | 20 | public function addParameterStoreFactory(string $identifier, ParameterStoreFactory $factory) : void { 21 | $this->mappedParameterStores[$identifier] = $factory; 22 | } 23 | 24 | public function createParameterStore(string $identifier) : ParameterStore { 25 | $factory = $this->mappedParameterStores[$identifier] ?? $this->defaultFactory; 26 | return $factory->createParameterStore($identifier); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/ContainerFactory/ContainerFactory.php: -------------------------------------------------------------------------------- 1 | name; 30 | } 31 | 32 | public function getClass() : ObjectType { 33 | return $this->class; 34 | } 35 | 36 | public function getMethodName() : string { 37 | return $this->methodName; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Autowire/AutowireableFactory.php: -------------------------------------------------------------------------------- 1 | rootDir, $subPath); 14 | } 15 | 16 | public function getPathFromRoot(string $subPath) : string { 17 | return sprintf('%s/%s', $this->rootDir, $subPath); 18 | } 19 | 20 | public function getCachePath(string $subPath) : string { 21 | return sprintf('%s/%s', $this->rootDir, $subPath); 22 | } 23 | 24 | /** 25 | * @deprecated 26 | */ 27 | public function getLogPath(string $subPath) : string { 28 | return sprintf('%s/%s', $this->rootDir, $subPath); 29 | } 30 | 31 | public function getVendorPath() : string { 32 | return sprintf('%s/vendor', $this->rootDir); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/ContainerFactory/ParameterStore.php: -------------------------------------------------------------------------------- 1 | dateTimeProvider = $dateTimeProvider; 25 | } 26 | 27 | public function log($level, Stringable|string $message, array $context = []) : void { 28 | $format = '[%s] annotated-container.%s: %s %s%s'; 29 | $contents = sprintf( 30 | $format, 31 | ($this->dateTimeProvider)()->format('Y-m-d\TH:i:s.uP'), 32 | strtoupper((string) $level), 33 | (string) $message, 34 | json_encode($context), 35 | PHP_EOL 36 | ); 37 | fwrite(STDOUT, $contents); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Profiles/ActiveProfilesParser.php: -------------------------------------------------------------------------------- 1 | 9 | */ 10 | private readonly array $providers; 11 | 12 | public function __construct( 13 | DefinitionProvider $provider, 14 | DefinitionProvider...$providers 15 | ) { 16 | $this->providers = [ 17 | $provider, 18 | ...$providers 19 | ]; 20 | } 21 | 22 | public function consume(DefinitionProviderContext $context) : void { 23 | foreach ($this->providers as $provider) { 24 | $provider->consume($context); 25 | } 26 | } 27 | 28 | public function getDefinitionProviders() : array { 29 | return $this->providers; 30 | } 31 | 32 | public function __toString() : string { 33 | $classes = array_map( 34 | static fn(DefinitionProvider $provider) => $provider::class, 35 | $this->getDefinitionProviders() 36 | ); 37 | return sprintf('Composite<%s>', implode(', ', $classes)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Definition/ServiceDelegateDefinition.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | public function getScanDirectories() : array; 16 | 17 | public function getCacheDirectory() : ?string; 18 | 19 | #[SingleEntrypointDefinitionProvider] 20 | public function getContainerDefinitionProvider() : ?DefinitionProvider; 21 | 22 | /** 23 | * @return list 24 | */ 25 | public function getParameterStores() : array; 26 | 27 | /** 28 | * @return list 29 | * @deprecated 30 | */ 31 | public function getObservers() : array; 32 | 33 | /** 34 | * @return LoggerInterface|null 35 | * @deprecated 36 | */ 37 | public function getLogger() : ?LoggerInterface; 38 | 39 | /** 40 | * @return list 41 | * @deprecated 42 | */ 43 | public function getLoggingExcludedProfiles() : array; 44 | } 45 | -------------------------------------------------------------------------------- /src/StaticAnalysis/ContainerDefinitionAnalysisOptions.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function getScanDirectories() : array; 19 | 20 | /** 21 | * If you need to modify the ContainerDefinitionBuilder return a proper consumer, otherwise null. 22 | * 23 | * This is the primary entrypoint for adding third-party services that can't be annotated to the container. 24 | * 25 | * @return DefinitionProvider|null 26 | */ 27 | #[SingleEntrypointDefinitionProvider] 28 | public function getDefinitionProvider() : ?DefinitionProvider; 29 | 30 | /** 31 | * If a LoggerInterface is returned information about the compilation and container creation process will be logged 32 | * to it. 33 | * 34 | * @return LoggerInterface|null 35 | * 36 | * @deprecated 37 | */ 38 | public function getLogger() : ?LoggerInterface; 39 | } 40 | -------------------------------------------------------------------------------- /src/ContainerFactory/HasMethodInjectState.php: -------------------------------------------------------------------------------- 1 | >> 12 | */ 13 | private array $methodInject = []; 14 | 15 | /** 16 | * @param class-string $class 17 | * @param non-empty-string $method 18 | * @param non-empty-string $param 19 | * @return void 20 | */ 21 | public function addMethodInject(string $class, string $method, string $param, mixed $value) : void { 22 | $this->methodInject[$class] ??= []; 23 | $this->methodInject[$class][$method] ??= []; 24 | $this->methodInject[$class][$method][$param] = $value; 25 | } 26 | 27 | /** 28 | * @param class-string $class 29 | * @param non-empty-string $method 30 | * @return array 31 | */ 32 | public function parametersForMethod(string $class, string $method) : array { 33 | return $this->methodInject[$class][$method] ?? []; 34 | } 35 | 36 | /** 37 | * @return array>> 38 | */ 39 | public function getMethodInject() : array { 40 | return $this->methodInject; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/LogicalConstraint/LogicalConstraintValidator.php: -------------------------------------------------------------------------------- 1 | logicalConstraints = $logicalConstraints; 19 | } 20 | 21 | /** 22 | * Run all the LogicalConstraint implementations that this library defines and return a merged collection of any 23 | * violations that might exist in $containerDefinition. 24 | * 25 | * @param ContainerDefinition $containerDefinition 26 | * @param list $profiles 27 | * @return LogicalConstraintViolationCollection 28 | */ 29 | public function validate(ContainerDefinition $containerDefinition, array $profiles) : LogicalConstraintViolationCollection { 30 | $collection = new LogicalConstraintViolationCollection(); 31 | 32 | foreach ($this->logicalConstraints as $constraint) { 33 | $collection->addAll( 34 | $constraint->getConstraintViolations($containerDefinition, $profiles) 35 | ); 36 | } 37 | 38 | return $collection; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/ContainerFactory/ContainerFactoryOptionsBuilder.php: -------------------------------------------------------------------------------- 1 | activeProfiles = [$profile, ...$additionalProfiles]; 18 | return $instance; 19 | } 20 | 21 | /** 22 | * @deprecated 23 | */ 24 | public function withLogger(LoggerInterface $logger) : self { 25 | $instance = clone $this; 26 | $instance->logger = $logger; 27 | return $instance; 28 | } 29 | 30 | public function build() : ContainerFactoryOptions { 31 | return new class($this->activeProfiles, $this->logger) implements ContainerFactoryOptions { 32 | public function __construct( 33 | private readonly array $activeProfiles, 34 | private readonly ?LoggerInterface $logger 35 | ) { 36 | } 37 | 38 | public function getActiveProfiles(): array { 39 | return $this->activeProfiles; 40 | } 41 | 42 | public function getLogger() : ?LoggerInterface { 43 | return $this->logger; 44 | } 45 | }; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Autowire/AutowireableParameterSet.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | interface AutowireableParameterSet extends Countable, IteratorAggregate { 17 | 18 | /** 19 | * @param AutowireableParameter $autowireableParameter The parameter that should be added to the set 20 | * @return void 21 | * @throws InvalidAutowireParameter Thrown if an error was found with the passed parameter 22 | */ 23 | public function add(AutowireableParameter $autowireableParameter) : void; 24 | 25 | /** 26 | * @param int $index The 0-based index for which parameter to retrieve 27 | * @return AutowireableParameter The parameter at the given index. 28 | * @throws AutowireParameterNotFound Thrown if an $index is passed that has no corresponding parameter 29 | */ 30 | public function get(int $index) : AutowireableParameter; 31 | 32 | /** 33 | * @param int $index The 0-based index for which parameter to check the existence of 34 | * @return bool Whether a parameter can be found at the given index. 35 | */ 36 | public function has(int $index) : bool; 37 | } 38 | -------------------------------------------------------------------------------- /src/Internal/FileLogger.php: -------------------------------------------------------------------------------- 1 | dateTimeProvider = $dateTimeProvider; 31 | if (! @touch($this->file)) { 32 | throw InvalidLogFile::fromLogFileNotWritable($this->file); 33 | } 34 | } 35 | 36 | /** 37 | * @param mixed $level 38 | * @param Stringable|string $message 39 | * @param array $context 40 | * @return void 41 | */ 42 | public function log($level, Stringable|string $message, array $context = []) : void { 43 | $format = '[%s] annotated-container.%s: %s %s' . PHP_EOL; 44 | $contents = sprintf( 45 | $format, 46 | ($this->dateTimeProvider)()->format('Y-m-d\TH:i:s.uP'), 47 | strtoupper((string) $level), 48 | (string) $message, 49 | json_encode((object) $context) 50 | ); 51 | 52 | file_put_contents($this->file, $contents, FILE_APPEND); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /bin/annotated-container: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | setDefaultCommand(new HelpCommand($commandExecutor)); 27 | $commandExecutor->addCommand(new InitCommand($directoryResolver, new ComposerJsonScanningThirdPartyInitializerProvider($directoryResolver))); 28 | $commandExecutor->addCommand(new BuildCommand($directoryResolver)); 29 | $commandExecutor->addCommand(new CacheClearCommand($directoryResolver)); 30 | $commandExecutor->addCommand(new ValidateCommand($directoryResolver)); 31 | 32 | $input = (new InputParser())->parse($argv); 33 | $terminalOutput = new TerminalOutput(); 34 | 35 | $exitCode = $commandExecutor->execute($input, $terminalOutput); 36 | exit($exitCode); 37 | 38 | -------------------------------------------------------------------------------- /src/Definition/InjectDefinition.php: -------------------------------------------------------------------------------- 1 | 44 | */ 45 | public function getProfiles() : array; 46 | 47 | /** 48 | * The store name to retrieve the value from, or null if getValue() should be used directly. 49 | * 50 | * @return non-empty-string|null 51 | */ 52 | public function getStoreName() : ?string; 53 | 54 | public function getAttribute() : ?InjectAttribute; 55 | } 56 | -------------------------------------------------------------------------------- /src/Definition/ServiceDefinition.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public function getProfiles() : array; 36 | 37 | /** 38 | * Return whether the Service is the Primary for this type and will be used by default if there are multiple aliases 39 | * resolved. 40 | * 41 | * @return bool 42 | */ 43 | public function isPrimary() : bool; 44 | 45 | /** 46 | * Returns whether the defined Service is a concrete class that can be instantiated. 47 | * 48 | * @return bool 49 | */ 50 | public function isConcrete() : bool; 51 | 52 | /** 53 | * Returns whether the defined Service is an abstract class or interface that cannot be instantiated. 54 | * 55 | * @return bool 56 | */ 57 | public function isAbstract() : bool; 58 | 59 | public function getAttribute() : ?ServiceAttribute; 60 | } 61 | -------------------------------------------------------------------------------- /src/LogicalConstraint/Check/NonPublicServicePrepare.php: -------------------------------------------------------------------------------- 1 | getServicePrepareDefinitions() as $prepareDefinition) { 17 | $reflection = new \ReflectionMethod(sprintf('%s::%s', $prepareDefinition->getService()->getName(), $prepareDefinition->getMethod())); 18 | if ($reflection->isPrivate() || $reflection->isProtected()) { 19 | $protectedOrPrivate = $reflection->isProtected() ? 'protected' : 'private'; 20 | $violations->add( 21 | LogicalConstraintViolation::critical( 22 | sprintf( 23 | 'A %s method, %s::%s, is marked as a service prepare. Service prepare methods MUST be marked public.', 24 | $protectedOrPrivate, 25 | $prepareDefinition->getService()->getName(), 26 | $prepareDefinition->getMethod() 27 | ) 28 | ) 29 | ); 30 | } 31 | } 32 | 33 | return $violations; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LogicalConstraint/Check/NonPublicServiceDelegate.php: -------------------------------------------------------------------------------- 1 | getServiceDelegateDefinitions() as $delegateDefinition) { 17 | $reflection = new \ReflectionMethod(sprintf('%s::%s', $delegateDefinition->getDelegateType()->getName(), $delegateDefinition->getDelegateMethod())); 18 | if ($reflection->isProtected() || $reflection->isPrivate()) { 19 | $protectedOrPrivate = $reflection->isProtected() ? 'protected' : 'private'; 20 | $violations->add( 21 | LogicalConstraintViolation::critical( 22 | sprintf( 23 | 'A %s method, %s::%s, is marked as a service delegate. Service delegates MUST be marked public.', 24 | $protectedOrPrivate, 25 | $delegateDefinition->getDelegateType()->getName(), 26 | $delegateDefinition->getDelegateMethod() 27 | ) 28 | ) 29 | ); 30 | } 31 | } 32 | 33 | return $violations; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/LogicalConstraint/LogicalConstraintViolationCollection.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | final class LogicalConstraintViolationCollection implements Countable, IteratorAggregate { 16 | 17 | /** 18 | * @var list 19 | */ 20 | private array $constraintViolations = []; 21 | 22 | /** 23 | * Will add any violations in $constraintViolationCollection to this collection. 24 | * 25 | * This is a mutable operation. 26 | * 27 | * @param LogicalConstraintViolationCollection $constraintViolationCollection 28 | * @return void 29 | */ 30 | public function addAll(LogicalConstraintViolationCollection $constraintViolationCollection) : void { 31 | foreach ($constraintViolationCollection as $constraintViolation) { 32 | $this->constraintViolations[] = $constraintViolation; 33 | } 34 | } 35 | 36 | public function add(LogicalConstraintViolation $logicalConstraintViolation) : void { 37 | $this->constraintViolations[] = $logicalConstraintViolation; 38 | } 39 | 40 | public function get(int $index) : ?LogicalConstraintViolation { 41 | return $this->constraintViolations[$index] ?? null; 42 | } 43 | 44 | /** 45 | * @return Traversable 46 | */ 47 | public function getIterator() : Traversable { 48 | return new ArrayIterator($this->constraintViolations); 49 | } 50 | 51 | public function count() : int { 52 | return count($this->constraintViolations); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Definition/ServicePrepareDefinitionBuilder.php: -------------------------------------------------------------------------------- 1 | service = $serviceDefinition; 24 | $instance->method = $method; 25 | return $instance; 26 | } 27 | 28 | public function withAttribute(ServicePrepareAttribute $attribute) : self { 29 | $instance = clone $this; 30 | $instance->attribute = $attribute; 31 | return $instance; 32 | } 33 | 34 | public function build() : ServicePrepareDefinition { 35 | return new class($this->service, $this->method, $this->attribute) implements ServicePrepareDefinition { 36 | 37 | public function __construct( 38 | private readonly ObjectType $service, 39 | private readonly string $method, 40 | private readonly ?ServicePrepareAttribute $attribute 41 | ) { 42 | } 43 | 44 | public function getService() : ObjectType { 45 | return $this->service; 46 | } 47 | 48 | public function getMethod() : string { 49 | return $this->method; 50 | } 51 | 52 | public function getAttribute() : ?ServicePrepareAttribute { 53 | return $this->attribute; 54 | } 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/LogicalConstraint/Check/DuplicateServiceName.php: -------------------------------------------------------------------------------- 1 | > $namedServiceMap */ 19 | $namedServiceMap = []; 20 | foreach ($containerDefinition->getServiceDefinitions() as $definition) { 21 | $name = $definition->getName(); 22 | if ($name === null) { 23 | continue; 24 | } 25 | 26 | $namedServiceMap[$name] ??= []; 27 | $namedServiceMap[$name][] = $definition->getType()->getName(); 28 | } 29 | 30 | foreach ($namedServiceMap as $name => $services) { 31 | if (count($services) > 1) { 32 | sort($services); 33 | $services = implode( 34 | '- ', 35 | array_map(static fn(string $type) => $type . PHP_EOL, $services) 36 | ); 37 | $message = <<add(LogicalConstraintViolation::critical(trim($message))); 44 | } 45 | } 46 | 47 | return $violations; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Exception/InvalidBootstrapConfiguration.php: -------------------------------------------------------------------------------- 1 | > $serviceTypeMap 18 | */ 19 | $serviceTypeMap = []; 20 | 21 | foreach ($containerDefinition->getServiceDefinitions() as $definition) { 22 | $type = $definition->getType()->getName(); 23 | $serviceTypeMap[$type] ??= []; 24 | $serviceTypeMap[$type][] = $definition->getAttribute(); 25 | } 26 | 27 | foreach ($serviceTypeMap as $type => $attributes) { 28 | if (count($attributes) > 1) { 29 | $attributeTypes = trim(implode('- ', array_map( 30 | static fn(?ServiceAttribute $attribute) => ($attribute === null ? 'Call to service() in DefinitionProvider' : 'Attributed with ' . $attribute::class) . PHP_EOL, 31 | $attributes 32 | ))); 33 | $message = <<add( 44 | LogicalConstraintViolation::warning( 45 | $message, 46 | ) 47 | ); 48 | } 49 | } 50 | 51 | return $violations; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Definition/AliasDefinitionBuilder.php: -------------------------------------------------------------------------------- 1 | abstractType = $serviceDefinition; 27 | return $instance; 28 | } 29 | 30 | /** 31 | * Define the concrete Service that acts as an alias for the given abstract Service. 32 | * 33 | * This method is immutable and a new AliasDefinitionBuilder will be returned. 34 | * 35 | * @param ObjectType $serviceDefinition 36 | * @return $this 37 | */ 38 | public function withConcrete(ObjectType $serviceDefinition) : self { 39 | $instance = clone $this; 40 | $instance->concreteType = $serviceDefinition; 41 | return $instance; 42 | } 43 | 44 | /** 45 | * Returns an AliasDefinition with the provided abstract and concrete Services. 46 | * 47 | * @return AliasDefinition 48 | */ 49 | public function build() : AliasDefinition { 50 | return new class($this->abstractType, $this->concreteType) implements AliasDefinition { 51 | public function __construct( 52 | private readonly ObjectType $abstractService, 53 | private readonly ObjectType $concreteService 54 | ) { 55 | } 56 | 57 | public function getAbstractService() : ObjectType { 58 | return $this->abstractService; 59 | } 60 | 61 | public function getConcreteService() : ObjectType { 62 | return $this->concreteService; 63 | } 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/LogicalConstraint/Check/DuplicateServicePrepare.php: -------------------------------------------------------------------------------- 1 | > $servicePrepareMap */ 17 | $servicePrepareMap = []; 18 | 19 | foreach ($containerDefinition->getServicePrepareDefinitions() as $prepareDefinition) { 20 | $classMethod = sprintf( 21 | '%s::%s', 22 | $prepareDefinition->getService()->getName(), 23 | $prepareDefinition->getMethod() 24 | ); 25 | 26 | $servicePrepareMap[$classMethod] ??= []; 27 | $servicePrepareMap[$classMethod][] = $prepareDefinition->getAttribute(); 28 | } 29 | 30 | foreach ($servicePrepareMap as $classMethod => $attributes) { 31 | if (count($attributes) > 1) { 32 | $attributeTypes = trim(implode('- ', array_map( 33 | static fn(?ServicePrepareAttribute $attribute) => ($attribute === null ? 'Call to servicePrepare() in DefinitionProvider' : 'Attributed with ' . $attribute::class) . PHP_EOL, 34 | $attributes 35 | ))); 36 | $message = <<add(LogicalConstraintViolation::warning($message)); 47 | } 48 | } 49 | 50 | return $violations; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Cli/Command/HelpCommand.php: -------------------------------------------------------------------------------- 1 | Annotated Container $version 26 | 27 | Available Commands: 28 | 29 | init 30 | 31 | \tSetup your app to scan Composer directories, cache your ContainerDefinition, 32 | \tand generate an appropriate configuration file. 33 | 34 | build 35 | 36 | \tBuild your ContainerDefinition from the configuration file and cache it. 37 | \tBuilding a ContainerDefinition without configuring cache support will result 38 | \tin an error. 39 | 40 | cache-clear 41 | 42 | \tDestroy the cache to allow rebuilding the Container. 43 | 44 | For more help: 45 | 46 | help 47 | SHELL; 48 | } 49 | 50 | public function handle(Input $input, TerminalOutput $output) : int { 51 | $arguments = $input->getArguments(); 52 | $argc = count($arguments); 53 | if ($argc !== 2) { 54 | if ($argc > 1) { 55 | $output->stdout->write('!! Warning !! - Expecting 1 arg, showing default help'); 56 | $output->stdout->br(); 57 | } 58 | $output->stdout->write($this->getHelp()); 59 | return 0; 60 | } 61 | 62 | $commandName = $arguments[1]; 63 | $command = $this->commandExecutor->getCommand($commandName); 64 | 65 | if (!isset($command)) { 66 | $output->stderr->write(sprintf('Could not find command "%s"!', $commandName)); 67 | return 1; 68 | } 69 | 70 | $output->stdout->write($command->getHelp()); 71 | return 0; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Definition/ConfigurationDefinitionBuilder.php: -------------------------------------------------------------------------------- 1 | classType = $objectType; 29 | return $instance; 30 | } 31 | 32 | public function withName(string $name) : self { 33 | $instance = clone $this; 34 | $instance->name = $name; 35 | return $instance; 36 | } 37 | 38 | public function withAttribute(ConfigurationAttribute $attribute) : self { 39 | $instance = clone $this; 40 | $instance->attribute = $attribute; 41 | return $instance; 42 | } 43 | 44 | public function build() : ConfigurationDefinition { 45 | return new class($this->classType, $this->name, $this->attribute) implements ConfigurationDefinition { 46 | public function __construct( 47 | private readonly ObjectType $classType, 48 | private readonly ?string $name, 49 | private readonly ?ConfigurationAttribute $attribute 50 | ) { 51 | } 52 | 53 | public function getClass() : ObjectType { 54 | return $this->classType; 55 | } 56 | 57 | public function getName() : ?string { 58 | return $this->name; 59 | } 60 | 61 | public function getAttribute() : ?ConfigurationAttribute { 62 | return $this->attribute; 63 | } 64 | }; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /docs/references/01-attributes-list.md: -------------------------------------------------------------------------------- 1 | # AnnotatedContainer Attributes 2 | 3 | The following Attributes are made available through this library. All Attributes listed are under the namespace 4 | `Cspray\AnnotatedContainer\Attribute`. 5 | 6 | | Attribute Name | Target | Description | 7 | |----------------------|------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 8 | | `Service` | `Attribute::TARGET_CLASS` | Describes an interface, abstract class, or concrete class as being a service. Will share and alias the types into the Injector based on what's annotated. | 9 | | `ServicePrepare` | `Attribute::TARGET_METHOD` | Describes a method, on an interface or class, that should be invoked when that type is created. | 10 | | `ServiceDelegate` | `Attribute::TARGET_METHOD` | Defines a method that will be used to generate a defined type. | 11 | | `Inject` | `Attribute::TARGET_PARAMETER`, `Attribute::TARGET_PROPERTY` | Defines a value that should be injected, can handle either scalar or service values based on the type-hint of the parameter. Inject Attributes on properties are only recognized if on `[#Configuration]` objects. | 12 | | `Configuration` | `Attribute::TARGET_CLASS` | Defines an instance as being a Configuration object. Properties can have values injected into them. | 13 | -------------------------------------------------------------------------------- /src/Internal/SerializerInjectValueParser.php: -------------------------------------------------------------------------------- 1 | convertStringToType($unionType); 33 | } 34 | /** @psalm-var list $types */ 35 | $type = typeUnion(...$types); 36 | } elseif (str_contains($rawType, '&')) { 37 | $types = []; 38 | foreach (explode('&', $rawType) as $intersectType) { 39 | $parsedType = $this->convertStringToType($intersectType); 40 | assert($parsedType instanceof ObjectType); 41 | $types[] = $parsedType; 42 | } 43 | $type = typeIntersect(...$types); 44 | } else { 45 | $type = match ($rawType) { 46 | 'string' => stringType(), 47 | 'int', 'integer' => intType(), 48 | 'float', 'double' => floatType(), 49 | 'bool', 'boolean' => boolType(), 50 | 'array' => arrayType(), 51 | 'mixed' => mixedType(), 52 | 'iterable' => iterableType(), 53 | 'null', 'NULL' => nullType(), 54 | 'void' => voidType(), 55 | 'callable' => callableType(), 56 | default => objectType($rawType) 57 | }; 58 | } 59 | 60 | return $type; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Profiles/CsvActiveProfilesParser.php: -------------------------------------------------------------------------------- 1 | > $delegateMap 19 | */ 20 | $delegateMap = []; 21 | 22 | foreach ($containerDefinition->getServiceDelegateDefinitions() as $definition) { 23 | $service = $definition->getServiceType()->getName(); 24 | $method = sprintf('%s::%s', $definition->getDelegateType()->getName(), $definition->getDelegateMethod()); 25 | $delegateMap[$service] ??= []; 26 | $attribute = $definition->getAttribute(); 27 | if ($attribute !== null) { 28 | $message = sprintf('%s attributed with %s%s', $method, $definition->getAttribute()::class, PHP_EOL); 29 | } else { 30 | $message = sprintf('%s added with serviceDelegate()%s', $method, PHP_EOL); 31 | } 32 | $delegateMap[$service][] = $message; 33 | } 34 | 35 | foreach ($delegateMap as $service => $factories) { 36 | if (count($factories) > 1) { 37 | $factoryTypes = trim(implode('- ', $factories)); 38 | $message = <<add( 48 | LogicalConstraintViolation::warning($message) 49 | ); 50 | } 51 | } 52 | 53 | return $violations; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/StaticAnalysis/ContainerDefinitionAnalyzerBuilder.php: -------------------------------------------------------------------------------- 1 | cacheDir = $cacheDir; 31 | return $instance; 32 | } 33 | 34 | /** 35 | * With this option the results of the ContainerDefinition WILL NOT be cached. 36 | * 37 | * Using this option will cause the static analysis parsing and conversion of AnnotatedTarget to occur on every 38 | * run. 39 | * 40 | * @return static 41 | */ 42 | public static function withoutCache() : self { 43 | return new self; 44 | } 45 | 46 | /** 47 | * Return the configured ContainerDefinitionCompiler 48 | * 49 | * @return ContainerDefinitionAnalyzer 50 | */ 51 | public function build() : ContainerDefinitionAnalyzer { 52 | return $this->getCacheAppropriateAnalyzer(); 53 | } 54 | 55 | private function getCacheAppropriateAnalyzer() : ContainerDefinitionAnalyzer { 56 | $phpParserCompiler = new AnnotatedTargetContainerDefinitionAnalyzer( 57 | new PhpParserAnnotatedTargetParser(), 58 | new AnnotatedTargetDefinitionConverter() 59 | ); 60 | if (!isset($this->cacheDir)) { 61 | return $phpParserCompiler; 62 | } 63 | return new CacheAwareContainerDefinitionAnalyzer( 64 | $phpParserCompiler, 65 | new ContainerDefinitionSerializer(), 66 | $this->cacheDir 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Bootstrap/ComposerJsonScanningThirdPartyInitializerProvider.php: -------------------------------------------------------------------------------- 1 | >|null 12 | */ 13 | private ?array $initializers = null; 14 | 15 | public function __construct( 16 | private readonly BootstrappingDirectoryResolver $resolver 17 | ) { 18 | } 19 | 20 | public function getThirdPartyInitializers() : array { 21 | if ($this->initializers === null) { 22 | $this->initializers = $this->scanVendorDirectoryForInitializers(); 23 | sort($this->initializers); 24 | } 25 | 26 | return $this->initializers; 27 | } 28 | 29 | /** 30 | * @return list> 31 | */ 32 | private function scanVendorDirectoryForInitializers() : array { 33 | $packages = []; 34 | $vendorIterator = new DirectoryIterator($this->resolver->getVendorPath()); 35 | /** @var SplFileInfo $fileInfo */ 36 | foreach ($vendorIterator as $fileInfo) { 37 | if (!$fileInfo->isDir() || $fileInfo->isDot()) { 38 | continue; 39 | } 40 | 41 | $vendorName = basename($fileInfo->getPathname()); 42 | foreach (new DirectoryIterator($fileInfo->getPathname()) as $vendorPackageInfo) { 43 | if (!$vendorPackageInfo->isDir() || $vendorPackageInfo->isDot()) { 44 | continue; 45 | } 46 | $packages[] = sprintf('%s/%s', $vendorName, basename($vendorPackageInfo->getPathname())); 47 | } 48 | } 49 | 50 | $initializers = []; 51 | foreach ($packages as $package) { 52 | $packageComposerJson = sprintf('%s/%s/composer.json', $this->resolver->getVendorPath(), $package); 53 | $composerData = json_decode( 54 | file_get_contents($packageComposerJson), 55 | true, 56 | 512, 57 | JSON_THROW_ON_ERROR 58 | ); 59 | 60 | $packageInitializers = $composerData['extra']['$annotatedContainer']['initializers'] ?? []; 61 | foreach ($packageInitializers as $packageInitializer) { 62 | $initializers[] = $packageInitializer; 63 | } 64 | } 65 | return $initializers; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Definition/ServiceDelegateDefinitionBuilder.php: -------------------------------------------------------------------------------- 1 | service = $service; 22 | return $instance; 23 | } 24 | 25 | public function withDelegateMethod(ObjectType $delegateType, string $delegateMethod) : self { 26 | if (trim($delegateMethod) === '') { 27 | throw InvalidServiceDelegateDefinition::fromEmptyDelegateMethod(); 28 | } 29 | $instance = clone $this; 30 | $instance->delegateType = $delegateType; 31 | $instance->delegateMethod = $delegateMethod; 32 | return $instance; 33 | } 34 | 35 | public function withAttribute(ServiceDelegateAttribute $attribute) : self { 36 | $instance = clone $this; 37 | $instance->attribute = $attribute; 38 | return $instance; 39 | } 40 | 41 | public function build() : ServiceDelegateDefinition { 42 | return new class($this->service, $this->delegateType, $this->delegateMethod, $this->attribute) implements ServiceDelegateDefinition { 43 | 44 | 45 | public function __construct( 46 | private readonly ObjectType $serviceDefinition, 47 | private readonly ObjectType $delegateType, 48 | private readonly string $delegateMethod, 49 | private readonly ?ServiceDelegateAttribute $attribute 50 | ) { 51 | } 52 | 53 | public function getDelegateType() : ObjectType { 54 | return $this->delegateType; 55 | } 56 | 57 | public function getDelegateMethod() : string { 58 | return $this->delegateMethod; 59 | } 60 | 61 | public function getServiceType() : ObjectType { 62 | return $this->serviceDefinition; 63 | } 64 | 65 | public function getAttribute() : ?ServiceDelegateAttribute { 66 | return $this->attribute; 67 | } 68 | }; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Definition/ContainerDefinition.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | public function getServiceDefinitions() : array; 19 | 20 | /** 21 | * Returns a set of AliasDefinition that define which concrete services are possible candidate for a given abstract 22 | * service. 23 | * 24 | * Note that it is possible for an abstract service to have multiple AliasDefinition defined for it. It is not the 25 | * job of the ContainerDefinition to determine what should be done in this situation; it is meant to simply provide 26 | * the results of a configuration for a Container and an annotated configuration is possible to have multiple 27 | * concrete services that could satisfy an abstract service. It is the responsibility of the ContainerFactory 28 | * or a LogicalConstraintValidator to recognize multiple aliases are possible and take the appropriate steps. 29 | * 30 | * @return list 31 | */ 32 | public function getAliasDefinitions() : array; 33 | 34 | /** 35 | * Returns a set of ServicePrepareDefinition that determine which service methods will be automatically invoked 36 | * after service construction. 37 | * 38 | * @return list 39 | */ 40 | public function getServicePrepareDefinitions() : array; 41 | 42 | /** 43 | * Returns a set of ServiceDelegateDefinition that determine which services require factories to be constructed. 44 | * 45 | * @return list 46 | */ 47 | public function getServiceDelegateDefinitions() : array; 48 | 49 | /** 50 | * Returns a set of InjectDefinition that determine what values are injected into methods or properties that 51 | * cannot be reliably autowired. 52 | * 53 | * @return list 54 | */ 55 | public function getInjectDefinitions() : array; 56 | 57 | /** 58 | * Returns a set of ConfigurationDefinition that determine what Configuration instances are available in the 59 | * container. 60 | * 61 | * @return list 62 | */ 63 | public function getConfigurationDefinitions() : array; 64 | } 65 | -------------------------------------------------------------------------------- /docs/tutorials/06-injecting-scalar-values.md: -------------------------------------------------------------------------------- 1 | ## Injecting Scalar Values 2 | 3 | There's a good chance there's some object or component of your application that requires scalar values to work. As much as we might add layers of abstraction some things just boil down to a good ol' string or int. Since we can't rightly inject a service for these values there are other mechanisms for injecting non-object values into your services. 4 | 5 | In this example we're going to create a `DatabaseConfiguration` object; these types of objects are ubiquitous, easy to relate to, and requires scalar values. Let's take a look at some code! 6 | 7 | ### Injecting Hard-coded Values 8 | 9 | ```php 10 | host; 24 | } 25 | 26 | public function getPort() : int { 27 | return $this->port; 28 | } 29 | 30 | } 31 | ``` 32 | 33 | In our example we still require values in our constructor like a normal dependency, but we also annotate it with an Attribute called `#[Inject]`. As the name suggests this Attribute will cause the Container to inject whatever value is present into the corresponding parameter. This works fine if you know what the value is ahead of time, but you might need to change the value based on what environment you're running. We've got you covered! 34 | 35 | ### Injecting Environment Variables 36 | 37 | ```php 38 | host; 53 | } 54 | 55 | public function getPort() : int { 56 | return $this->port; 57 | } 58 | 59 | } 60 | ``` 61 | 62 | Notice we've changed the Attribute on our first parameter, `$host`, to include `from: 'env'`. This will cause the Container to inject whatever value is present for the given environment variable _at runtime_. This way you can have your development environment and testing environments use different databases with no changes to the code in order to do so. -------------------------------------------------------------------------------- /src/Internal/CompositeLogger.php: -------------------------------------------------------------------------------- 1 | */ 14 | private readonly array $loggers; 15 | 16 | public function __construct( 17 | LoggerInterface $logger, 18 | LoggerInterface...$additionalLoggers 19 | ) { 20 | $this->loggers = [$logger, ...$additionalLoggers]; 21 | } 22 | 23 | /** 24 | * @return list 25 | */ 26 | public function getLoggers() : array { 27 | return $this->loggers; 28 | } 29 | 30 | public function emergency(Stringable|string $message, array $context = []) : void { 31 | foreach ($this->loggers as $logger) { 32 | $logger->emergency($message, $context); 33 | } 34 | } 35 | 36 | public function alert(Stringable|string $message, array $context = []) : void { 37 | foreach ($this->loggers as $logger) { 38 | $logger->alert($message, $context); 39 | } 40 | } 41 | 42 | public function critical(Stringable|string $message, array $context = []) : void { 43 | foreach ($this->loggers as $logger) { 44 | $logger->critical($message, $context); 45 | } 46 | } 47 | 48 | public function error(Stringable|string $message, array $context = []) : void { 49 | foreach ($this->loggers as $logger) { 50 | $logger->error($message, $context); 51 | } 52 | } 53 | 54 | public function warning(Stringable|string $message, array $context = []) : void { 55 | foreach ($this->loggers as $logger) { 56 | $logger->warning($message, $context); 57 | } 58 | } 59 | 60 | public function notice(Stringable|string $message, array $context = []) : void { 61 | foreach ($this->loggers as $logger) { 62 | $logger->notice($message, $context); 63 | } 64 | } 65 | 66 | public function info(Stringable|string $message, array $context = []) : void { 67 | foreach ($this->loggers as $logger) { 68 | $logger->info($message, $context); 69 | } 70 | } 71 | 72 | public function debug(Stringable|string $message, array $context = []) : void { 73 | foreach ($this->loggers as $logger) { 74 | $logger->debug($message, $context); 75 | } 76 | } 77 | 78 | public function log($level, Stringable|string $message, array $context = []) : void { 79 | foreach ($this->loggers as $logger) { 80 | $logger->log($level, $message, $context); 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /docs/tutorials/10-annotated-container-observers.md: -------------------------------------------------------------------------------- 1 | # Annotated Container Observers 2 | 3 | Annotated Container has a bootstrapping observer system that allows you to get access to pertinent information during the creation of your container. This system could be used to gather information about the static analysis process, perform action on the Container post creation, or some other action that might be necessary. Below we'll talk about how to take advantage of this system to bring more complex functionality to applications powered by Annotated Container. 4 | 5 | ## Registering Observers 6 | 7 | First, you'll need to decide on what type of observer you want to implement. The example will implement all possible interfaces, to show what's possible. You do not have to implement all of them; you can choose any of them to implement. 8 | 9 | ```php 10 | addObserver(new MyContainerObserver()); 51 | $container = $bootstrap->bootstrapContainer(); 52 | ``` 53 | 54 | -------------------------------------------------------------------------------- /src/StaticAnalysis/ContainerDefinitionAnalysisOptionsBuilder.php: -------------------------------------------------------------------------------- 1 | */ 14 | private array $directories = []; 15 | 16 | private ?DefinitionProvider $consumer = null; 17 | 18 | private ?LoggerInterface $logger = null; 19 | 20 | private function __construct() { 21 | } 22 | 23 | /** 24 | * Specify the directories that should be parsed when generating the ContainerDefinition 25 | * 26 | * @param string ...$directories 27 | * @return static 28 | */ 29 | public static function scanDirectories(string...$directories) : self { 30 | $instance = new self(); 31 | $instance->directories = $directories; 32 | return $instance; 33 | } 34 | 35 | /** 36 | * Specify that the ContainerDefinitionBuilder should be modified before the ContainerDefinition is built. 37 | * 38 | * @param DefinitionProvider $consumer 39 | * @return $this 40 | */ 41 | #[SingleEntrypointDefinitionProvider] 42 | public function withDefinitionProvider(DefinitionProvider $consumer) : self { 43 | $instance = clone $this; 44 | $instance->consumer = $consumer; 45 | return $instance; 46 | } 47 | 48 | /** 49 | * @deprecated 50 | */ 51 | public function withLogger(LoggerInterface $logger) : self { 52 | $instance = clone $this; 53 | $instance->logger = $logger; 54 | return $instance; 55 | } 56 | 57 | public function build() : ContainerDefinitionAnalysisOptions { 58 | return new class( 59 | $this->directories, 60 | $this->consumer, 61 | $this->logger 62 | ) implements ContainerDefinitionAnalysisOptions { 63 | public function __construct( 64 | private readonly array $directories, 65 | private readonly ?DefinitionProvider $consumer, 66 | private readonly ?LoggerInterface $logger 67 | ) { 68 | } 69 | 70 | public function getScanDirectories(): array { 71 | return $this->directories; 72 | } 73 | 74 | public function getDefinitionProvider(): ?DefinitionProvider { 75 | return $this->consumer; 76 | } 77 | 78 | public function getLogger() : ?LoggerInterface { 79 | return $this->logger; 80 | } 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /docs/tutorials/09-autowire-aware-invoker.md: -------------------------------------------------------------------------------- 1 | # Autowire Aware Invoker 2 | 3 | It is common in PHP to use callables for a wide variety of functionality. Annotated Container provides functionality to invoke a callable and recursively autowire the dependencies it requires from available services. The Container returned from a `ContainerFactory` is a [type intersect](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.composite.intersection) that includes the `Cspray\AnnotatedContainer\AutowireableInvoker`. You can depend on this type in your constructors to invoke autowired callables! 4 | 5 | ## Example 6 | 7 | In our example we're going to create some callables that interact with `Widget` implementations. Some of those implementations depend on services from the Container, while other implementations depend on scalar values that must be provided when you create the object. Before we look at how to use the `AutowireableInvoker` let's take a look at an example codebase. 8 | 9 | ```php 10 | bootstrapContainer(); 54 | 55 | $someServiceConsumer = function(SomeService $service) {}; 56 | $widgetConsumer = function(Widget $widget) {}; 57 | $serviceAndScalarConsumer = function(SomeServce $service, int $meaning) {}; 58 | 59 | // Will be passed the FooService 60 | $autowiredInvoker->invoke($someServiceConsumer); 61 | 62 | // Will be passed the FooWidget, the Container will automagically instantiate 63 | // an instance of FooWidget and inject its dependencies 64 | $autowiredInvoker->invoke($widgetConsumer, autowiredParams(serviceParam('widget', FooWidget::class))); 65 | 66 | // If the value can't be resolved by the Container you can define just its value 67 | $autowiredInvoker->invoke($serviceAndScalarConsumer, autowiredParams(rawParam('meaning', 42))); 68 | ``` 69 | -------------------------------------------------------------------------------- /src/ContainerFactory/AurynContainerFactoryState.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $nameTypeMap = []; 21 | 22 | 23 | public function __construct( 24 | private readonly ContainerDefinition $containerDefinition 25 | ) { 26 | $this->injector = new Injector(); 27 | } 28 | 29 | 30 | /** 31 | * @param class-string $class 32 | * @param non-empty-string $method 33 | * @param non-empty-string $param 34 | * @return void 35 | * @throws \Auryn\InjectionException 36 | */ 37 | public function addMethodInject(string $class, string $method, string $param, mixed $value) : void { 38 | if ($value instanceof ContainerReference) { 39 | $key = $param; 40 | $nameType = $this->getTypeForName($value->name); 41 | if ($nameType !== null) { 42 | $value = $nameType->getName(); 43 | } else { 44 | $value = $value->name; 45 | } 46 | } elseif ($value instanceof ServiceCollectorReference) { 47 | $key = '+' . $param; 48 | $values = []; 49 | foreach ($this->containerDefinition->getServiceDefinitions() as $serviceDefinition) { 50 | if ($serviceDefinition->isAbstract() || $serviceDefinition->getType()->getName() === $class) { 51 | continue; 52 | } 53 | 54 | if (is_a($serviceDefinition->getType()->getName(), $value->valueType->getName(), true)) { 55 | $values[] = $this->injector->make($serviceDefinition->getType()->getName()); 56 | } 57 | } 58 | 59 | $value = static fn() => $value->listOf->toCollection($values); 60 | } else { 61 | $key = ':' . $param; 62 | } 63 | 64 | $this->addResolvedMethodInject($class, $method, $key, $value); 65 | } 66 | 67 | /** 68 | * @param non-empty-string $name 69 | * @param ObjectType $type 70 | * @return void 71 | */ 72 | public function addNameType(string $name, ObjectType $type) : void { 73 | $this->nameTypeMap[$name] = $type; 74 | } 75 | 76 | /** 77 | * @param non-empty-string $name 78 | * @return ObjectType|null 79 | */ 80 | public function getTypeForName(string $name) : ?ObjectType { 81 | return $this->nameTypeMap[$name] ?? null; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cspray/annotated-container", 3 | "description": "Create Dependency Injection containers configured with PHP8 Attributes.", 4 | "keywords": ["dependency injection", "container", "attributes"], 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Charles Sprayberry", 9 | "email": "771345+cspray@users.noreply.github.com", 10 | "homepage": "https://cspray.io", 11 | "role": "Project Maintainer" 12 | } 13 | ], 14 | "require": { 15 | "php": "^8.1", 16 | "ext-dom": "*", 17 | "ext-libxml": "*", 18 | "composer-runtime-api": "^2", 19 | "brick/varexporter": "^0.3.7", 20 | "cspray/annotated-container-adr": "^3", 21 | "cspray/annotated-container-attribute": "^1.3", 22 | "cspray/annotated-target": "^v0.3", 23 | "cspray/precision-stopwatch": "^0.2.0", 24 | "cspray/typiphy": "^0.3", 25 | "nikic/php-parser": "^4.10", 26 | "ocramius/package-versions": "^2.7", 27 | "psr/container": "^2.0", 28 | "psr/log": "^2 || ^3" 29 | }, 30 | "require-dev": { 31 | "illuminate/container": "^10.11", 32 | "jetbrains/phpstorm-attributes": "^1.1", 33 | "php-di/php-di": "^7.0", 34 | "rdlowrey/auryn": "^1.4", 35 | "roave/security-advisories": "dev-latest" 36 | }, 37 | "bin": ["bin/annotated-container"], 38 | "autoload": { 39 | "psr-4": { 40 | "Cspray\\AnnotatedContainer\\": "src" 41 | }, 42 | "files": [ 43 | "src/Function/auto-wired-parameters.php", 44 | "src/Function/definitions.php" 45 | ] 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "Cspray\\AnnotatedContainer\\": "test", 50 | "Cspray\\AnnotatedContainerFixture\\": "fixture_src" 51 | }, 52 | "files": [ 53 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/other_src/DependencyDefinitionProvider.php", 54 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/src/DependencyObserver.php", 55 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/src/FirstInitializer.php", 56 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/src/SecondInitializer.php", 57 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/src/SomeService.php", 58 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/other_src/ThirdInitializer.php", 59 | "fixture_src/VendorScanningInitializers/vendor/cspray/package/src/ThirdPartyDependency.php", 60 | "fixture_src/VendorScanningInitializers/src/ActualService.php" 61 | ] 62 | }, 63 | "suggest": { 64 | "illuminate/container": "Install to use Illuminate\\Container\\Container as the backing container.", 65 | "php-di/php-di": "Install 7.0+ to use DI\\Container as the backing container.", 66 | "rdlowrey/auryn": "Install to use Auryn\\Injector as the backing container." 67 | }, 68 | "extra": { 69 | "$architecturalDecision": { 70 | "initializers": [ 71 | "Cspray\\AnnotatedContainer\\ArchitecturalDecisions\\Initializer" 72 | ] 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/references/02-functional-api.md: -------------------------------------------------------------------------------- 1 | # Functional API 2 | 3 | Annotated Container provides a series of functions that are designed to: 4 | 5 | - Provide a mechanism for defining services that can't be annotated 6 | - Define a standardized way for passing arguments to `AutowireableFactory` and `AutowireableInvoker`. 7 | 8 | This document lists the functions for each purpose. 9 | 10 | ## Defining Services 11 | 12 | ```php 13 | \Cspray\AnnotatedContainer\service( 14 | \Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext $context, 15 | \Cspray\Typiphy\ObjectType $service, 16 | ?string $name = null, 17 | array $profiles = [], 18 | bool $isPrimary = false 19 | ) : \Cspray\AnnotatedContainer\Definition\ServiceDefinition; 20 | 21 | \Cspray\AnnotatedContainer\alias( 22 | \Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext $context, 23 | \Cspray\Typiphy\ObjectType $abstract, 24 | \Cspray\Typiphy\ObjectType $concrete 25 | ) : \Cspray\AnnotatedContainer\Definition\AliasDefinition; 26 | 27 | \Cspray\AnnotatedContainer\serviceDelegate( 28 | \Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext $context, 29 | \Cspray\Typiphy\ObjectType $service, 30 | \Cspray\Typiphy\ObjectType $factoryClass, 31 | string $factoryMethod 32 | ) : \Cspray\AnnotatedContainer\Definition\ServiceDelegateDefinition; 33 | 34 | \Cspray\AnnotatedContainer\servicePrepare( 35 | \Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext $context, 36 | \Cspray\Typiphy\ObjectType $service, 37 | string $method 38 | ) : \Cspray\AnnotatedContainer\Definition\ServicePrepareDefinition; 39 | 40 | \Cspray\AnnotatedContainer\injectMethodParam( 41 | \Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext $context, 42 | \Cspray\Typiphy\ObjectType $service, 43 | string $method, 44 | string $paramName, 45 | \Cspray\Typiphy\Type|\Cspray\Typiphy\TypeUnion|\Cspray\Typiphy\TypeIntersect $type, 46 | mixed $value, 47 | array $profiles = [], 48 | string $from = null 49 | ) : \Cspray\AnnotatedContainer\Definition\InjectDefinition; 50 | 51 | \Cspray\AnnotatedContainer\injectProperty( 52 | \Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext $context, 53 | \Cspray\Typiphy\ObjectType $service, 54 | string $property, 55 | \Cspray\Typiphy\Type|\Cspray\Typiphy\TypeUnion|\Cspray\Typiphy\TypeIntersect $type, 56 | mixed $value, 57 | array $profiles = [], 58 | string $from = null 59 | ) : \Cspray\AnnotatedContainer\Definition\InjectDefinition; 60 | ``` 61 | 62 | ## Autowireable Parameters 63 | 64 | ```php 65 | \Cspray\AnnotatedContainer\autowiredParams( 66 | \Cspray\AnnotatedContainer\AutowireableParameter... $parameters 67 | ) : \Cspray\AnnotatedContainer\AutowireableParameterSet; 68 | 69 | \Cspray\AnnotatedContainer\serviceParam( 70 | string $name, 71 | \Cspray\Typiphy\ObjectType $service 72 | ) : \Cspray\AnnotatedContainer\AutowireableParameter; 73 | 74 | \Cspray\AnnotatedContainer\rawParam( 75 | string $name, 76 | mixed $value 77 | ) : \Cspray\AnnotatedContainer\AutowireableParameter; 78 | ``` 79 | -------------------------------------------------------------------------------- /docs/tutorials/08-autowire-aware-factory.md: -------------------------------------------------------------------------------- 1 | # Autowire Aware Factory 2 | 3 | Sometimes you might want to take advantage of the autowiring capabilities of Annotated Container when you're creating an object that isn't a Service. The Container returned from a `ContainerFactory` is a [type intersect](https://www.php.net/manual/en/language.types.declarations.php#language.types.declarations.composite.intersection) that includes the `Cspray\AnnotatedContainer\AutowireableFactory` interface. You can depend on this type in your factory constructors to create autowired objects! 4 | 5 | ## Example 6 | 7 | In our example we're going to create a `WidgetFactory` that creates `Widget` implementations. Some of those implementations depend on services from the Container, while other implementations depend on scalar values that must be provided when you create the object. Before we look at how to use the `AutowireableFactory` let's take a look at an example codebase. 8 | 9 | ```php 10 | bootstrapContainer(); 52 | 53 | $fooWidget = $autowiredFactory->make(FooWidget::class); 54 | $fooWidget->service instanceof FooService; // true 55 | 56 | $barWidget = $autowiredFactory->make(BarWidget::class, autowiredParams(rawParam('foo', 'bar'))); 57 | $barWidget->service instanceof BarService; // true 58 | $barWidget->foo === 'bar'; // true 59 | ``` 60 | 61 | ## Making Defined Services 62 | 63 | It should be noted that using the `AutowireableFactory` interface to create defined Services is not recommended. Making a shared service won't recreate the instance the way you'd might expect. Which means if you call `AutowireableFactory::make` and attempt to override the parameters that are used it will still use what is defined in the Container. If you have defined the class in the Container you should use the `ContainerInterface::get()` method to retrieve it. 64 | 65 | 66 | -------------------------------------------------------------------------------- /src/LogicalConstraint/Check/MultiplePrimaryForAbstractService.php: -------------------------------------------------------------------------------- 1 | getAbstractServices($containerDefinition) as $abstract) { 20 | $abstractService = $abstract->getType()->getName(); 21 | $abstractPrimaryMap[$abstractService] ??= []; 22 | $concreteServices = $this->getConcreteServicesInstanceOf($containerDefinition, $abstract); 23 | foreach ($concreteServices as $concrete) { 24 | if ($concrete->isPrimary()) { 25 | $abstractPrimaryMap[$abstractService][] = $concrete->getType()->getName() . PHP_EOL; 26 | } 27 | } 28 | } 29 | 30 | foreach ($abstractPrimaryMap as $abstract => $concreteServices) { 31 | if (count($concreteServices) > 1) { 32 | sort($concreteServices); 33 | $types = trim(implode('- ', $concreteServices)); 34 | $message = <<add( 44 | LogicalConstraintViolation::warning($message) 45 | ); 46 | } 47 | } 48 | 49 | return $violations; 50 | } 51 | 52 | /** 53 | * @return Generator 54 | */ 55 | private function getAbstractServices(ContainerDefinition $containerDefinition) : Generator { 56 | foreach ($containerDefinition->getServiceDefinitions() as $serviceDefinition) { 57 | if ($serviceDefinition->isAbstract()) { 58 | yield $serviceDefinition; 59 | } 60 | } 61 | } 62 | 63 | /** 64 | * @return Generator 65 | */ 66 | private function getConcreteServicesInstanceOf(ContainerDefinition $containerDefinition, ServiceDefinition $serviceDefinition) : Generator { 67 | foreach ($containerDefinition->getServiceDefinitions() as $service) { 68 | if ($service->isConcrete()) { 69 | if (is_subclass_of($service->getType()->getName(), $serviceDefinition->getType()->getName())) { 70 | yield $service; 71 | } 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Cli/InputParser.php: -------------------------------------------------------------------------------- 1 | $argv 11 | * @return Input 12 | */ 13 | public function parse(array $argv) : Input { 14 | array_shift($argv); 15 | $options = []; 16 | /** @var list $arguments */ 17 | $arguments = []; 18 | 19 | $handleOption = function(string $arg) use(&$options) : void { 20 | assert(is_array($options)); 21 | if (str_contains($arg, '=')) { 22 | [$opt, $val] = explode('=', $arg); 23 | } else { 24 | $opt = $arg; 25 | $val = true; 26 | } 27 | $opt = str_replace('--', '', $opt); 28 | 29 | if (isset($options[$opt])) { 30 | $optVal = $val; 31 | if (!is_array($options[$opt])) { 32 | $val = [$options[$opt]]; 33 | } else { 34 | $val = $options[$opt]; 35 | } 36 | $val[] = $optVal; 37 | } 38 | $options[$opt] = $val; 39 | }; 40 | 41 | foreach ($argv as $arg) { 42 | if (str_starts_with($arg, '--')) { 43 | $handleOption($arg); 44 | } elseif (str_starts_with($arg, '-')) { 45 | if (str_contains($arg, '=')) { 46 | $handleOption('-' . $arg); 47 | } else { 48 | $arg = str_replace('-', '', $arg); 49 | $shortOpts = str_split($arg); 50 | foreach ($shortOpts as $shortOpt) { 51 | $handleOption('--' . $shortOpt); 52 | } 53 | } 54 | } else { 55 | $arguments[] = $arg; 56 | } 57 | } 58 | 59 | return new class($options, $arguments) implements Input { 60 | 61 | /** 62 | * @param array|string|bool> $options 63 | * @param list $args 64 | */ 65 | public function __construct( 66 | private readonly array $options, 67 | private readonly array $args 68 | ) { 69 | } 70 | 71 | /** 72 | * @return array|string|bool> 73 | */ 74 | public function getOptions() : array { 75 | return $this->options; 76 | } 77 | 78 | public function getArguments() : array { 79 | return $this->args; 80 | } 81 | 82 | public function getOption(string $opt) : array|string|bool|null { 83 | return $this->options[$opt] ?? null; 84 | } 85 | 86 | public function requireOption(string $opt) : array|string|bool { 87 | if (!isset($this->options[$opt])) { 88 | throw new OptionNotFound(sprintf('The option "%s" was not provided.', $opt)); 89 | } 90 | 91 | return $this->options[$opt]; 92 | } 93 | }; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docs/how-to/01-add-third-party-services.md: -------------------------------------------------------------------------------- 1 | # Adding Third-Party Services 2 | 3 | It is very likely that you'll need to add some service to the Container that can't be annotated. AnnotatedContainer offers a set of functions to easily add third-party services with all the feature-parity and functionality available to annotated code. This guide goes through a step-by-step guide on how to integrate the popular [Monolog](https://github.com/Seldaek/monolog) library with [PSR-3](https://www.php-fig.org/psr/psr-3/) services. 4 | 5 | > This guide assumes a basic understanding on how to interact with this library. If you're unsure of something we discuss here it is recommended you checkout the rest of the /docs/tutorials section. 6 | 7 | ## Step 1 - Install PSR-3 and Monolog 8 | 9 | ```shell 10 | composer require monolog/monolog psr/log 11 | ``` 12 | 13 | ## Step 2 - Create a Service Factory 14 | 15 | ```php 16 | pushHandler(new StreamHandler('php://stdout')); 28 | 29 | return $log; 30 | } 31 | 32 | } 33 | ``` 34 | 35 | ## Step 3 - Define a DefinitionProvider 36 | 37 | ```php 38 | 66 | 67 | 68 | 69 | src 70 | tests 71 | 72 | 73 | .annotated-container-cache 74 | 75 | ThirdPartyServicesProvider 76 | 77 | 78 | ``` 79 | 80 | ## Step 5 - Bootstrap your Container 81 | 82 | ```php 83 | bootstrapContainer(); 89 | ``` 90 | 91 | Now, your PSR Logger will be created through a factory. Any services can inject a `LoggerInterface` directly in the constructor, preferred, or implement the `LoggerAwareInterface` to have it injected automatically after construction. -------------------------------------------------------------------------------- /src/ContainerFactory/IlluminateContainerFactoryState.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | private array $delegates = []; 19 | 20 | /** 21 | * @var array 22 | */ 23 | private array $concreteServices = []; 24 | 25 | /** 26 | * @var array 27 | */ 28 | private array $abstractServices = []; 29 | 30 | /** 31 | * @var array 32 | */ 33 | private array $aliases = []; 34 | 35 | /** 36 | * @var array 37 | */ 38 | private array $namedServices = []; 39 | 40 | public function __construct( 41 | public readonly Container $container, 42 | public readonly ContainerDefinition $containerDefinition 43 | ) { 44 | } 45 | 46 | /** 47 | * @param class-string $service 48 | * @param class-string $delegate 49 | * @param non-empty-string $method 50 | * @return void 51 | */ 52 | public function addStaticDelegate(string $service, string $delegate, string $method) : void { 53 | $this->delegates[$service] = [ 54 | 'delegateType' => $delegate, 55 | 'delegateMethod' => $method, 56 | 'isStatic' => true 57 | ]; 58 | } 59 | 60 | public function addInstanceDelegate(string $service, string $delegate, string $method) : void { 61 | $this->delegates[$service] = [ 62 | 'delegateType' => $delegate, 63 | 'delegateMethod' => $method, 64 | 'isStatic' => false 65 | ]; 66 | } 67 | 68 | public function addAbstractService(string $service) : void { 69 | $this->abstractServices[] = $service; 70 | } 71 | 72 | public function addConcreteService(string $service) : void { 73 | $this->concreteServices[] = $service; 74 | } 75 | 76 | public function addNamedService(string $service, string $name) : void { 77 | $this->namedServices[$service] = $name; 78 | } 79 | 80 | public function addAlias(string $abstract, string $concrete) : void { 81 | $this->aliases[$abstract] = $concrete; 82 | } 83 | 84 | public function getAbstractServices() : array { 85 | return $this->abstractServices; 86 | } 87 | 88 | public function getConcreteServices() : array { 89 | return $this->concreteServices; 90 | } 91 | 92 | public function getAliases() : array { 93 | return $this->aliases; 94 | } 95 | 96 | /** 97 | * @return array 98 | */ 99 | public function getDelegates() : array { 100 | return $this->delegates; 101 | } 102 | 103 | public function getNamedServices() : array { 104 | return $this->namedServices; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/Cli/CommandExecutor.php: -------------------------------------------------------------------------------- 1 | defaultCommand = $command->getName(); 18 | $this->addCommand($command); 19 | } 20 | 21 | public function getCommand(string $name) : ?Command { 22 | return array_reduce($this->commands, fn(?Command $carry, Command $item) => $item->getName() === $name ? $item : $carry); 23 | } 24 | 25 | public function addCommand(Command $command) : void { 26 | $this->commands[$command->getName()] = $command; 27 | } 28 | 29 | public function execute(Input $input, TerminalOutput $output) : int { 30 | $noArgs = empty($input->getArguments()); 31 | $exitCode = null; 32 | if ($noArgs && !isset($this->defaultCommand)) { 33 | $this->notFoundCommand($output); 34 | } else { 35 | $command = $noArgs ? $this->defaultCommand : $input->getArguments()[0]; 36 | if (!isset($this->commands[$command])) { 37 | $this->notFoundCommand($output, $command); 38 | } else { 39 | try { 40 | $exitCode = $this->commands[$command]->handle($input, $output); 41 | } catch (Throwable $throwable) { 42 | $exitCode = (int) $throwable->getCode(); 43 | // If using the default exception code then we need to make sure we don't return a success code 44 | if ($exitCode === 0) { 45 | $exitCode = 1; 46 | } 47 | $output->stderr->write(sprintf('Unhandled exception executing "%s"!', $command)); 48 | $output->stderr->br(); 49 | ; 50 | $output->stderr->write(sprintf('Type: %s', $throwable::class)); 51 | $output->stderr->write(sprintf('Message: %s', $throwable->getMessage())); 52 | $output->stderr->write(sprintf('Location: %sL#%s', $throwable->getFile(), $throwable->getLine())); 53 | $output->stderr->write('Stack Trace:'); 54 | $output->stderr->br(); 55 | $output->stderr->write($throwable->getTraceAsString()); 56 | } 57 | } 58 | } 59 | 60 | return $exitCode ?? 1; 61 | } 62 | 63 | private function notFoundCommand(TerminalOutput $output, string $command = null) : void { 64 | if (isset($command)) { 65 | $output->stderr->write(sprintf('Unable to find command "%s"!', $command)); 66 | } else { 67 | $output->stderr->write('Unable to find command to execute!'); 68 | } 69 | $output->stderr->br(); 70 | $output->stderr->write('Available Commands:'); 71 | $output->stderr->br(); 72 | 73 | if (empty($this->commands)) { 74 | $output->stderr->write("\tNo Commands Found"); 75 | } else { 76 | $names = array_keys($this->commands); 77 | sort($names); 78 | foreach ($names as $name) { 79 | $output->stderr->write(sprintf("\t%s", $name)); 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /docs/tutorials/05-calling-post-construct-methods.md: -------------------------------------------------------------------------------- 1 | ## Calling Post Construct Methods 2 | 3 | Sometimes you might not need the full power, and complexity, of having a service factory, but you do need to have some method called after the object is constructed. AnnotatedContainer can help you _prepare_ your Services by automatically calling these methods and injecting services and values into them. 4 | 5 | We're going to use the same example from `/docs/tutorials/01-getting-started.md` and change from using constructor injection to setter injection. For clarity, here's the code from that tutorial with some slight modifications to change injection types. 6 | 7 | ```php 8 | storeListeners[] = $listener; 45 | } 46 | 47 | public function onRetrieve(callable $listener) : void { 48 | $this->retrieveListeners[] = $listener; 49 | } 50 | 51 | public function emitStoreEvent(string $identifier) : void { 52 | foreach ($this->storeListeners as $storeListener) { 53 | $storeListener($identifier); 54 | } 55 | } 56 | 57 | public function emitRetrieveEvent(string $identifier) : void { 58 | foreach ($this->retrieveListeners as $retrieveListener) { 59 | $retrieveListener($identifier); 60 | } 61 | } 62 | 63 | } 64 | 65 | #[Service] 66 | class FilesystemStorage implements BlobStorage { 67 | private BlobStorageEventEmitter $emitter; 68 | 69 | public function store(string $identifier, string $contents) : void { 70 | file_put_contents($identifier, $contents); 71 | $this->emitter->emitStoreEvent($identifier); 72 | } 73 | 74 | public function retrieve(string $identifier) : ?string { 75 | $contents = file_get_contents($identifier) ?? null; 76 | $this->emitter->emitRetrieveEvent($identifier); 77 | return $contents; 78 | } 79 | 80 | #[ServicePrepare] 81 | public function setEmitter(BlobStorageEventEmitter $emitter) { 82 | $this->emitter = $emitter; 83 | } 84 | 85 | } 86 | ``` 87 | 88 | Note the new method, `FilesystemStorage::setEmitter`. It has an Attribute on it called `#[ServicePrepare]`; this tells AnnotatedContainer that you want to invoke this method immediately after construction. Also note that we declared the`BlobStorageEventEmitter` service as a dependency. Any service or value the container can inject you can declare in your method's arguments. -------------------------------------------------------------------------------- /src/Cli/TerminalOutput.php: -------------------------------------------------------------------------------- 1 | stdout = $this->getDecoratedOutput($stdout ?? new Stdout()); 14 | $this->stderr = $this->getDecoratedOutput($stderr ?? new Stderr()); 15 | } 16 | 17 | private function getDecoratedOutput(Output $output) : OutputWithHelpers { 18 | return new class($output) implements Output, OutputWithHelpers { 19 | 20 | /** 21 | * @var array 22 | */ 23 | private const FORMATS = [ 24 | 'bold' => ['open' => '1', 'close' => '22'], 25 | 'em' => ['open' => '3', 'close' => '23'], 26 | 'underline' => ['open' => '4', 'close' => '24'], 27 | 'dim' => ['open' => '2', 'close' => '22'], 28 | 'del' => ['open' => '9', 'close' => '29'], 29 | 30 | 'fg:black' => ['open' => '30', 'close' => '0'], 31 | 'bg:black' => ['open' => '40', 'close' => '0'], 32 | 'fg:red' => ['open' => '31', 'close' => '0'], 33 | 'bg:red' => ['open' => '41', 'close' => '0'], 34 | 'fg:green' => ['open' => '32', 'close' => '0'], 35 | 'bg:green' => ['open' => '42', 'close' => '0'], 36 | 'fg:yellow' => ['open' => '33', 'close' => '0'], 37 | 'bg:yellow' => ['open' => '43', 'close' => '0'], 38 | 'fg:blue' => ['open' => '34', 'close' => '0'], 39 | 'bg:blue' => ['open' => '44', 'close' => '0'], 40 | 'fg:magenta' => ['open' => '35', 'close' => '0'], 41 | 'bg:magenta' => ['open' => '45', 'close' => '0'], 42 | 'fg:cyan' => ['open' => '36', 'close' => '0'], 43 | 'bg:cyan' => ['open' => '46', 'close' => '0'], 44 | 'fg:white' => ['open' => '37', 'close' => '0'], 45 | 'bg:white' => ['open' => '47', 'close' => '0'] 46 | ]; 47 | 48 | private readonly array $tagCodes; 49 | 50 | public function __construct(private readonly Output $output) { 51 | $tags = []; 52 | $codes = []; 53 | foreach (self::FORMATS as $tag => $code) { 54 | $openTag = sprintf('<%s>', $tag); 55 | $closeTag = sprintf('', $tag); 56 | $openCode = sprintf("\033[%sm", $code['open']); 57 | $exitCode = sprintf("\033[%sm", $code['close']); 58 | 59 | $tags[] = $openTag; 60 | $tags[] = $closeTag; 61 | $codes[] = $openCode; 62 | $codes[] = $exitCode; 63 | } 64 | $this->tagCodes = [ 65 | 'tags' => $tags, 66 | 'codes' => $codes 67 | ]; 68 | } 69 | 70 | public function write(string|Stringable $msg, bool $appendNewLine = true) : void { 71 | $msg = str_replace($this->tagCodes['tags'], $this->tagCodes['codes'], (string) $msg); 72 | $this->output->write($msg, $appendNewLine); 73 | } 74 | 75 | public function br() : void { 76 | $this->output->write(PHP_EOL, false); 77 | } 78 | }; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/StaticAnalysis/CacheAwareContainerDefinitionAnalyzer.php: -------------------------------------------------------------------------------- 1 | getCacheFile($containerDefinitionCompileOptions->getScanDirectories()); 39 | if (is_file($cacheFile)) { 40 | $containerDefinition = $this->containerDefinitionSerializer->deserialize(file_get_contents($cacheFile)); 41 | if ($containerDefinition instanceof ContainerDefinition) { 42 | $logger = $containerDefinitionCompileOptions->getLogger(); 43 | if ($logger !== null) { 44 | $logger->info(sprintf( 45 | 'Skipping Annotated Container compiling. Using cached definition from %s.', 46 | $cacheFile 47 | )); 48 | } 49 | return $containerDefinition; 50 | } 51 | } 52 | 53 | $containerDefinition = $this->containerDefinitionCompiler->analyze($containerDefinitionCompileOptions); 54 | $serialized = $this->containerDefinitionSerializer->serialize($containerDefinition); 55 | $contentWritten = @file_put_contents($cacheFile, $serialized); 56 | if (!$contentWritten) { 57 | throw InvalidCache::fromUnwritableDirectory($this->cacheDir); 58 | } 59 | return $containerDefinition; 60 | } 61 | 62 | private function getCacheFile(array $dirs) : string { 63 | sort($dirs); 64 | return sprintf( 65 | '%s/%s', 66 | $this->cacheDir, 67 | md5(join($dirs)) 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Definition/ProfilesAwareContainerDefinition.php: -------------------------------------------------------------------------------- 1 | containerDefinition->getServiceDefinitions() as $serviceDefinition) { 19 | if ($this->hasActiveProfile($serviceDefinition)) { 20 | $filtered[] = $serviceDefinition; 21 | } 22 | } 23 | 24 | return $filtered; 25 | } 26 | 27 | public function getAliasDefinitions() : array { 28 | $filtered = []; 29 | foreach ($this->containerDefinition->getAliasDefinitions() as $aliasDefinition) { 30 | $abstract = $this->getServiceDefinition($aliasDefinition->getAbstractService()); 31 | if ($abstract === null) { 32 | throw InvalidAlias::fromAbstractNotService($aliasDefinition->getAbstractService()->getName()); 33 | } 34 | 35 | $concrete = $this->getServiceDefinition($aliasDefinition->getConcreteService()) ?? $this->getConfigurationDefinition($aliasDefinition->getConcreteService()); 36 | if ($concrete === null) { 37 | throw InvalidAlias::fromConcreteNotService($aliasDefinition->getConcreteService()->getName()); 38 | } 39 | 40 | if ($this->hasActiveProfile($abstract) && $this->hasActiveProfile($concrete)) { 41 | $filtered[] = $aliasDefinition; 42 | } 43 | } 44 | return $filtered; 45 | } 46 | 47 | public function getServicePrepareDefinitions() : array { 48 | return $this->containerDefinition->getServicePrepareDefinitions(); 49 | } 50 | 51 | public function getServiceDelegateDefinitions() : array { 52 | return $this->containerDefinition->getServiceDelegateDefinitions(); 53 | } 54 | 55 | public function getInjectDefinitions() : array { 56 | $filtered = []; 57 | foreach ($this->containerDefinition->getInjectDefinitions() as $injectDefinition) { 58 | if ($this->hasActiveProfile($injectDefinition)) { 59 | $filtered[] = $injectDefinition; 60 | } 61 | } 62 | return $filtered; 63 | } 64 | 65 | public function getConfigurationDefinitions() : array { 66 | return $this->containerDefinition->getConfigurationDefinitions(); 67 | } 68 | 69 | private function getServiceDefinition(ObjectType $objectType) : ?ServiceDefinition { 70 | foreach ($this->containerDefinition->getServiceDefinitions() as $serviceDefinition) { 71 | if ($serviceDefinition->getType() === $objectType) { 72 | return $serviceDefinition; 73 | } 74 | } 75 | 76 | return null; 77 | } 78 | 79 | private function hasActiveProfile(ServiceDefinition|InjectDefinition|ConfigurationDefinition $definition) : bool { 80 | if ($definition instanceof ConfigurationDefinition) { 81 | return true; 82 | } 83 | 84 | return count(array_intersect($this->activeProfiles, $definition->getProfiles())) >= 1; 85 | } 86 | 87 | private function getConfigurationDefinition(ObjectType $objectType) : ?ConfigurationDefinition { 88 | foreach ($this->getConfigurationDefinitions() as $configurationDefinition) { 89 | if ($configurationDefinition->getClass() === $objectType) { 90 | return $configurationDefinition; 91 | } 92 | } 93 | 94 | return null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/ContainerFactory/PhpDiContainerFactoryState.php: -------------------------------------------------------------------------------- 1 | services[] = AutowireableFactory::class; 33 | $this->services[] = AutowireableInvoker::class; 34 | $this->services[] = ActiveProfiles::class; 35 | } 36 | 37 | /** 38 | * @param class-string $class 39 | * @param non-empty-string $method 40 | * @param non-empty-string $param 41 | * @return void 42 | */ 43 | public function addMethodInject(string $class, string $method, string $param, mixed $value) : void { 44 | if ($value instanceof ContainerReference) { 45 | $value = get($value->name); 46 | } 47 | 48 | if ($value instanceof ServiceCollectorReference) { 49 | $values = []; 50 | foreach ($this->containerDefinition->getServiceDefinitions() as $serviceDefinition) { 51 | if ($serviceDefinition->isAbstract() || $serviceDefinition->getType()->getName() === $class) { 52 | continue; 53 | } 54 | 55 | if (is_a($serviceDefinition->getType()->getName(), $value->valueType->getName(), true)) { 56 | $values[] = get($serviceDefinition->getType()->getName()); 57 | } 58 | } 59 | 60 | $value = factory(function(Container $container) use ($values, $value) { 61 | $resolvedValues = []; 62 | /** @var Reference $val */ 63 | foreach ($values as $val) { 64 | $resolvedValues[] = $val->resolve($container); 65 | } 66 | return $value->listOf->toCollection($resolvedValues); 67 | }); 68 | } 69 | 70 | $this->addResolvedMethodInject($class, $method, $param, $value); 71 | } 72 | 73 | public function getDefinitions() : array { 74 | return $this->definitions; 75 | } 76 | 77 | public function getServices() : array { 78 | return $this->services; 79 | } 80 | 81 | public function addService(string $service) : void { 82 | $this->services[] = $service; 83 | } 84 | 85 | public function autowireService(string $service) : void { 86 | $this->definitions[$service] = autowire(); 87 | } 88 | 89 | public function referenceService(string $name, string $service) : void { 90 | $this->definitions[$name] = get($service); 91 | } 92 | 93 | public function factoryService(string $name, \Closure $closure) : void { 94 | $this->definitions[$name] = $closure; 95 | } 96 | 97 | public function setServiceKey(string $serviceType, string $key) : void { 98 | $this->serviceKeys[$serviceType] = $key; 99 | } 100 | 101 | public function getServiceKey(string $serviceType) : ?string { 102 | return $this->serviceKeys[$serviceType] ?? null; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/ContainerFactory/AliasResolution/StandardAliasDefinitionResolver.php: -------------------------------------------------------------------------------- 1 | isServiceDelegate($containerDefinition, $abstractService)) { 17 | $definition = null; 18 | $reason = AliasResolutionReason::ServiceIsDelegated; 19 | } else { 20 | $aliases = []; 21 | foreach ($containerDefinition->getAliasDefinitions() as $aliasDefinition) { 22 | if ($aliasDefinition->getAbstractService()->getName() === $abstractService->getName()) { 23 | $aliases[] = $aliasDefinition; 24 | } 25 | } 26 | 27 | if (count($aliases) === 1) { 28 | $definition = $aliases[0]; 29 | $reason = AliasResolutionReason::SingleConcreteService; 30 | } elseif (count($aliases) > 1) { 31 | $definition = null; 32 | $primaryAliases = []; 33 | foreach ($aliases as $alias) { 34 | $concreteDefinition = $this->getServiceDefinition($containerDefinition, $alias->getConcreteService()); 35 | if ($concreteDefinition?->isPrimary()) { 36 | $primaryAliases[] = $alias; 37 | } 38 | } 39 | 40 | if (count($primaryAliases) === 1) { 41 | $definition = $primaryAliases[0]; 42 | $reason = AliasResolutionReason::ConcreteServiceIsPrimary; 43 | } elseif (count($primaryAliases) === 0) { 44 | $reason = AliasResolutionReason::MultipleConcreteService; 45 | } else { 46 | $reason = AliasResolutionReason::MultiplePrimaryService; 47 | } 48 | } else { 49 | $definition = null; 50 | $reason = AliasResolutionReason::NoConcreteService; 51 | } 52 | } 53 | 54 | return new class($reason, $definition) implements AliasDefinitionResolution { 55 | 56 | public function __construct( 57 | private readonly AliasResolutionReason $reason, 58 | private readonly ?AliasDefinition $definition 59 | ) { 60 | } 61 | 62 | public function getAliasResolutionReason() : AliasResolutionReason { 63 | return $this->reason; 64 | } 65 | 66 | public function getAliasDefinition() : ?AliasDefinition { 67 | return $this->definition; 68 | } 69 | }; 70 | } 71 | 72 | private function getServiceDefinition(ContainerDefinition $containerDefinition, ObjectType $objectType) : ?ServiceDefinition { 73 | foreach ($containerDefinition->getServiceDefinitions() as $serviceDefinition) { 74 | if ($serviceDefinition->getType()->getName() === $objectType->getName()) { 75 | return $serviceDefinition; 76 | } 77 | } 78 | 79 | return null; 80 | } 81 | 82 | private function isServiceDelegate(ContainerDefinition $containerDefinition, ObjectType $service) : bool { 83 | foreach ($containerDefinition->getServiceDelegateDefinitions() as $serviceDelegateDefinition) { 84 | if ($serviceDelegateDefinition->getServiceType()->getName() === $service->getName()) { 85 | return true; 86 | } 87 | } 88 | 89 | return false; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Definition/ServiceDefinitionBuilder.php: -------------------------------------------------------------------------------- 1 | 19 | */ 20 | private array $profiles = []; 21 | private bool $isPrimary = false; 22 | 23 | private function __construct() { 24 | } 25 | 26 | public static function forAbstract(ObjectType $type) : self { 27 | $instance = new self; 28 | $instance->type = $type; 29 | $instance->isAbstract = true; 30 | return $instance; 31 | } 32 | 33 | public static function forConcrete(ObjectType $type, bool $isPrimary = false) : self { 34 | $instance = new self; 35 | $instance->type = $type; 36 | $instance->isAbstract = false; 37 | $instance->isPrimary = $isPrimary; 38 | return $instance; 39 | } 40 | 41 | /** 42 | * @param non-empty-string $name 43 | * @return $this 44 | */ 45 | public function withName(string $name) : self { 46 | $instance = clone $this; 47 | $instance->name = $name; 48 | return $instance; 49 | } 50 | 51 | /** 52 | * @param list $profiles 53 | * @return $this 54 | */ 55 | public function withProfiles(array $profiles) : self { 56 | $instance = clone $this; 57 | $instance->profiles = $profiles; 58 | return $instance; 59 | } 60 | 61 | public function withAttribute(ServiceAttribute $attribute) : self { 62 | $instance = clone $this; 63 | $instance->attribute = $attribute; 64 | return $instance; 65 | } 66 | 67 | public function build() : ServiceDefinition { 68 | $profiles = $this->profiles; 69 | if (empty($profiles)) { 70 | $profiles[] = 'default'; 71 | } 72 | return new class($this->name, $this->type, $this->isAbstract, $profiles, $this->isPrimary, $this->attribute) implements ServiceDefinition { 73 | 74 | /** 75 | * @param ?non-empty-string $name 76 | * @param list $profiles 77 | */ 78 | public function __construct( 79 | private readonly ?string $name, 80 | private readonly ObjectType $type, 81 | private readonly bool $isAbstract, 82 | private readonly array $profiles, 83 | private readonly bool $isPrimary, 84 | private readonly ?ServiceAttribute $attribute 85 | ) { 86 | } 87 | 88 | /** 89 | * @return ?non-empty-string 90 | */ 91 | public function getName() : ?string { 92 | return $this->name; 93 | } 94 | 95 | public function getType() : ObjectType { 96 | return $this->type; 97 | } 98 | 99 | /** 100 | * @return list 101 | */ 102 | public function getProfiles() : array { 103 | return $this->profiles; 104 | } 105 | 106 | public function isPrimary() : bool { 107 | return $this->isPrimary; 108 | } 109 | 110 | public function isConcrete() : bool { 111 | return !$this->isAbstract; 112 | } 113 | 114 | public function isAbstract() : bool { 115 | return $this->isAbstract; 116 | } 117 | 118 | public function getAttribute() : ?ServiceAttribute { 119 | return $this->attribute; 120 | } 121 | }; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/Profiles/ActiveProfilesBuilder.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | private array $profiles = []; 19 | 20 | private function __construct() { 21 | trigger_error( 22 | 'The ' . ActiveProfilesBuilder::class . ' is being removed in 3.0. There is no corresponding feature replacing this. Please use a static constructor on the new Profiles class instead.', 23 | E_USER_DEPRECATED 24 | ); 25 | } 26 | 27 | /** 28 | * Ensures that the 'default' profile is included. 29 | * 30 | * There is not currently a way to use this builder without including the 'default' profile. This is intended behavior, 31 | * if you do not want 'default' profile to be included you should not use the ActiveProfilesBuilder. 32 | * 33 | * @return static 34 | */ 35 | public static function hasDefault() : self { 36 | return new self(); 37 | } 38 | 39 | /** 40 | * Add 1 or more profiles to the list of active profiles. 41 | * 42 | * An exception will be thrown if $profiles is empty, contains the 'default' value, or contains a value that has 43 | * already been added. 44 | * 45 | * @param string ...$profiles 46 | * @return $this 47 | */ 48 | public function add(string...$profiles) : self { 49 | if (empty($profiles)) { 50 | throw new InvalidArgumentException('When adding a profile at least 1 value must be provided.'); 51 | } elseif (in_array('default', $profiles)) { 52 | throw new InvalidArgumentException("The 'default' profile is already active and should not be added explicitly."); 53 | } elseif (!empty($dupes = array_intersect($this->profiles, $profiles))) { 54 | throw new InvalidArgumentException(sprintf( 55 | "The '%s' %s already active and cannot be added again.", 56 | join("', '", $dupes), 57 | count($dupes) === 1 ? 'profile is' : 'profiles are' 58 | )); 59 | } 60 | $instance = clone $this; 61 | $instance->profiles = [...$this->profiles, ...$profiles]; 62 | return $instance; 63 | } 64 | 65 | /** 66 | * Will add a single $profile if the return value from $decider is a truthy value. 67 | * 68 | * Will throw an exception if $decider returns true and the $profile is the 'default' value or the $profile has 69 | * already been added. 70 | * 71 | * @param string $profile 72 | * @param callable $decider 73 | * @return $this 74 | */ 75 | public function addIf(string $profile, callable $decider) : self { 76 | $instance = clone $this; 77 | if ($decider()) { 78 | $instance = $instance->add($profile); 79 | } 80 | return $instance; 81 | } 82 | 83 | /** 84 | * Will add multiple $profiles if the return value from $decider is a truthy value. 85 | * 86 | * Will throw an exception if $decider returns true and any value in $profiles is the 'default' value or has already 87 | * been added. 88 | * 89 | * @param list $profiles 90 | * @param callable $decider 91 | * @return $this 92 | */ 93 | public function addAllIf(array $profiles, callable $decider) : self { 94 | $instance = clone $this; 95 | if ($decider()) { 96 | $instance = $instance->add(...$profiles); 97 | } 98 | return $instance; 99 | } 100 | 101 | /** 102 | * Return an array of active profiles based on previous calls to the builder. 103 | * 104 | * @return list 105 | */ 106 | public function build() : array { 107 | return ['default', ...$this->profiles]; 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/Cli/Command/CacheClearCommand.php: -------------------------------------------------------------------------------- 1 | cache-clear [OPTION]... 35 | 36 | DESCRIPTION 37 | 38 | cache-clear ensures that a ContainerDefinition previously compiled 39 | with build, or by bootstrapping your app, is removed from a configured cache. 40 | This ensures the next time your ContainerDefinition is compiled it runs static 41 | analysis again. 42 | 43 | If you do not have a cacheDir configured this command will error. 44 | 45 | OPTIONS 46 | 47 | --config-file="file-path.xml" 48 | 49 | Set the name of the configuration file to be used. If not provided the 50 | default value will be "annotated-container.xml". 51 | 52 | SHELL; 53 | } 54 | 55 | public function handle(Input $input, TerminalOutput $output) : int { 56 | $configName = $input->getOption('config-file'); 57 | if (!isset($configName)) { 58 | // This not being present would be highly irregular and not party of the happy path 59 | // But it is possible that somebody created the configuration manually and is not using composer 60 | $composerFile = $this->directoryResolver->getConfigurationPath('composer.json'); 61 | if (file_exists($composerFile)) { 62 | $composer = json_decode(file_get_contents($composerFile), true); 63 | $configName = $composer['extra']['annotatedContainer']['configFile'] ?? 'annotated-container.xml'; 64 | } else { 65 | $configName = 'annotated-container.xml'; 66 | } 67 | } else { 68 | if (is_bool($configName)) { 69 | throw InvalidOptionType::fromBooleanOption('config-file'); 70 | } elseif (is_array($configName)) { 71 | throw InvalidOptionType::fromArrayOption('config-file'); 72 | } 73 | } 74 | 75 | $configPath = $this->directoryResolver->getConfigurationPath($configName); 76 | if (!file_exists($configPath)) { 77 | throw ConfigurationNotFound::fromMissingFile($configName); 78 | } 79 | 80 | $config = new XmlBootstrappingConfiguration($configPath, $this->directoryResolver); 81 | $cacheDir = $config->getCacheDirectory(); 82 | if (!isset($cacheDir)) { 83 | throw CacheDirConfigurationNotFound::fromCacheCommand(); 84 | } 85 | 86 | $cachePath = $this->directoryResolver->getCachePath($cacheDir); 87 | if (!is_dir($cachePath)) { 88 | throw CacheDirNotFound::fromMissingDirectory($cacheDir); 89 | } 90 | 91 | $sourceDirs = []; 92 | foreach ($config->getScanDirectories() as $scanDirectory) { 93 | $sourceDirs[] = $this->directoryResolver->getPathFromRoot($scanDirectory); 94 | } 95 | 96 | sort($sourceDirs); 97 | $cacheKey = md5(join($sourceDirs)); 98 | $cachePath = $this->directoryResolver->getCachePath(sprintf('%s/%s', $cacheDir, $cacheKey)); 99 | 100 | if (file_exists($cachePath)) { 101 | unlink($cachePath); 102 | } 103 | 104 | $output->stdout->write('Annotated Container cache has been cleared.'); 105 | return 0; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /docs/tutorials/07-using-configuration-objects.md: -------------------------------------------------------------------------------- 1 | # Using Configuration Objects 2 | 3 | AnnotatedContainer provides out-of-the-box functionality to allow defining type-safe configuration objects. Configurations are classes with public, readonly properties that can take advantage of all the features found in the `#[Inject]` Attribute. In the example below we'll take a look at a well-known configuration, the database config. 4 | 5 | ## Example Configuration 6 | 7 | ```php 8 | 2 | 10 | 11 | 12 | This Schema defines how to configure Annotated Container to bootstrap itself. 13 | 14 | 15 | 16 | 17 | 18 | Root Element 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | Define locations that should be scanned for Attributes to create your ContainerDefinition. These directories 28 | should exist within the root of your project. 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | --------------------------------------------------------------------------------