Cartoon avatar of Charles Sprayberry
cspray.io

Annotated Container Without Attributes

In blog articles and documentation about Annotated Container I talk a lot about using the library with Attributes. I like wiring my Container this way and believe it is a neat feature. However, it isn't required to use Attributes with Annotated Container. Attributes are just one way to configure Cspray\AnnotatedContainer\Definition\ContainerDefinition implementation. I actually consider this interface, and the other interfaces in the Cspray\AnnotatedContainer\Definition namespace, to be the most important feature and a key design goal with the library.

The ContainerDefinition is a way to declare what your Container should have in it without defining how that Container is created. I believe that this has a tremendous amount of value.

  • You can create a variety of different Container implementations. Annotated Container has support for 3 implementations at the date of this article, and has plans for more.
  • Static analysis and other validation can happen on your Container wiring before any objects are created or your app actually runs. I've started some of this already with the ./vendor/bin/annotated-container validate command that runs a series of checks on your ContainerDefinition. There are plans to expand on the checks and add support for other static analysis tools.
  • You can create IDE and other tooling to inspect and see more information about what's in your Container. This is a long-term goal, but I want to have IDE plugins that allow for easily searching and viewing details of your Container wiring.

I gave all this context to show that I don't think Attributes are all that important in the grand scheme of things. They are the way I prefer to wire my Container, but you don't have to follow my preferences! The rest of this document shows how you can use the library without Attributes at all or how you can use Attributes while highly limiting their spread across your codebase.

Annotated Container Without Attributes

In the docs "Adding Third-Party Services" I talk about how there are some situations you simply can't put Attributes on code you want to wire. After the library has analyzed your code for Attributes a Cspray\AnnotatedContainer\Definition\DefinitionProvider is invoked, if you've provided one. Using a series of low-level functions you can do everything that you otherwise might do with an Attribute.

<?php declare(strict_types=1);

namespace Acme\MyContainerLayer;

use Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProvider;
use Cspray\AnnotatedContainer\StaticAnalysis\DefinitionProviderContext;
use function Cspray\AnnotatedContainer\alias;
use function Cspray\AnnotatedContainer\service;
use function Cspray\AnnotatedContainer\serviceDelegate;
use function Cspray\AnnotatedContainer\servicePrepare;
use function Cspray\AnnotatedContainer\injectMethodParam;
use function Cspray\AnnotatedContainer\injectProperty;

final class MyDefinitionProvider implements DefinitionProvider {

    public function consume(DefinitionProviderContext $context) : void {
        // use the imported functions to wire up your Container
    }

}

If you're using Annotated Container's default functionality you should ensure this class is defined in your annotated-container.xml file. Otherwise, you should ensure it is added to your Container's bootstrapping using whatever mechanism you've designed. With implementations of DefinitionProvider you can define your Container without ever making use of Attributes.

Annotated Container With Limited Attributes

Perhaps you aren't opposed to using Attributes, you just don't want them spread across your codebase. You'd prefer the places you're marking are limited in scope and not part of your domain code. This is possible with judicious use of the #[ServiceDelegate] Attribute. This Attribute allows you to define a class method, static or instance, that will be invoked when the service is created. You can keep these factory methods in their own namespace and ensure service wiring Attributes are only used in this namespace.

In this example I'm also going to demonstrate another core principle behind Annotated Container. Do not use the service locator pattern! Generally speaking, you should not declare your Container as a type in constructor dependencies, setter dependencies, or methods. Even in factories! Using a Container like this leads to code that's harder to reason about, refactor, and test.

<?php declare(strict_types=1);

namespace Acme\MyServiceLayer {

    class Foo {}

    class Bar {

        public function __construct(private readonly Foo $foo) {}

    }

    class Baz {

        public function __construct(
            private readonly Foo $foo,
            private readonly Bar $bar
        ) {}

    }

}

namespace Acme\MyContainerLayer {

    use Acme\MyServiceLayer\Foo;
    use Acme\MyServiceLayer\Bar;
    use Acme\MyServiceLayer\Baz;
    use Cspray\AnnotatedContainer\Attribute\ServiceDelegate;

    class MyFactory {

        #[ServiceDelegate]
        public static function createFoo(): Foo {
            return new Foo();
        }

        #[ServiceDelegate]
        public static function createBar(Foo $foo): Bar {
            return new Bar($foo);
        }

        #[ServiceDelegate]
        public static function createBaz(Foo $foo, Bar $bar): Baz {
            return new Baz($foo, $bar);
        }
    }
}

In v2.2 and lower, you would also need to declare each type created by a #[ServiceDelegate] as an explicit Service; either with Attributes or with the functional API. In v2.3+ this requirement has been lifted and declaring a #[ServiceDelegate] for a type that is not a defined Service will implicitly make it a defined Service.

In this example we're using a minimal amount of Attributes to define our Container wiring. The use of those Attributes is confined to its own namespace and isn't polluting your service or domain layers. Additionally, and most importantly in my opinion, we're constructing an object with dependencies and not passing a Container around!

Attributes are a means to an end... use different means!

Although I prefer Attributes, they're just a means to an end. That end being a declaration of what your Container should have in it so that a variety of Container implementations, static analysis, and tooling can be built with it. There are other means to this end. If you don't like my preference, you should use those instead!