diff --git a/docs/1-essentials/06-configuration.md b/docs/1-essentials/06-configuration.md index 6d7cf10d75..8f997b463b 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 f2095d477f..16f9261994 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 42158ecb69..7df85538b0 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 e6b6d5d833..3842ea9b5a 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 ab1c1dc833..20e623e658 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 f9f389a694..00147bb94c 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 351df4cdf4..584f123fd1 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 1d92321a41..f0ebdb8a09 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 93e758436b..658835b69e 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 0000000000..cac26c694c --- /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 9d9995857a..77fd56a13e 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 fe3e24e01f..17afb0e891 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 d7514b03bb..d1a4cf32ea 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 951e812da6..6398c44fa8 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 0000000000..11ae381e38 --- /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 85ace2ef14..8b5e1b30ca 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 36753b8baf..d7b8029499 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 147a74938e..ba3ed5e630 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 b3fc0bd5b1..c879433b76 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 4c01640456..d174973262 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 85f97735e5..eb491f737f 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 b5f0fd4695..f6eb55b6a0 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 0d2280f15d..3fcff7ad7a 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 652e33629b..fe21d7a39b 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 b91f87c85e..c98b2692b0 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 2d5a24f450..a47e9ceafe 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 10a20779ef..365fff006f 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 c2816e7176..7e3146c570 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 b3cffa1dbd..58a8ef99e7 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 ffcab0b465..05d392c276 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 961b3e00a4..b444868504 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 504b72de0d..c02b8b179c 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'