diff --git a/docs/1-essentials/06-configuration.md b/docs/1-essentials/06-configuration.md index 6d7cf10d7..8f997b463 100644 --- a/docs/1-essentials/06-configuration.md +++ b/docs/1-essentials/06-configuration.md @@ -28,30 +28,30 @@ return new PostgresConfig( The configuration object above instructs Tempest to use PostgreSQL as its database, replacing the framework's default database, SQLite. -## Accessing configuration objects +### Accessing configuration objects To access a configuration object, you may inject it from the container like any other dependency. ```php -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; -final readonly class HomeController +final readonly class AboutController { public function __construct( - private AppConfig $config, + private Environment $environment, ) {} #[Get('/')] public function __invoke(): View { - return view('home.view.php', environment: $this->config->environment); + return view('about.view.php', environment: $this->environment); } } ``` -## Updating configuration objects +### Updating configuration objects -To update a property in a configuration object, you may simply assign a new value. Due to the object being a singleton, the modification will be persisted throught the rest of the application's lifecycle. +To update a property in a configuration object, you may simply assign a new value. Due to the object being a singleton, the modification will be persisted through the rest of the application's lifecycle. ```php use Tempest\Support\Random; @@ -81,7 +81,7 @@ final class SlackConfig public string $token, public string $baseUrl, public string $applicationId, - public string $userAgent, + public ?string $userAgent = null, ) {} } ``` @@ -120,7 +120,7 @@ final class SlackConnector extends HttpConnector ## Per-environment configuration -Whenever possible, you should have a single configuration file per feature. You may use the `Tempest\env()` function inside that file to reference credentials and environment-specific values. +Whenever possible, you should have a single configuration file per feature. You may use the {b`Tempest\env()`} function inside that file to reference credentials and environment-specific values. However, it's sometimes needed to have completely different configurations in development and in production. For instance, you may use S3 for your [storage](../2-features/05-file-storage.md) in production, but use the local filesystem during development. @@ -135,6 +135,13 @@ return new S3StorageConfig( ); ``` +The following suffixes are supported: + +- `.prd.config.php`, `.prod.config.php`, and `.production.config.php` for the production environment. +- `.stg.config.php` and `.staging.config.php` for the staging environment. +- `.dev.config.php` and `.local.config.php` for the development environment. +- `.test.config.php` and `.testing.config.php` for the testing environment. + ## Disabling the configuration cache During development, Tempest will discover configuration files every time the framework is booted. In a production environment, configuration files are automatically cached. diff --git a/docs/1-essentials/07-testing.md b/docs/1-essentials/07-testing.md index f2095d477..16f926199 100644 --- a/docs/1-essentials/07-testing.md +++ b/docs/1-essentials/07-testing.md @@ -125,6 +125,18 @@ $this->console And many, many more. +## Spoofing the environment + +By default, Tempest provides a `phpunit.xml` that sets the `ENVIRONMENT` variable to `testing`. This is needed so that Tempest can adapt its boot process and load the proper configuration files for the testing environment. + +During tests, you may want to test different paths of your application depending on the environment. For instance, you may want to test that certain features are only available in production. To do this, you may override the {b`Tempest\Core\Environment`} singleton: + +```php +use Tempest\Core\Environment; + +$this->container->singleton(Environment::class, Environment::PRODUCTION); +``` + ## Changing the location of tests The `phpunit.xml` file contains a `{html}` element that configures the directory in which PHPUnit looks for test files. This may be changed to follow any rule of your convenience. diff --git a/packages/cache/src/Commands/CacheStatusCommand.php b/packages/cache/src/Commands/CacheStatusCommand.php index 42158ecb6..7df85538b 100644 --- a/packages/cache/src/Commands/CacheStatusCommand.php +++ b/packages/cache/src/Commands/CacheStatusCommand.php @@ -12,9 +12,9 @@ use Tempest\Console\HasConsole; use Tempest\Container\Container; use Tempest\Container\GenericContainer; -use Tempest\Core\AppConfig; use Tempest\Core\ConfigCache; use Tempest\Core\DiscoveryCache; +use Tempest\Core\Environment; use Tempest\Icon\IconCache; use Tempest\Support\Str; use Tempest\View\ViewCache; @@ -22,7 +22,7 @@ use function Tempest\Support\arr; -if (class_exists(\Tempest\Console\ConsoleCommand::class)) { +if (class_exists(ConsoleCommand::class)) { final readonly class CacheStatusCommand { use HasConsole; @@ -30,7 +30,7 @@ public function __construct( private Console $console, private Container $container, - private AppConfig $appConfig, + private Environment $environment, private DiscoveryCache $discoveryCache, ) {} @@ -73,7 +73,7 @@ public function __invoke(bool $internal = true): void }, ); - if ($this->appConfig->environment->isProduction() && ! $this->discoveryCache->enabled) { + if ($this->environment->requiresCaution() && ! $this->discoveryCache->enabled) { $this->console->writeln(); $this->console->error('Discovery cache is disabled in production. This is not recommended.'); } diff --git a/packages/console/src/Middleware/CautionMiddleware.php b/packages/console/src/Middleware/CautionMiddleware.php index e6b6d5d83..3842ea9b5 100644 --- a/packages/console/src/Middleware/CautionMiddleware.php +++ b/packages/console/src/Middleware/CautionMiddleware.php @@ -9,7 +9,7 @@ use Tempest\Console\ConsoleMiddlewareCallable; use Tempest\Console\ExitCode; use Tempest\Console\Initializers\Invocation; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\Discovery\SkipDiscovery; #[SkipDiscovery] @@ -17,14 +17,12 @@ { public function __construct( private Console $console, - private AppConfig $appConfig, + private Environment $environment, ) {} public function __invoke(Invocation $invocation, ConsoleMiddlewareCallable $next): ExitCode|int { - $environment = $this->appConfig->environment; - - if ($environment->isProduction() || $environment->isStaging()) { + if ($this->environment->requiresCaution()) { if ($this->console->isForced) { return $next($invocation); } diff --git a/packages/core/src/AppConfig.php b/packages/core/src/AppConfig.php index ab1c1dc83..20e623e65 100644 --- a/packages/core/src/AppConfig.php +++ b/packages/core/src/AppConfig.php @@ -8,15 +8,11 @@ final class AppConfig { - public Environment $environment; - public string $baseUri; public function __construct( public ?string $name = null, - ?Environment $environment = null, - ?string $baseUri = null, /** @var class-string<\Tempest\Core\ExceptionProcessor>[] */ @@ -27,7 +23,6 @@ public function __construct( */ public array $insightsProviders = [], ) { - $this->environment = $environment ?? Environment::fromEnv(); $this->baseUri = $baseUri ?? env('BASE_URI') ?? ''; } } diff --git a/packages/core/src/Commands/DiscoveryGenerateCommand.php b/packages/core/src/Commands/DiscoveryGenerateCommand.php index f9f389a69..00147bb94 100644 --- a/packages/core/src/Commands/DiscoveryGenerateCommand.php +++ b/packages/core/src/Commands/DiscoveryGenerateCommand.php @@ -9,10 +9,10 @@ use Tempest\Console\HasConsole; use Tempest\Container\Container; use Tempest\Container\GenericContainer; -use Tempest\Core\AppConfig; use Tempest\Core\DiscoveryCache; use Tempest\Core\DiscoveryCacheStrategy; use Tempest\Core\DiscoveryConfig; +use Tempest\Core\Environment; use Tempest\Core\FrameworkKernel; use Tempest\Core\Kernel; use Tempest\Core\Kernel\LoadDiscoveryClasses; @@ -27,7 +27,7 @@ public function __construct( private Kernel $kernel, private DiscoveryCache $discoveryCache, - private AppConfig $appConfig, + private Environment $environment, ) {} #[ConsoleCommand( @@ -37,7 +37,7 @@ public function __construct( )] public function __invoke(): void { - $strategy = DiscoveryCacheStrategy::make(env('DISCOVERY_CACHE', default: $this->appConfig->environment->isProduction())); + $strategy = DiscoveryCacheStrategy::make(env('DISCOVERY_CACHE', default: $this->environment->requiresCaution())); if ($strategy === DiscoveryCacheStrategy::NONE) { $this->info('Discovery cache disabled, nothing to generate.'); diff --git a/packages/core/src/ConfigCacheInitializer.php b/packages/core/src/ConfigCacheInitializer.php index 351df4cdf..584f123fd 100644 --- a/packages/core/src/ConfigCacheInitializer.php +++ b/packages/core/src/ConfigCacheInitializer.php @@ -14,18 +14,16 @@ final class ConfigCacheInitializer implements Initializer public function initialize(Container $container): ConfigCache { return new ConfigCache( - enabled: $this->shouldCacheBeEnabled( - $container->get(AppConfig::class)->environment->isProduction(), - ), + enabled: $this->shouldCacheBeEnabled(), ); } - private function shouldCacheBeEnabled(bool $isProduction): bool + private function shouldCacheBeEnabled(): bool { if (env('INTERNAL_CACHES') === false) { return false; } - return (bool) env('CONFIG_CACHE', default: $isProduction); + return (bool) env('CONFIG_CACHE', default: Environment::guessFromEnvironment()->requiresCaution()); } } diff --git a/packages/core/src/DiscoveryCacheInitializer.php b/packages/core/src/DiscoveryCacheInitializer.php index 1d92321a4..f0ebdb8a0 100644 --- a/packages/core/src/DiscoveryCacheInitializer.php +++ b/packages/core/src/DiscoveryCacheInitializer.php @@ -14,19 +14,17 @@ final class DiscoveryCacheInitializer implements Initializer public function initialize(Container $container): DiscoveryCache { return new DiscoveryCache( - strategy: $this->resolveDiscoveryCacheStrategy( - $container->get(AppConfig::class)->environment->isProduction(), - ), + strategy: $this->resolveDiscoveryCacheStrategy(), ); } - private function resolveDiscoveryCacheStrategy(bool $isProduction): DiscoveryCacheStrategy + private function resolveDiscoveryCacheStrategy(): DiscoveryCacheStrategy { if ($this->isDiscoveryGenerateCommand() || $this->isDiscoveryClearCommand()) { return DiscoveryCacheStrategy::NONE; } - $current = DiscoveryCacheStrategy::make(env('DISCOVERY_CACHE', default: $isProduction)); + $current = DiscoveryCacheStrategy::make(env('DISCOVERY_CACHE', default: Environment::guessFromEnvironment()->requiresCaution())); if ($current === DiscoveryCacheStrategy::NONE) { return $current; diff --git a/packages/core/src/Environment.php b/packages/core/src/Environment.php index 93e758436..658835b69 100644 --- a/packages/core/src/Environment.php +++ b/packages/core/src/Environment.php @@ -6,14 +6,24 @@ use function Tempest\env; +/** + * Represents the environment in which the application is running. + */ enum Environment: string { case LOCAL = 'local'; case STAGING = 'staging'; case PRODUCTION = 'production'; - case CI = 'ci'; + case CONTINUOUS_INTEGRATION = 'ci'; case TESTING = 'testing'; - case OTHER = 'other'; + + /** + * Determines if this environment requires caution for destructive operations. + */ + public function requiresCaution(): bool + { + return in_array($this, [self::PRODUCTION, self::STAGING], strict: true); + } public function isProduction(): bool { @@ -30,9 +40,9 @@ public function isLocal(): bool return $this === self::LOCAL; } - public function isCI(): bool + public function isContinuousIntegration(): bool { - return $this === self::CI; + return $this === self::CONTINUOUS_INTEGRATION; } public function isTesting(): bool @@ -40,14 +50,17 @@ public function isTesting(): bool return $this === self::TESTING; } - public function isOther(): bool + /** + * Guesses the environment from the `ENVIRONMENT` environment variable. + */ + public static function guessFromEnvironment(): self { - return $this === self::OTHER; - } + $value = env('ENVIRONMENT', default: 'local'); - public static function fromEnv(): self - { - $value = env('ENVIRONMENT', 'local'); + // Can be removed after https://github.com/tempestphp/tempest-framework/pull/1836 + if (is_null($value)) { + $value = 'local'; + } return self::tryFrom($value) ?? throw new EnvironmentValueWasInvalid($value); } diff --git a/packages/core/src/EnvironmentInitalizer.php b/packages/core/src/EnvironmentInitalizer.php new file mode 100644 index 000000000..cac26c694 --- /dev/null +++ b/packages/core/src/EnvironmentInitalizer.php @@ -0,0 +1,16 @@ + PHP_VERSION, 'Composer version' => $this->getComposerVersion(), 'Operating system' => $this->getOperatingSystem(), - 'Environment' => $this->appConfig->environment->value, + 'Environment' => $this->environment->value, 'Application URL' => $this->appConfig->baseUri ?: new Insight('Not set', Insight::ERROR), ]; } diff --git a/packages/core/src/EnvironmentValueWasInvalid.php b/packages/core/src/EnvironmentValueWasInvalid.php index 9d9995857..77fd56a13 100644 --- a/packages/core/src/EnvironmentValueWasInvalid.php +++ b/packages/core/src/EnvironmentValueWasInvalid.php @@ -12,8 +12,10 @@ final class EnvironmentValueWasInvalid extends Exception { public function __construct(string $value) { - $possibleValues = arr(Environment::cases())->map(fn (Environment $environment) => $environment->value)->implode(', '); + $possibleValues = arr(Environment::cases()) + ->map(fn (Environment $environment) => $environment->value) + ->join(); - parent::__construct("Invalid environment value `{$value}`, possible values are {$possibleValues}."); + parent::__construct("Invalid environment [{$value}]. Possible values are {$possibleValues}."); } } diff --git a/packages/core/src/ExceptionHandlerInitializer.php b/packages/core/src/ExceptionHandlerInitializer.php index fe3e24e01..17afb0e89 100644 --- a/packages/core/src/ExceptionHandlerInitializer.php +++ b/packages/core/src/ExceptionHandlerInitializer.php @@ -13,11 +13,11 @@ final class ExceptionHandlerInitializer implements Initializer #[Singleton] public function initialize(Container $container): ExceptionHandler { - $config = $container->get(AppConfig::class); + $environment = $container->get(Environment::class); return match (true) { PHP_SAPI === 'cli' => $container->get(ConsoleExceptionHandler::class), - $config->environment->isLocal() => $container->get(DevelopmentExceptionHandler::class), + $environment->isLocal() => $container->get(DevelopmentExceptionHandler::class), default => $container->get(HttpExceptionHandler::class), }; } diff --git a/packages/core/src/FrameworkKernel.php b/packages/core/src/FrameworkKernel.php index d7514b03b..d1a4cf32e 100644 --- a/packages/core/src/FrameworkKernel.php +++ b/packages/core/src/FrameworkKernel.php @@ -168,10 +168,7 @@ public function loadDiscoveryLocations(): self public function loadDiscovery(): self { $this->container->addInitializer(DiscoveryCacheInitializer::class); - $this->container->invoke( - LoadDiscoveryClasses::class, - discoveryLocations: $this->discoveryLocations, - ); + $this->container->invoke(LoadDiscoveryClasses::class, discoveryLocations: $this->discoveryLocations); return $this; } @@ -179,7 +176,9 @@ public function loadDiscovery(): self public function loadConfig(): self { $this->container->addInitializer(ConfigCacheInitializer::class); - $this->container->invoke(LoadConfig::class); + + $loadConfig = $this->container->get(LoadConfig::class, environment: Environment::guessFromEnvironment()); + $loadConfig(); return $this; } @@ -223,7 +222,7 @@ public function event(object $event): self public function registerEmergencyExceptionHandler(): self { - $environment = Environment::fromEnv(); + $environment = Environment::guessFromEnvironment(); // During tests, PHPUnit registers its own error handling. if ($environment->isTesting()) { @@ -232,7 +231,7 @@ public function registerEmergencyExceptionHandler(): self // In development, we want to register a developer-friendly error // handler as soon as possible to catch any kind of exception. - if (PHP_SAPI !== 'cli' && ! $environment->isProduction()) { + if (PHP_SAPI !== 'cli' && $environment->isLocal()) { new RegisterEmergencyExceptionHandler()->register(); } @@ -241,10 +240,10 @@ public function registerEmergencyExceptionHandler(): self public function registerExceptionHandler(): self { - $appConfig = $this->container->get(AppConfig::class); + $environment = $this->container->get(Environment::class); // During tests, PHPUnit registers its own error handling. - if ($appConfig->environment->isTesting()) { + if ($environment->isTesting()) { return $this; } diff --git a/packages/core/src/Kernel/LoadConfig.php b/packages/core/src/Kernel/LoadConfig.php index 951e812da..6398c44fa 100644 --- a/packages/core/src/Kernel/LoadConfig.php +++ b/packages/core/src/Kernel/LoadConfig.php @@ -4,8 +4,8 @@ namespace Tempest\Core\Kernel; -use Tempest\Core\AppConfig; use Tempest\Core\ConfigCache; +use Tempest\Core\Environment; use Tempest\Core\Kernel; use Tempest\Support\Arr\MutableArray; use Tempest\Support\Filesystem; @@ -17,7 +17,7 @@ public function __construct( private Kernel $kernel, private ConfigCache $cache, - private AppConfig $appConfig, + private Environment $environment, ) {} public function __invoke(): void @@ -52,9 +52,9 @@ public function find(): array return $configPaths ->filter(fn (string $path) => match (true) { - $this->appConfig->environment->isLocal() => ! Str\contains($path, [...$suffixes['production'], ...$suffixes['staging'], ...$suffixes['testing']]), - $this->appConfig->environment->isProduction() => ! Str\contains($path, [...$suffixes['staging'], ...$suffixes['testing'], ...$suffixes['development']]), - $this->appConfig->environment->isStaging() => ! Str\contains($path, [...$suffixes['testing'], ...$suffixes['development'], ...$suffixes['production']]), + $this->environment->isLocal() => ! Str\contains($path, [...$suffixes['production'], ...$suffixes['staging'], ...$suffixes['testing']]), + $this->environment->isProduction() => ! Str\contains($path, [...$suffixes['staging'], ...$suffixes['testing'], ...$suffixes['development']]), + $this->environment->isStaging() => ! Str\contains($path, [...$suffixes['testing'], ...$suffixes['development'], ...$suffixes['production']]), default => true, }) ->sortByCallback(function (string $path1, string $path2) use ($suffixes): int { diff --git a/packages/core/tests/EnvironmentTest.php b/packages/core/tests/EnvironmentTest.php new file mode 100644 index 000000000..11ae381e3 --- /dev/null +++ b/packages/core/tests/EnvironmentTest.php @@ -0,0 +1,45 @@ +assertSame(Environment::LOCAL, Environment::guessFromEnvironment()); + } + + #[Test] + public function throws_on_unknown_value(): void + { + putenv('ENVIRONMENT=unknown'); + + $this->expectException(EnvironmentValueWasInvalid::class); + + Environment::guessFromEnvironment(); + } + + #[Test] + public function can_be_resolved_from_container(): void + { + $container = new GenericContainer(); + $container->addInitializer(EnvironmentInitalizer::class); + + putenv('ENVIRONMENT=staging'); + $this->assertSame(Environment::STAGING, $container->get(Environment::class)); + + // ensure it's a singleton + putenv('ENVIRONMENT=production'); + $this->assertSame(Environment::STAGING, $container->get(Environment::class)); + } +} diff --git a/packages/discovery/src/DiscoveryItems.php b/packages/discovery/src/DiscoveryItems.php index 85ace2ef1..8b5e1b30c 100644 --- a/packages/discovery/src/DiscoveryItems.php +++ b/packages/discovery/src/DiscoveryItems.php @@ -17,7 +17,7 @@ public function __construct( private array $items = [], ) {} - public function addForLocation(DiscoveryLocation $location, array $values): self + public function addForLocation(DiscoveryLocation $location, iterable $values): self { $existingValues = $this->items[$location->path] ?? []; diff --git a/packages/http/src/Session/VerifyCsrfMiddleware.php b/packages/http/src/Session/VerifyCsrfMiddleware.php index 36753b8ba..d7b802949 100644 --- a/packages/http/src/Session/VerifyCsrfMiddleware.php +++ b/packages/http/src/Session/VerifyCsrfMiddleware.php @@ -6,6 +6,7 @@ use Tempest\Clock\Clock; use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\Core\Priority; use Tempest\Cryptography\Encryption\Encrypter; use Tempest\Cryptography\Encryption\Exceptions\EncryptionException; @@ -29,6 +30,7 @@ public function __construct( private Session $session, private AppConfig $appConfig, + private Environment $environment, private SessionConfig $sessionConfig, private CookieManager $cookies, private Clock $clock, @@ -60,7 +62,7 @@ private function shouldSkipCheck(Request $request): bool return true; } - if ($this->appConfig->environment->isTesting()) { + if ($this->environment->isTesting()) { return true; } diff --git a/packages/log/src/GenericLogger.php b/packages/log/src/GenericLogger.php index 147a74938..ba3ed5e63 100644 --- a/packages/log/src/GenericLogger.php +++ b/packages/log/src/GenericLogger.php @@ -8,7 +8,7 @@ use Monolog\Logger as Monolog; use Psr\Log\LogLevel as PsrLogLevel; use Stringable; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\EventBus\EventBus; final class GenericLogger implements Logger @@ -18,7 +18,7 @@ final class GenericLogger implements Logger public function __construct( private readonly LogConfig $logConfig, - private readonly AppConfig $appConfig, + private readonly Environment $environment, private readonly EventBus $eventBus, ) {} @@ -97,7 +97,7 @@ private function resolveDriver(LogChannel $channel, MonologLogLevel $level): Mon if (! isset($this->drivers[$key])) { $this->drivers[$key] = new Monolog( - name: $this->logConfig->prefix ?? $this->appConfig->environment->value, + name: $this->logConfig->prefix ?? $this->environment->value, handlers: $channel->getHandlers($level), processors: $channel->getProcessors(), ); diff --git a/packages/log/src/LoggerInitializer.php b/packages/log/src/LoggerInitializer.php index b3fc0bd5b..c879433b7 100644 --- a/packages/log/src/LoggerInitializer.php +++ b/packages/log/src/LoggerInitializer.php @@ -8,7 +8,7 @@ use Tempest\Container\Container; use Tempest\Container\DynamicInitializer; use Tempest\Container\Singleton; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\EventBus\EventBus; use Tempest\Reflection\ClassReflector; use UnitEnum; @@ -25,7 +25,7 @@ public function initialize(ClassReflector $class, null|string|UnitEnum $tag, Con { return new GenericLogger( logConfig: $container->get(LogConfig::class, $tag), - appConfig: $container->get(AppConfig::class), + environment: $container->get(Environment::class), eventBus: $container->get(EventBus::class), ); } diff --git a/packages/view/src/Components/x-icon.view.php b/packages/view/src/Components/x-icon.view.php index 4c0164045..d17497326 100644 --- a/packages/view/src/Components/x-icon.view.php +++ b/packages/view/src/Components/x-icon.view.php @@ -4,7 +4,7 @@ * @var string|null $class */ -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\Icon\Icon; use function Tempest\get; @@ -12,7 +12,7 @@ $class ??= null; $name ??= null; -$appConfig = get(AppConfig::class); +$environment = get(Environment::class); if ($name) { $svg = get(Icon::class)->render($name); @@ -20,7 +20,7 @@ $svg = null; } -if ($svg === null && $appConfig->environment->isLocal()) { +if ($svg === null && $environment->isLocal()) { $svg = ''; } diff --git a/packages/view/src/Elements/ViewComponentElement.php b/packages/view/src/Elements/ViewComponentElement.php index 85f97735e..eb491f737 100644 --- a/packages/view/src/Elements/ViewComponentElement.php +++ b/packages/view/src/Elements/ViewComponentElement.php @@ -183,7 +183,7 @@ public function compile(): string // A slot doesn't have any content, so we'll comment it out. // This is to prevent DOM parsing errors (slots in tags is one example, see #937) - return $this->environment->isProduction() ? '' : ''; + return $this->environment->isLocal() ? '' : ''; } $slotElement = $this->getSlotElement($slot->name); diff --git a/packages/view/src/Initializers/ElementFactoryInitializer.php b/packages/view/src/Initializers/ElementFactoryInitializer.php index b5f0fd469..f6eb55b6a 100644 --- a/packages/view/src/Initializers/ElementFactoryInitializer.php +++ b/packages/view/src/Initializers/ElementFactoryInitializer.php @@ -5,7 +5,7 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\View\Elements\ElementFactory; use Tempest\View\ViewConfig; @@ -15,8 +15,8 @@ final class ElementFactoryInitializer implements Initializer public function initialize(Container $container): ElementFactory { return new ElementFactory( - $container->get(ViewConfig::class), - $container->get(AppConfig::class)->environment, + viewConfig: $container->get(ViewConfig::class), + environment: $container->get(Environment::class), ); } } diff --git a/packages/view/src/Initializers/ViewCacheInitializer.php b/packages/view/src/Initializers/ViewCacheInitializer.php index 0d2280f15..3fcff7ad7 100644 --- a/packages/view/src/Initializers/ViewCacheInitializer.php +++ b/packages/view/src/Initializers/ViewCacheInitializer.php @@ -5,7 +5,7 @@ use Tempest\Container\Container; use Tempest\Container\Initializer; use Tempest\Container\Singleton; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\View\ViewCache; use function Tempest\env; @@ -16,20 +16,18 @@ final class ViewCacheInitializer implements Initializer public function initialize(Container $container): ViewCache { $viewCache = new ViewCache( - enabled: $this->shouldCacheBeEnabled( - $container->get(AppConfig::class)->environment->isProduction(), - ), + enabled: $this->shouldCacheBeEnabled(), ); return $viewCache; } - private function shouldCacheBeEnabled(bool $isProduction): bool + private function shouldCacheBeEnabled(): bool { if (env('INTERNAL_CACHES') === false) { return false; } - return (bool) env('VIEW_CACHE', default: $isProduction); + return (bool) env('VIEW_CACHE', default: Environment::guessFromEnvironment()); } } diff --git a/packages/vite/src/Vite.php b/packages/vite/src/Vite.php index 652e33629..fe21d7a39 100644 --- a/packages/vite/src/Vite.php +++ b/packages/vite/src/Vite.php @@ -5,7 +5,7 @@ namespace Tempest\Vite; use Tempest\Container\Container; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\Support\Filesystem; use Tempest\Support\Json; use Tempest\Vite\Exceptions\DevelopmentServerWasNotRunning; @@ -28,7 +28,7 @@ final class Vite private static ?Manifest $manifest = null; public function __construct( - private readonly AppConfig $appConfig, + private readonly Environment $environment, private readonly ViteConfig $viteConfig, private readonly Container $container, private readonly TagCompiler $tagCompiler, @@ -109,7 +109,7 @@ private function getManifest(): Manifest private function shouldUseManifest(): bool { - if ($this->appConfig->environment->isTesting() && ! $this->viteConfig->useManifestDuringTesting) { + if ($this->environment->isTesting() && ! $this->viteConfig->useManifestDuringTesting) { return false; } diff --git a/tests/Integration/Console/Middleware/CautionMiddlewareTest.php b/tests/Integration/Console/Middleware/CautionMiddlewareTest.php index b91f87c85..c98b2692b 100644 --- a/tests/Integration/Console/Middleware/CautionMiddlewareTest.php +++ b/tests/Integration/Console/Middleware/CautionMiddlewareTest.php @@ -4,7 +4,8 @@ namespace Tests\Tempest\Integration\Console\Middleware; -use Tempest\Core\AppConfig; +use PHPUnit\Framework\Attributes\Test; +use PHPUnit\Framework\Attributes\TestWith; use Tempest\Core\Environment; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; @@ -13,17 +14,22 @@ */ final class CautionMiddlewareTest extends FrameworkIntegrationTestCase { - public function test_in_local(): void + #[Test] + public function in_local(): void { + $this->container->singleton(Environment::class, Environment::LOCAL); + $this->console ->call('caution') ->assertContains('CAUTION confirmed'); } - public function test_in_production(): void + #[Test] + #[TestWith([Environment::PRODUCTION])] + #[TestWith([Environment::STAGING])] + public function in_caution_environments(Environment $environment): void { - $appConfig = $this->container->get(AppConfig::class); - $appConfig->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, $environment); $this->console ->call('caution') diff --git a/tests/Integration/Core/AppConfigTest.php b/tests/Integration/Core/AppConfigTest.php index 2d5a24f45..a47e9ceaf 100644 --- a/tests/Integration/Core/AppConfigTest.php +++ b/tests/Integration/Core/AppConfigTest.php @@ -4,8 +4,8 @@ namespace Tests\Tempest\Integration\Core; +use PHPUnit\Framework\Attributes\Test; use Tempest\Core\AppConfig; -use Tempest\Core\Environment; use Tests\Tempest\Integration\FrameworkIntegrationTestCase; /** @@ -13,11 +13,12 @@ */ final class AppConfigTest extends FrameworkIntegrationTestCase { - public function test_defaults(): void + #[Test] + public function defaults(): void { $appConfig = $this->container->get(AppConfig::class); - $this->assertSame(Environment::TESTING, $appConfig->environment); $this->assertSame('', $appConfig->baseUri); + $this->assertSame(null, $appConfig->name); } } diff --git a/tests/Integration/Core/Config/LoadConfigTest.php b/tests/Integration/Core/Config/LoadConfigTest.php index 10a20779e..365fff006 100644 --- a/tests/Integration/Core/Config/LoadConfigTest.php +++ b/tests/Integration/Core/Config/LoadConfigTest.php @@ -2,7 +2,6 @@ namespace Tests\Tempest\Integration\Core\Config; -use Tempest\Core\AppConfig; use Tempest\Core\ConfigCache; use Tempest\Core\Environment; use Tempest\Core\Kernel\LoadConfig; @@ -64,7 +63,8 @@ public function test_non_production_configs_are_discarded_in_production(): void 'db.config.php', ]); - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); + $config = $this->container->get(LoadConfig::class)->find(); $this->assertCount(2, $config); @@ -82,7 +82,8 @@ public function test_non_staging_configs_are_discarded_in_staging(): void 'db.config.php', ]); - $this->container->get(AppConfig::class)->environment = Environment::STAGING; + $this->container->singleton(Environment::class, Environment::STAGING); + $config = $this->container->get(LoadConfig::class)->find(); $this->assertCount(2, $config); @@ -100,7 +101,8 @@ public function test_non_dev_configs_are_discarded_in_dev(): void 'db.config.php', ]); - $this->container->get(AppConfig::class)->environment = Environment::LOCAL; + $this->container->singleton(Environment::class, Environment::LOCAL); + $config = $this->container->get(LoadConfig::class)->find(); $this->assertCount(3, $config); diff --git a/tests/Integration/Framework/Commands/DatabaseSeedCommandTest.php b/tests/Integration/Framework/Commands/DatabaseSeedCommandTest.php index c2816e717..7e3146c57 100644 --- a/tests/Integration/Framework/Commands/DatabaseSeedCommandTest.php +++ b/tests/Integration/Framework/Commands/DatabaseSeedCommandTest.php @@ -2,7 +2,6 @@ namespace Tests\Tempest\Integration\Framework\Commands; -use Tempest\Core\AppConfig; use Tempest\Core\Environment; use Tempest\Database\Config\SeederConfig; use Tempest\Database\Migrations\CreateMigrationsTable; @@ -129,8 +128,7 @@ public function test_seed_via_migrate_fresh(): void public function test_db_seed_caution(): void { - $appConfig = $this->container->get(AppConfig::class); - $appConfig->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->console ->call('migrate:fresh --seed --all') diff --git a/tests/Integration/Http/CsrfTest.php b/tests/Integration/Http/CsrfTest.php index b3cffa1db..58a8ef99e 100644 --- a/tests/Integration/Http/CsrfTest.php +++ b/tests/Integration/Http/CsrfTest.php @@ -3,7 +3,6 @@ namespace Tests\Tempest\Integration\Http; use PHPUnit\Framework\Attributes\TestWith; -use Tempest\Core\AppConfig; use Tempest\Core\Environment; use Tempest\Cryptography\Encryption\Encrypter; use Tempest\Http\GenericRequest; @@ -20,7 +19,7 @@ final class CsrfTest extends FrameworkIntegrationTestCase { public function test_csrf_is_sent_as_cookie(): void { - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $token = $this->container->get(Session::class)->get(Session::CSRF_TOKEN_KEY); @@ -37,7 +36,7 @@ public function test_throws_when_missing_in_write_verbs(Method $method): void { $this->expectException(CsrfTokenDidNotMatch::class); - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->http->sendRequest(new GenericRequest($method, uri: '/test')); } @@ -46,7 +45,7 @@ public function test_throws_when_missing_in_write_verbs(Method $method): void #[TestWith([Method::HEAD])] public function test_allows_missing_in_read_verbs(Method $method): void { - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->http ->sendRequest(new GenericRequest($method, uri: '/test')) @@ -57,7 +56,7 @@ public function test_throws_when_mismatch_from_body(): void { $this->expectException(CsrfTokenDidNotMatch::class); - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->container->get(Session::class)->set(Session::CSRF_TOKEN_KEY, 'abc'); $this->http->post('/test', [Session::CSRF_TOKEN_KEY => 'def']); @@ -67,7 +66,7 @@ public function test_throws_when_mismatch_from_header(): void { $this->expectException(CsrfTokenDidNotMatch::class); - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->container->get(Session::class)->set(Session::CSRF_TOKEN_KEY, 'abc'); $this->http->post('/test', [Session::CSRF_TOKEN_KEY => 'def']); @@ -75,7 +74,7 @@ public function test_throws_when_mismatch_from_header(): void public function test_matches_from_body(): void { - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $session = $this->container->get(Session::class); @@ -86,7 +85,7 @@ public function test_matches_from_body(): void public function test_matches_from_header_when_encrypted(): void { - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $session = $this->container->get(Session::class); // Encrypt the token as it would be in a real request @@ -103,7 +102,7 @@ public function test_matches_from_header_when_encrypted(): void public function test_throws_csrf_exception_when_header_is_non_serialized_hash(): void { $this->expectException(CsrfTokenDidNotMatch::class); - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $session = $this->container->get(Session::class); // simulate a non-serialized hash diff --git a/tests/Integration/Log/GenericLoggerTest.php b/tests/Integration/Log/GenericLoggerTest.php index ffcab0b46..05d392c27 100644 --- a/tests/Integration/Log/GenericLoggerTest.php +++ b/tests/Integration/Log/GenericLoggerTest.php @@ -11,7 +11,7 @@ use PHPUnit\Framework\Attributes\Test; use Psr\Log\LogLevel as PsrLogLevel; use ReflectionClass; -use Tempest\Core\AppConfig; +use Tempest\Core\Environment; use Tempest\DateTime\Duration; use Tempest\EventBus\EventBus; use Tempest\Log\Channels\AppendLogChannel; @@ -35,8 +35,8 @@ final class GenericLoggerTest extends FrameworkIntegrationTestCase get => $this->container->get(EventBus::class); } - private AppConfig $appConfig { - get => $this->container->get(AppConfig::class); + private Environment $environment { + get => $this->container->get(Environment::class); } #[PreCondition] @@ -58,7 +58,7 @@ public function simple_log_config(): void $config = new SimpleLogConfig($filePath, prefix: 'tempest'); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); @@ -72,18 +72,18 @@ public function daily_log_config(): void $filePath = __DIR__ . '/logs/tempest-' . date('Y-m-d') . '.log'; $config = new DailyLogConfig(__DIR__ . '/logs/tempest.log', prefix: 'tempest'); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); $this->assertStringContainsString('test', Filesystem\read_file($filePath)); $clock->plus(Duration::day()); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->info('test'); $clock->plus(Duration::days(2)); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->info('test'); } @@ -93,7 +93,7 @@ public function weekly_log_config(): void $filePath = __DIR__ . '/logs/tempest-' . date('Y-W') . '.log'; $config = new WeeklyLogConfig(__DIR__ . '/logs/tempest.log', prefix: 'tempest'); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); @@ -114,7 +114,7 @@ public function multiple_same_log_channels(): void prefix: 'tempest', ); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->info('test'); $this->assertFileExists($filePath); @@ -136,7 +136,7 @@ public function log_levels(mixed $level, string $expected): void prefix: 'tempest', ); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->log($level, 'test'); $this->assertFileExists($filePath); @@ -155,7 +155,7 @@ public function message_logged_emitted(LogLevel $level, string $_expected): void $this->assertSame(['foo' => 'bar'], $event->context); }); - $logger = new GenericLogger(new NullLogConfig(), $this->appConfig, $this->bus); + $logger = new GenericLogger(new NullLogConfig(), $this->environment, $this->bus); $logger->log($level, 'This is a log message of level: ' . $level->value, context: ['foo' => 'bar']); } @@ -168,7 +168,7 @@ public function different_log_levels(): void prefix: 'tempest', ); - $logger = new GenericLogger($config, $this->appConfig, $this->bus); + $logger = new GenericLogger($config, $this->environment, $this->bus); $logger->critical('critical'); $logger->debug('debug'); diff --git a/tests/Integration/View/Components/IconComponentTest.php b/tests/Integration/View/Components/IconComponentTest.php index 961b3e00a..b44486850 100644 --- a/tests/Integration/View/Components/IconComponentTest.php +++ b/tests/Integration/View/Components/IconComponentTest.php @@ -4,7 +4,6 @@ namespace Tests\Tempest\Integration\View\Components; -use Tempest\Core\AppConfig; use Tempest\Core\ConfigCache; use Tempest\Core\Environment; use Tempest\DateTime\Duration; @@ -127,7 +126,7 @@ public function test_it_renders_a_debug_comment_in_local_env_when_icon_does_not_ ->willReturn(new GenericResponse(status: Status::NOT_FOUND, body: '')); $this->container->register(HttpClient::class, fn () => $mockHttpClient); - $this->container->singleton(AppConfig::class, fn () => new AppConfig(environment: Environment::LOCAL)); + $this->container->singleton(Environment::class, Environment::LOCAL); $this->assertSame( '', @@ -145,7 +144,7 @@ public function test_it_renders_an_empty_string__in_non_local_env_when_icon_does ->willReturn(new GenericResponse(status: Status::NOT_FOUND, body: '')); $this->container->register(HttpClient::class, fn () => $mockHttpClient); - $this->container->singleton(AppConfig::class, fn () => new AppConfig(environment: Environment::PRODUCTION)); + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->assertSame( '', diff --git a/tests/Integration/View/ViewComponentTest.php b/tests/Integration/View/ViewComponentTest.php index 504b72de0..c02b8b179 100644 --- a/tests/Integration/View/ViewComponentTest.php +++ b/tests/Integration/View/ViewComponentTest.php @@ -6,7 +6,6 @@ use Generator; use PHPUnit\Framework\Attributes\DataProvider; -use Tempest\Core\AppConfig; use Tempest\Core\Environment; use Tempest\Http\Session\Session; use Tempest\Validation\Rules\IsAlphaNumeric; @@ -497,6 +496,8 @@ public function test_full_html_document_as_component(): void public function test_empty_slots_are_commented_out(): void { + $this->container->singleton(Environment::class, Environment::LOCAL); + $this->registerViewComponent('x-layout', <<<'HTML' @@ -519,7 +520,7 @@ public function test_empty_slots_are_commented_out(): void public function test_empty_slots_are_removed_in_production(): void { - $this->container->get(AppConfig::class)->environment = Environment::PRODUCTION; + $this->container->singleton(Environment::class, Environment::PRODUCTION); $this->registerViewComponent('x-layout', <<<'HTML' @@ -886,9 +887,9 @@ public function test_nested_slots_with_escaping(): void $this->registerViewComponent('x-b', <<<'HTML' - {{ get(AppConfig::class)->environment->value }} + {{ get(Environment::class)->value }} HTML); $html = $this->render(<<<'HTML'