Skip to content

Conversation

aazsamir
Copy link
Contributor

@aazsamir aazsamir commented Aug 23, 2025

Decorator pattern is one I'm using often and I'm really missing it in Tempest. It can be somehow emulated using initializers but it's not the same.

I added Decorator attribute

#[Attribute(Attribute::TARGET_CLASS)]
final readonly class Decorator
{
    public function __construct(
        public string $decorates,
    ) {}
}

and to decorate existing implementation

#[Decorator(Repository::class)]
class CacheRepository implements Repository
{
    public function __construct(
        private Repository $decorated,
    ) {}
}

That was v1, I'll leave that for the archaeologists.
I added Decorator interface

<?php

declare(strict_types=1);

namespace Tempest\Container;

interface Decorator
{
    public function setDecorated(mixed $decorated): mixed;
}

and to decorate existing implementation

<?php

declare(strict_types=1);

namespace App;

use Tempest\Container\Decorator;

class CacheRepository implements Decorator, Repository
{
    private Repository $decorated;

    public function setDecorated(mixed $decorated): Repository
    {
        assert($decorated instanceof Repository);
        $this->decorated = $decorated;

        return $decorated;
    }
}

As you can see, decorated instance is indicated to discovery by return type of setDecorated method (like in Initializers).

@innocenzi
Copy link
Member

Would be nice to have built-in decorators, but not a fan of this implementation, which forces having a setDecorated method. I'm not sure if this is possible, but what if we had this instead?

#[Decorates(Repository::class)]
final class CacheRepository implements Repository
{
    public function __construct(
        private readonly Repository $repository, // container knows to inject the original one
        private readonly Cache $cache,
    ) {}
}

Tempest would discover this class and register it as a decorator. When resolving Repository, it would detect that there is a decorator for it and resolve the decorator—but during resolution, it would inject the original interface instead of trying to inject the decorated one (that would be an infinite loop otherwise)

@aazsamir
Copy link
Contributor Author

Would be nice to have built-in decorators, but not a fan of this implementation, which forces having a setDecorated method. I'm not sure if this is possible, but what if we had this instead?

#[Decorates(Repository::class)]
final class CacheRepository implements Repository
{
    public function __construct(
        private readonly Repository $repository, // container knows to inject the original one
        private readonly Cache $cache,
    ) {}
}

Tempest would discover this class and register it as a decorator. When resolving Repository, it would detect that there is a decorator for it and resolve the decorator—but during resolution, it would inject the original interface instead of trying to inject the decorated one (that would be an infinite loop otherwise)

Yeah, you are right. I changed it to attribute #[Decorator(decorates: Repository::class)]. Avoiding circular dependency was easy because we pass params through all the stack up to autowireObjectDependency method, so we just need to pass it all along.

@innocenzi
Copy link
Member

Here's a first review, but it will need @brendt's approval and review as well

@innocenzi innocenzi changed the title feat(container): add Decorator feat(container): support decorators Aug 23, 2025
@aazsamir aazsamir marked this pull request as ready for review August 24, 2025 09:03
@brendt
Copy link
Member

brendt commented Sep 12, 2025

I'm currently working through my backlog and I'll come back to this one as soon as possible (after 2.0)

@brendt brendt added this to the 2.x milestone Sep 12, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants