diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..1bf6a06b --- /dev/null +++ b/.env.example @@ -0,0 +1,5 @@ +# Local environment configuration. +# Copy this file to .env and adjust as needed. +# In production, set environment variables via server or container configuration instead. +APP_ENV=dev +APP_DEBUG=true diff --git a/.gitignore b/.gitignore index 806d488f..d3abd8ec 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,6 @@ Thumbs.db # Codeception C3 c3.php + +# Local environment configuration +.env diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c92f5aa..7d98b40a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Chg #449: Remove `yiisoft/data-response` dependency (@vjik) - Chg #443: Do not write logs to file since that's not needed for both Docker and `./yii serve` (@samdark) - Enh #456: Add "service update paused" case for swarm deployment log parsing (@samdark) +- Enh #417: Add `.env` for development without Docker (@samdark) ## 1.2.0 February 20, 2026 diff --git a/README.md b/README.md index ab4d55ab..913d9290 100644 --- a/README.md +++ b/README.md @@ -47,15 +47,25 @@ cd myproject > [!NOTE] > Ensure that Composer is executed with the same PHP version that will be used to run the application. +Copy the example environment file and adjust as needed: + +```shell +cp .env.example .env +``` + To run the app: ```shell -APP_ENV=dev ./yii serve +./yii serve ``` Now you should be able to access the application through the URL printed to console. Usually it is `http://localhost:8080`. +> [!TIP] +> The `.env` file is for local development only and is excluded from version control. +> In production, configure environment variables via your server or container instead. + ### Installation with Docker > [!WARNING] @@ -110,6 +120,7 @@ src/ Application source code. Web/ Web-specific code (actions, handlers, layout). Shared/ Shared web components. Layout/ Layout components and templates. + bootstrap.php Application bootstrap (autoloading, environment setup). Environment.php Environment configuration class. tests/ A set of Codeception tests for the application. Console/ Console command tests. diff --git a/composer-dependency-analyser.php b/composer-dependency-analyser.php index 25a4db2c..852c0fd6 100644 --- a/composer-dependency-analyser.php +++ b/composer-dependency-analyser.php @@ -10,6 +10,8 @@ ->disableComposerAutoloadPathScan() ->setFileExtensions(['php']) ->addPathToScan($root . '/src', isDev: false) + ->addPathToExclude($root . '/src/bootstrap.php') + ->addPathToScan($root . '/src/bootstrap.php', isDev: true) ->addPathToScan($root . '/config', isDev: false) ->addPathToScan($root . '/public/index.php', isDev: false) ->addPathToScan($root . '/yii', isDev: false) diff --git a/composer.json b/composer.json index 09c50b8a..cc8879d4 100644 --- a/composer.json +++ b/composer.json @@ -66,6 +66,7 @@ "yiisoft/yii-view-renderer": "^7.4.1" }, "require-dev": { + "vlucas/phpdotenv": "^5.6", "codeception/c3": "^2.9", "codeception/codeception": "^5.3.5", "codeception/module-asserts": "^3.3.0", diff --git a/public/index.php b/public/index.php index e557ad8f..9a54b70e 100644 --- a/public/index.php +++ b/public/index.php @@ -12,7 +12,7 @@ $root = dirname(__DIR__); -require_once $root . '/src/autoload.php'; +require_once $root . '/src/bootstrap.php'; if (Environment::appC3()) { $c3 = $root . '/c3.php'; diff --git a/src/Environment.php b/src/Environment.php index 62359b7f..8482e972 100644 --- a/src/Environment.php +++ b/src/Environment.php @@ -78,19 +78,18 @@ public static function appDebug(): bool private static function setEnvironment(): void { - $environment = self::getRawValue('APP_ENV'); + $environment = self::getRawValue('APP_ENV') ?: self::PROD; if (!in_array($environment, self::ENVIRONMENTS, true)) { - if ($environment === null) { - $message = 'APP_ENV environment variable is empty.'; - } else { - $message = sprintf('APP_ENV="%s" environment is invalid.', $environment); - } - - $message .= sprintf(' Valid values are "%s".', implode('", "', self::ENVIRONMENTS)); - - throw new RuntimeException($message); + throw new RuntimeException( + sprintf( + 'APP_ENV="%s" is invalid. Valid values are "%s".', + $environment, + implode('", "', self::ENVIRONMENTS), + ), + ); } + self::$values['APP_ENV'] = $environment; } diff --git a/src/autoload.php b/src/autoload.php deleted file mode 100644 index 3ed4f694..00000000 --- a/src/autoload.php +++ /dev/null @@ -1,9 +0,0 @@ -safeLoad(); +} + +Environment::prepare(); diff --git a/tests/Unit/EnvironmentTest.php b/tests/Unit/EnvironmentTest.php index 4950bc81..3e1ae71a 100644 --- a/tests/Unit/EnvironmentTest.php +++ b/tests/Unit/EnvironmentTest.php @@ -6,18 +6,169 @@ use App\Environment; use Codeception\Test\Unit; +use RuntimeException; +use function PHPUnit\Framework\assertFalse; +use function PHPUnit\Framework\assertNull; use function PHPUnit\Framework\assertSame; +use function PHPUnit\Framework\assertTrue; final class EnvironmentTest extends Unit { + /** @var array */ + private array $originalEnv = []; + protected function _before(): void { + foreach (['APP_ENV', 'APP_DEBUG', 'APP_C3', 'APP_HOST_PATH'] as $key) { + $this->originalEnv[$key] = getenv($key); + } + } + + protected function _after(): void + { + foreach ($this->originalEnv as $key => $value) { + if ($value === false) { + putenv($key); + unset($_ENV[$key]); + } else { + putenv("$key=$value"); + $_ENV[$key] = $value; + } + } Environment::prepare(); } - public function testAppEnv(): void + public function testAppEnvIsReadFromEnvironment(): void { + $this->setEnv('APP_ENV', 'test'); + assertSame('test', Environment::appEnv()); } + + public function testAppEnvDefaultsToProdWhenNotSet(): void + { + $this->unsetEnv('APP_ENV'); + + assertSame('prod', Environment::appEnv()); + } + + public function testAppEnvDefaultsToProdWhenEmpty(): void + { + $this->setEnv('APP_ENV', ''); + + assertSame('prod', Environment::appEnv()); + } + + public function testInvalidAppEnvThrows(): void + { + $this->unsetEnv('APP_ENV'); + putenv('APP_ENV=staging'); + + try { + Environment::prepare(); + $this->fail('Expected RuntimeException was not thrown.'); + } catch (RuntimeException $e) { + assertSame('APP_ENV="staging" is invalid. Valid values are "dev", "test", "prod".', $e->getMessage()); + } finally { + putenv('APP_ENV'); + } + } + + public function testIsDev(): void + { + $this->setEnv('APP_ENV', 'dev'); + + assertTrue(Environment::isDev()); + assertFalse(Environment::isTest()); + assertFalse(Environment::isProd()); + } + + public function testIsTest(): void + { + $this->setEnv('APP_ENV', 'test'); + + assertTrue(Environment::isTest()); + assertFalse(Environment::isDev()); + assertFalse(Environment::isProd()); + } + + public function testIsProd(): void + { + $this->setEnv('APP_ENV', 'prod'); + + assertTrue(Environment::isProd()); + assertFalse(Environment::isDev()); + assertFalse(Environment::isTest()); + } + + public function testAppDebugDefaultsToFalse(): void + { + $this->unsetEnv('APP_DEBUG'); + + assertFalse(Environment::appDebug()); + } + + public function testAppDebugTrue(): void + { + $this->setEnv('APP_DEBUG', 'true'); + + assertTrue(Environment::appDebug()); + } + + public function testAppDebugFalse(): void + { + $this->setEnv('APP_DEBUG', 'false'); + + assertFalse(Environment::appDebug()); + } + + public function testAppC3DefaultsToFalse(): void + { + $this->unsetEnv('APP_C3'); + + assertFalse(Environment::appC3()); + } + + public function testAppC3True(): void + { + $this->setEnv('APP_C3', 'true'); + + assertTrue(Environment::appC3()); + } + + public function testAppHostPathDefaultsToNull(): void + { + $this->unsetEnv('APP_HOST_PATH'); + + assertNull(Environment::appHostPath()); + } + + public function testAppHostPathIsRead(): void + { + $this->setEnv('APP_HOST_PATH', '/projects/myapp'); + + assertSame('/projects/myapp', Environment::appHostPath()); + } + + public function testAppHostPathEmptyStringTreatedAsNull(): void + { + $this->setEnv('APP_HOST_PATH', ''); + + assertNull(Environment::appHostPath()); + } + + private function setEnv(string $key, string $value): void + { + putenv("$key=$value"); + $_ENV[$key] = $value; + Environment::prepare(); + } + + private function unsetEnv(string $key): void + { + putenv($key); + unset($_ENV[$key]); + Environment::prepare(); + } } diff --git a/yii b/yii index 8a928a77..6f1fc029 100755 --- a/yii +++ b/yii @@ -6,7 +6,7 @@ declare(strict_types=1); use App\Environment; use Yiisoft\Yii\Runner\Console\ConsoleApplicationRunner; -require_once __DIR__ . '/src/autoload.php'; +require_once __DIR__ . '/src/bootstrap.php'; // Run console application runner $runner = new ConsoleApplicationRunner(