Skip to content

Use attributes instead of neon to register services #324

@ondrejmirtes

Description

@ondrejmirtes

I want to open a discussion about registering services with attributes above classes instead of neon files.

I know about the idea behind the current way, I was there 13 years ago when it was conceived 😅 The idea is that classes shouldn't care about DI, about the way they are constructed. They should only care about asking for dependencies in constructor.

People should be encouraged to register the same class multiple times with different arguments, for reusability and to truly distinguish OOP from "class-based programming".

But in practice 90 % of services are one-off autowired classes. And the reality is that .neon files become huge mess with thousands of lines of code in big applications. Something like this: https://github.com/phpstan/phpstan-src/blob/2.1.17/conf/config.neon

I prototyped and implemented these attributes in phpstan-src. Thanks to this I was able to cut my config.neon to just 250 lines (https://github.com/phpstan/phpstan-src/blob/4997fb752ac4d4c7cc32eecca5d26a2d14d04dd8/conf/config.neon). Autowired services are now registered via attributes, and the non-autowired services are in a separate file (https://github.com/phpstan/phpstan-src/blob/4997fb752ac4d4c7cc32eecca5d26a2d14d04dd8/conf/services.neon).

A service like this:

	-
		class: PHPStan\File\FileMonitor
		arguments:
			analyseFileFinder: @fileFinderAnalyse
			scanFileFinder: @fileFinderScan
			analysedPaths: %analysedPaths%
			analysedPathsFromConfig: %analysedPathsFromConfig%
			scanFiles: %scanFiles%
			scanDirectories: %scanDirectories%

Can now be registered like this:

#[AutowiredService]
final class FileMonitor
{

	/**
	 * @param string[] $analysedPaths
	 * @param string[] $analysedPathsFromConfig
	 * @param string[] $scanFiles
	 * @param string[] $scanDirectories
	 */
	public function __construct(
		#[AutowiredParameter(ref: '@fileFinderAnalyse')]
		private FileFinder $analyseFileFinder,
		#[AutowiredParameter(ref: '@fileFinderScan')]
		private FileFinder $scanFileFinder,
		#[AutowiredParameter]
		private array $analysedPaths,
		#[AutowiredParameter]
		private array $analysedPathsFromConfig,
		#[AutowiredParameter]
		private array $scanFiles,
		#[AutowiredParameter]
		private array $scanDirectories,
	)
	{
	}

	...
}  

A service like this:

	-
		class: PHPStan\Reflection\SignatureMap\NativeFunctionReflectionProvider
		arguments:
			reflector: @betterReflectionReflector

Can now be registered like this:

#[AutowiredService]
final class NativeFunctionReflectionProvider
{

	/** @var NativeFunctionReflection[] */
	private array $functionMap = [];

	public function __construct(
		private SignatureMapProvider $signatureMapProvider,
		#[AutowiredParameter(ref: '@betterReflectionReflector')]
		private Reflector $reflector,
		private FileTypeMapper $fileTypeMapper,
		private StubPhpDocProvider $stubPhpDocProvider,
		private AttributeReflectionFactory $attributeReflectionFactory,
	)
	{
	}
	...
}  

If the original neon service has autowired key, I implemented as as attribute parameter. From:

	-
		class: PHPStan\PhpDoc\DefaultStubFilesProvider
		arguments:
			stubFiles: %stubFiles%
			composerAutoloaderProjectPaths: %composerAutoloaderProjectPaths%
		autowired:
			- PHPStan\PhpDoc\StubFilesProvider

to:

#[AutowiredService(as: StubFilesProvider::class)]
final class DefaultStubFilesProvider implements StubFilesProvider
{

	/**
	 * @param string[] $stubFiles
	 * @param string[] $composerAutoloaderProjectPaths
	 */
	public function __construct(
		private Container $container,
		#[AutowiredParameter]
		private array $stubFiles,
		#[AutowiredParameter]
		private array $composerAutoloaderProjectPaths,
	)
	{
	}
	...
}  

But what I like best is how I did the implement key. The attribute is called GeneratedFactory. From:

	-
		implement: PHPStan\Reflection\FunctionReflectionFactory
		arguments:
			parser: @defaultAnalysisParser

to:

#[GenerateFactory(interface: FunctionReflectionFactory::class)]
final class PhpFunctionReflection implements FunctionReflection
{

	public function __construct(
		private InitializerExprTypeResolver $initializerExprTypeResolver,
		private ReflectionFunction $reflection,
		#[AutowiredParameter(ref: '@defaultAnalysisParser')]
		private Parser $parser,
		...
	)
	{
	}

The downside of implement in neon is that the arguments do not belong to FunctionReflectionFactory, but in reality to the return type of FunctionReflectionFactory::create(). With the GenerateFactory attribute, we can put AutowiredParameter above the parser parameter which is much more intuitive and closer together.

This is just some food for thought, maybe nette/di can introduce something like that in the future. In phpstan-src I was able to implement all of this (and a little bit more PHPStan-specific stuff) in around 130 lines of code: https://github.com/phpstan/phpstan-src/blob/4997fb752ac4d4c7cc32eecca5d26a2d14d04dd8/src/DependencyInjection/AutowiredAttributeServicesExtension.php

What helped me a lot too is the https://github.com/olvlvl/composer-attribute-collector package.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions