Application services

Controllers, middleware, and services support constructor injection. Use #[Service] to bind an interface or abstract class to an implementation:

use GustavPHP\Gustav\Attribute\Service;

interface DogRepository
{
    public function findAll(): array;
}

#[Service(as: DogRepository::class)]
final class SqlDogRepository implements DogRepository
{
    public function __construct(private readonly PDO $database)
    {
    }

    public function findAll(): array
    {
        // ...
    }
}

Gustav recursively discovers #[Service] classes under your application's Services namespace. as connects an interface or abstract class to that implementation. Execution scope is the default, so no lifetime argument is needed for most repositories and application services.

Concrete classes with the default scoped lifetime need no attribute at all; Gustav autowires them when they are first requested. Use #[Service] when you need an interface binding or a different lifetime.

Constructor injection

Type-hint dependencies in a controller, middleware, or another service:

use GustavPHP\Gustav\Attribute\{Controller, Get};

#[Controller('/dogs')]
final readonly class DogsController
{
    public function __construct(
        private readonly DogRepository $dogs,
    ) {
    }

    #[Get]
    public function list(): array
    {
        return $this->dogs->findAll();
    }
}

Unregistered concrete classes are autowired and reused within the current HTTP request or console command. Interfaces and abstract classes require a binding. Move scalar settings into typed configuration, then inject that configuration into the service that needs it.

Circular dependencies and unresolvable constructor parameters produce configuration errors that identify the affected service chain.

Third-party objects with factories

Use a discovered factory when an object must be constructed with scalar configuration or library-specific setup. One invokable factory produces one service:

namespace App\Services;

use App\Config\DatabaseConfig;
use GustavPHP\Gustav\Attribute\Factory;
use GustavPHP\Gustav\Service\Lifetime;
use PDO;

#[Factory(lifetime: Lifetime::Singleton)]
final readonly class DatabaseFactory
{
    public function __construct(private DatabaseConfig $configuration)
    {
    }

    public function __invoke(): PDO
    {
        return new PDO($this->configuration->url);
    }
}

Place the class under the application Services namespace. Gustav autowires the factory constructor and uses the non-nullable __invoke() return type as the product's service identifier. In this example, any controller or service can inject PDO directly.

The factory runs when PDO is first requested. Its configured lifetime applies to the returned product. Lifetime::Scoped is the default; choose Singleton only when the product and all of its dependencies are safe to share across requests and commands.

Gustav validates factory declarations during startup. A factory must be an instantiable class with exactly one #[Factory] attribute and a public, non-static, zero-argument __invoke() method. Its return type must be one existing, non-nullable class or interface; scalar, union, and intersection return types are rejected. A factory cannot also be a #[Service] or service provider.

Declaring two factories for the same product, two services for the same identifier, or a service and factory for one identifier stops startup with an error naming both declarations. One application factory may replace a framework default such as LoggerInterface or ViewRendererInterface.

Lifetimes

Choose a lifetime based on how long service state is safe to retain:

LifetimeBehavior
Lifetime::SingletonOne instance for the application process, shared by every HTTP request and command.
Lifetime::ScopedOne instance during an HTTP request or command, released after success or failure. This is the default.
Lifetime::TransientA new instance on every resolution.
use GustavPHP\Gustav\Attribute\Service;
use GustavPHP\Gustav\Service\Lifetime;

#[Service(lifetime: Lifetime::Singleton)]
final class MetricsRegistry {}

#[Service(as: Cache::class, lifetime: Lifetime::Singleton)]
final class RedisCache implements Cache {}

#[Service(lifetime: Lifetime::Transient)]
final class PayloadEncoder {}

Singleton services and factory products are created outside any execution scope. They cannot resolve scoped services, preventing a singleton from accidentally retaining the first request, user, or command state for the lifetime of a process.

Request-aware services

The current request and its validated request ID are available inside the request scope:

use GustavPHP\Gustav\Http\RequestId;
use Psr\Http\Message\ServerRequestInterface;

final readonly class RequestContext
{
    public function __construct(
        public ServerRequestInterface $request,
        public RequestId $requestId,
    ) {
    }
}

During HTTP requests, Gustav automatically provides these framework services:

  • Application and Configuration as singletons
  • Psr\Log\LoggerInterface as the default singleton logger
  • Router\UrlGeneratorInterface for named application routes
  • View\ViewRendererInterface for HTML view responses
  • ServerRequestInterface for the active request
  • Http\RequestId for the active request
  • Session for lazy server-side request state
  • Security\CsrfTokenManager for form and request tokens
  • Service\Container, resolving to the active scope

Commands receive their own active scope with Symfony's input, output, and SymfonyStyle services. See Commands for the complete command contract.

Prefer injecting the specific dependency a class needs. Declarative factories also use constructor injection and do not receive the container in __invoke().

See Logging for writing PSR-3 records, correlating them with requests, and replacing the default logger through service discovery.

The default session store is a singleton. A discovered singleton #[Service(as: SessionStoreInterface::class)] replaces it before requests are handled, while Session and CsrfTokenManager remain isolated to the active request. See Sessions and CSRF.

Service providers

Factories cover the normal third-party integration case. Keep a discovered service provider for application composition that is genuinely dynamic, such as choosing between implementations at startup:

use GustavPHP\Gustav\Service\{Container, Provider};

final class InfrastructureProvider implements Provider
{
    public function register(Container $services): void
    {
        $services->singleton(
            PaymentClient::class,
            function (Container $services): PaymentClient {
                $configuration = $services->get(PaymentConfig::class);

                return $configuration->sandbox
                    ? new SandboxPaymentClient($configuration->key)
                    : new LivePaymentClient($configuration->key);
            },
        );
    }
}

Place providers under the application Services namespace. Gustav discovers them and calls register() during startup. Providers must have a public zero-argument constructor.

The registry exposes bind(), singleton(), scoped(), and transient() for dynamic application composition. Providers run after attributed services and factories, making them the explicit low-level override layer. The registry is frozen before request or command handling begins.

Application configuration does not require a provider or a singleton service marker. Define a #[Config] readonly class and inject it directly; Gustav hydrates and registers it before factories and providers run. See Configuration for environment conversion, validation, defaults, and test overrides.