Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ Thumbs.db

# Codeception C3
c3.php

# Local environment configuration
.env
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions composer-dependency-analyser.php
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion public/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
19 changes: 9 additions & 10 deletions src/Environment.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
9 changes: 0 additions & 9 deletions src/autoload.php

This file was deleted.

16 changes: 16 additions & 0 deletions src/bootstrap.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

use App\Environment;

require_once dirname(__DIR__) . '/vendor/autoload.php';

// Load .env for non-Docker/non-container environments.
// Existing process environment variables take precedence (Docker, CI, server config).
// phpdotenv is a dev dependency — not available in production (composer install --no-dev).
if (empty($_ENV['APP_ENV']) && class_exists(\Dotenv\Dotenv::class)) {
\Dotenv\Dotenv::createImmutable(dirname(__DIR__))->safeLoad();
}

Environment::prepare();
153 changes: 152 additions & 1 deletion tests/Unit/EnvironmentTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, string|false> */
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();
}
}
2 changes: 1 addition & 1 deletion yii
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading