Skip to content

Commit 2b9414c

Browse files
committed
fix(env-loader): prevent dotenv override and preserve runtime/test variables
Ensure EnvironmentLoader no longer overrides existing $_ENV / putenv values. Added pre-load snapshot + post-load restore mechanism to protect local, CI, and PHPUnit variables from unexpected .env overwrites. Improves reliability across libraries and guarantees test isolation.
1 parent ff871e8 commit 2b9414c

File tree

1 file changed

+43
-35
lines changed

1 file changed

+43
-35
lines changed

src/Core/EnvironmentLoader.php

Lines changed: 43 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,43 +22,42 @@
2222
/**
2323
* ⚙️ **Class EnvironmentLoader**
2424
*
25-
* 🧩 **Purpose:**
26-
* Provides a unified, consistent, and secure method for loading environment
27-
* variables across all **Maatify** projects and libraries.
28-
* Ensures correct prioritization between `.env` variants, safe immutability,
29-
* and automatic timezone setup post-load.
25+
* 🎯 **Purpose:**
26+
* Provides a robust, unified mechanism for loading environment variables across
27+
* all **Maatify** libraries and applications, ensuring consistency, safety, and
28+
* predictable configuration behavior.
3029
*
31-
* 🧠 **Priority Loading Order:**
32-
* 1️⃣ `.env.local` — Local developer overrides
33-
* 2️⃣ `.env.testing` — Test environment variables
34-
* 3️⃣ `.env` — Default fallback for production or staging
35-
* 4️⃣ `.env.example` — Used as a last-resort fallback for validation or defaults
30+
* 🧠 **Core Responsibilities:**
31+
* - Load environment variables from the correct file (`.env.local`, `.env.testing`, `.env`, or `.env.example`).
32+
* - Prevent overriding system or CI-defined environment variables (immutable mode).
33+
* - Apply the application timezone automatically after loading.
34+
* - Throw explicit errors when no `.env` file is found.
3635
*
37-
* **Key Features:**
38-
* - Loads the first available file in the priority list.
39-
* - Prevents overwriting of already defined environment variables (immutable mode).
40-
* - Automatically applies application timezone via `date_default_timezone_set()`.
41-
* - Throws clear exception if no `.env` file is found.
36+
* 🧩 **Priority Load Order:**
37+
* 1️⃣ `.env.local` — local developer overrides
38+
* 2️⃣ `.env.testing` — test environment variables
39+
* 3️⃣ `.env` — default environment file
40+
* 4️⃣ `.env.example` — fallback for defaults or CI builds
4241
*
43-
* ⚙️ **Example Usage:**
42+
* **Example Usage:**
4443
* ```php
4544
* use Maatify\Bootstrap\Core\EnvironmentLoader;
4645
*
47-
* // Initialize loader at the project root
46+
* // Initialize and load environment variables
4847
* $env = new EnvironmentLoader(__DIR__ . '/../');
4948
* $env->load();
5049
*
51-
* echo 'Environment: ' . $_ENV['APP_ENV'] ?? 'unknown';
50+
* echo 'Environment: ' . ($_ENV['APP_ENV'] ?? 'unknown');
5251
* ```
5352
*
5453
* @package Maatify\Bootstrap\Core
5554
*/
5655
final class EnvironmentLoader
5756
{
5857
/**
59-
* 📂 Base directory containing environment files.
58+
* 📂 **Base Directory Path**
6059
*
61-
* Typically this is the project root directory.
60+
* The directory that contains the environment files. Typically the project root.
6261
*
6362
* @var string
6463
*/
@@ -67,46 +66,55 @@ public function __construct(private readonly string $basePath)
6766
}
6867

6968
/**
70-
* 🎯 Load the appropriate `.env` file based on Maatify priority rules.
69+
* 🚀 **Load Environment Variables**
7170
*
72-
* The loader iterates over `.env.local`, `.env.testing`, `.env`, and `.env.example`
73-
* in order, and loads the first existing file it encounters.
74-
* It uses **immutable mode** to ensure that previously defined environment
75-
* variables (via system or CI/CD) are not overridden.
71+
* Loads environment variables based on the Maatify priority rules.
72+
* The first matching file among `.env.local`, `.env.testing`, `.env`, or `.env.example`
73+
* is loaded using **immutable mode**, meaning system-level environment variables
74+
* (e.g., from Docker, CI/CD, or OS) will not be overridden.
7675
*
77-
* 🧠 After successful loading, the application timezone is automatically
78-
* set using the value of `APP_TIMEZONE` or defaults to **Africa/Cairo**.
76+
* 🧠 **Post-Load Behavior:**
77+
* - Ensures system-defined variables remain intact.
78+
* - Automatically applies timezone via `date_default_timezone_set()`.
79+
* - Throws clear exception if no `.env` file exists.
7980
*
80-
* 🚫 Throws:
81-
* - `Exception` when no environment file is found in the provided base path.
81+
* @throws Exception When no valid environment file is found.
8282
*
83-
* @throws Exception If no `.env` file exists in the given directory.
8483
* @return void
8584
*/
8685
public function load(): void
8786
{
88-
// 🔍 Check environment files in order of precedence
87+
// 🔍 Priority order for environment files
8988
$envFiles = ['.env.local', '.env.testing', '.env', '.env.example'];
9089
$loaded = false;
9190

91+
// Preserve pre-existing environment variables
92+
$existing = $_ENV;
93+
9294
foreach ($envFiles as $file) {
9395
$path = $this->basePath . DIRECTORY_SEPARATOR . $file;
9496

95-
// ✅ Load the first available file and stop checking further
97+
// ✅ Load first available file, stop after success
9698
if (is_file($path)) {
97-
// 🧩 Use Dotenv's immutable mode for safety (prevents accidental overrides)
99+
// 🧩 Load in immutable mode prevents overriding pre-defined variables
98100
Dotenv::createImmutable($this->basePath, $file)->load();
99101
$loaded = true;
100102
break;
101103
}
102104
}
103105

104-
// 🚫 Throw if no valid environment file was found
106+
// 🚫 Throw if no valid .env file found
105107
if (! $loaded) {
106108
throw new Exception('No .env file found in ' . $this->basePath);
107109
}
108110

109-
// 🕒 Set timezone from environment or default to Africa/Cairo
111+
// ♻️ Restore original environment variables (maintain immutability guarantee)
112+
foreach ($existing as $key => $value) {
113+
$_ENV[$key] = $value;
114+
putenv("$key=$value");
115+
}
116+
117+
// 🕒 Apply timezone setting (default: Africa/Cairo)
110118
$timezone = $_ENV['APP_TIMEZONE']
111119
?? $_SERVER['APP_TIMEZONE']
112120
?? 'Africa/Cairo';

0 commit comments

Comments
 (0)