Configuration

Gustav separates framework bootstrap settings from application settings. The starter uses conventional project paths and reads MODE. app/bootstrap.php returns the configuration shared by the HTTP worker and project CLI:

use GustavPHP\Gustav\Configuration;

return Configuration::forProject(
    namespace: 'App',
    root: dirname(__DIR__),
);

Configuration::forProject() selects development when MODE is absent and accepts MODE=development or MODE=production. It configures these paths relative to the supplied project root:

SettingConventional path
Static filespublic/
Viewsviews/
Session storagestorage/sessions/

Typed application configuration

Put immutable configuration classes under your application's Config namespace. Mark the class with #[Config] and map every constructor parameter to an environment variable with #[Env]:

namespace App\Config;

use GustavPHP\Gustav\Attribute\{Config, Env, Validate};
use GustavPHP\Gustav\Validation\Common\Integer;

enum DatabaseRole: string
{
    case Primary = 'primary';
    case Replica = 'replica';
}

#[Config]
final readonly class DatabaseConfig
{
    /** @param list<string> $replicas */
    public function __construct(
        #[Env('DATABASE_URL')]
        public string $url,
        #[Env('DATABASE_POOL_SIZE'), Validate(new Integer(min: 1, max: 100))]
        public int $poolSize = 10,
        #[Env('DATABASE_SSL')]
        public bool $ssl = true,
        #[Env('DATABASE_REPLICAS')]
        public array $replicas = [],
        #[Env('DATABASE_ROLE')]
        public DatabaseRole $role = DatabaseRole::Primary,
    ) {
    }
}

Gustav discovers and hydrates every #[Config] class before request handling starts. Each object is registered as an application singleton, so controllers, services, middleware, and service factories use ordinary constructor injection:

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);
    }
}

See Third-party objects with factories for the complete factory contract and lifetime rules.

Conversion and optional values

Environment values are strings. Gustav converts them from the declared constructor type:

PHP typeAccepted environment value
stringThe value unchanged
intA valid whole number, including 0 and negative values
floatA finite decimal number
booltrue, false, 1, or 0, case-insensitive
arrayJSON that decodes to a PHP array
backed enumAn exact string backing value or valid integer backing value

Nullable forms of these types are supported. Missing values follow PHP constructor semantics:

  • A parameter without a default is required, even when its type is nullable.
  • An omitted parameter with a default keeps that default.
  • Use ?string $value = null when absence should resolve to null.
  • An empty environment string is still a string; it is not treated as null.

Ambiguous unions and unsupported object types are rejected during startup instead of being guessed at runtime. Arrays do not infer element types; validate their contents inside application code when needed.

Repeatable #[Validate(...)] attributes use the same built-in validation rules as request input. All type and rule failures across discovered configuration classes are collected before Gustav throws one configuration exception.

Environment files and secrets

Configuration::forProject() loads optional files from the project root in this order:

  1. .env provides safe, committed development defaults.
  2. .env.local overrides those defaults for one machine and should be ignored by Git.
  3. Real process environment variables override both files.

For example:

MODE=development
DATABASE_POOL_SIZE=10
DATABASE_SSL=true
DATABASE_REPLICAS='["postgres-replica.internal"]'

Use deployment environment variables for production secrets. Gustav never includes the rejected raw value in its own startup diagnostics. Errors identify the variable and target field so multiple problems can be fixed together:

Application configuration is invalid:
- DATABASE_POOL_SIZE (App\Config\DatabaseConfig::$poolSize): Value must be integer
- DATABASE_URL (App\Config\DatabaseConfig::$url): Value is required

Configuration objects contain the resolved values by design. Do not dump or log an entire configuration object when it contains credentials.

Testing configuration

Use an isolated Environment instead of changing process-global variables in tests:

use GustavPHP\Gustav\Config\Environment;
use GustavPHP\Gustav\Configuration;

$configuration = Configuration::forProject(
    namespace: 'App',
    root: dirname(__DIR__),
    environment: Environment::fromArray([
        'MODE' => 'production',
        'DATABASE_URL' => 'sqlite::memory:',
    ]),
);

The supplied map is the complete test environment, preventing process variables from leaking between tests.

Custom framework layout

Construct Configuration directly when the project does not use the conventional directories:

use GustavPHP\Gustav\{Configuration, Mode};
use GustavPHP\Gustav\Session\SessionOptions;

$configuration = new Configuration(
    mode: Mode::Production,
    namespace: 'App',
    files: '/srv/example/web/',
    views: '/srv/example/templates/',
    routeNamespaces: ['Module\Billing\Routes'],
    eventNamespaces: ['Module\Billing\Events'],
    serializerNamespaces: ['Module\Billing\Serializers'],
    serviceNamespaces: ['Module\Billing\Services'],
    middlewareNamespaces: ['Module\Billing\Middlewares'],
    configurationNamespaces: ['Module\Billing\Config'],
    commandNamespaces: ['Module\Billing\Commands'],
    exceptionHandlerNamespaces: ['Module\Billing\ExceptionHandlers'],
    session: new SessionOptions(directory: '/srv/example/var/sessions/'),
);

Direct construction reads real process variables for typed application configuration. Pass an explicit Environment when another source is required. The framework Configuration object itself remains injectable as a singleton. Direct construction disables sessions unless session is supplied. See Sessions and CSRF for cookie options and custom shared stores.