├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── src ├── DotEnv.php └── Processor │ ├── AbstractProcessor.php │ ├── BooleanProcessor.php │ ├── IProcessor.php │ ├── NullProcessor.php │ ├── NumberProcessor.php │ └── QuotedProcessor.php └── tests ├── DotenvTest.php └── env ├── .env.boolean ├── .env.default ├── .env.number └── .env.quotes /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 F. Michel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP-DotEnv 2 | 3 | PHP-DotEnv is a lightweight PHP library designed to simplify the management of environment variables in your PHP applications. It provides an elegant solution for loading configuration values from a `.env` file into the environment variables accessible via `getenv()`, `$_ENV`, and `$_SERVER`. This documentation aims to guide you through the installation, usage, and features of PHP-DotEnv. 4 | 5 | ## Installation 6 | 7 | To install PHP-DotEnv, you can use [Composer](https://getcomposer.org/), the dependency manager for PHP. 8 | 9 | ### Composer Require 10 | ```bash 11 | composer require phpdevcommunity/php-dotenv 12 | ``` 13 | 14 | ## Requirements 15 | 16 | - PHP version 7.4 or higher 17 | 18 | ## Usage 19 | 20 | ### 1. Define Environment Variables 21 | 22 | Before using PHP-DotEnv, you need to define your environment variables in a `.env` file. This file should be placed in the root directory of your project. Each line in the file should follow the `KEY=VALUE` format. 23 | 24 | ```dotenv 25 | APP_ENV=dev 26 | DATABASE_DNS=mysql:host=localhost;dbname=test; 27 | DATABASE_USER="root" 28 | DATABASE_PASSWORD=root 29 | MODULE_ENABLED=true 30 | NUMBER_LITERAL=0 31 | NULL_VALUE=null 32 | ``` 33 | 34 | ### 2. Load the Variables 35 | 36 | After defining your environment variables, you can load them into your PHP application using PHP-DotEnv. 37 | 38 | ```php 39 | load(); 45 | ``` 46 | 47 | ### 3. Access Environment Variables 48 | 49 | Once loaded, you can access the environment variables using PHP's `getenv()` function. 50 | 51 | ```php 52 | /** 53 | * Retrieve the value of DATABASE_DNS 54 | */ 55 | var_dump(getenv('DATABASE_DNS')); 56 | ``` 57 | 58 | ### Automatic Type Conversion 59 | 60 | PHP-DotEnv provides automatic type conversion for certain types of values: 61 | 62 | - Booleans: Processed as `true` or `false`. 63 | - Quoted Strings: Surrounding quotes are removed. 64 | - Null Values: Converted to `null`. 65 | - Numeric Values: Converted to integers or floats. 66 | 67 | ## Processors 68 | 69 | PHP-DotEnv allows you to define custom processors to handle specific types of values in your `.env` file. These processors enable you to control how values are parsed and converted. 70 | 71 | ### BooleanProcessor 72 | 73 | The `BooleanProcessor` converts boolean values specified in the `.env` file to PHP boolean types (`true` or `false`). 74 | 75 | ```dotenv 76 | MODULE_ENABLED=true 77 | ``` 78 | 79 | ### QuotedProcessor 80 | 81 | The `QuotedProcessor` removes surrounding quotes from quoted strings in the `.env` file. 82 | 83 | ```dotenv 84 | DATABASE_USER="root" 85 | ``` 86 | 87 | ### NullProcessor 88 | 89 | The `NullProcessor` converts the string "null" to the PHP `null` value. 90 | 91 | ```dotenv 92 | NULL_VALUE=null 93 | ``` 94 | 95 | ### NumberProcessor 96 | 97 | The `NumberProcessor` converts numeric values to integers or floats. 98 | 99 | ```dotenv 100 | NUMBER_LITERAL=0 101 | ``` 102 | 103 | ## Conclusion 104 | 105 | PHP-DotEnv offers a straightforward and efficient solution for managing environment variables in PHP applications. By providing automatic type conversion and customizable processors, it simplifies the process of loading and handling configuration values from `.env` files. Whether you're working on a small project or a large-scale application, PHP-DotEnv can help streamline your development process and ensure smooth configuration management. Explore its features, integrate it into your projects, and experience the convenience it brings to your PHP development workflow. 106 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpdevcommunity/php-dotenv", 3 | "description": "PHP-DotEnv is a lightweight PHP library designed to simplify the management of environment variables in your PHP applications.", 4 | "type": "library", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "F. Michel", 9 | "homepage": "https://www.phpdevcommunity.com" 10 | } 11 | ], 12 | "autoload": { 13 | "psr-4": { 14 | "PhpDevCommunity\\": "src", 15 | "Test\\PhpDevCommunity\\": "tests" 16 | } 17 | }, 18 | "require": { 19 | "php": ">=7.4" 20 | }, 21 | "minimum-stability": "alpha", 22 | "require-dev": { 23 | "phpdevcommunity/unitester": "^0.1.0@alpha" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/DotEnv.php: -------------------------------------------------------------------------------- 1 | path = $path; 36 | 37 | $this->setProcessors($processors); 38 | } 39 | 40 | /** 41 | * Loads the configuration data from the specified file path. 42 | * Parses the values into $_SERVER and $_ENV arrays, skipping empty and commented lines. 43 | */ 44 | public function load(): void 45 | { 46 | if (!is_readable($this->path)) { 47 | throw new RuntimeException(sprintf('%s file is not readable', $this->path)); 48 | } 49 | 50 | $lines = file($this->path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); 51 | foreach ($lines as $line) { 52 | 53 | if (strpos(trim($line), '#') === 0) { 54 | continue; 55 | } 56 | 57 | list($name, $value) = explode('=', $line, 2); 58 | $name = trim($name); 59 | $value = $this->processValue($value); 60 | 61 | if (!array_key_exists($name, $_SERVER) && !array_key_exists($name, $_ENV)) { 62 | putenv(sprintf('%s=%s', $name, $value)); 63 | $_ENV[$name] = $value; 64 | $_SERVER[$name] = $value; 65 | } 66 | } 67 | } 68 | 69 | private function setProcessors(?array $processors = null): void 70 | { 71 | /** 72 | * Fill with default processors 73 | */ 74 | if ($processors === null) { 75 | $this->processors = [ 76 | NullProcessor::class, 77 | BooleanProcessor::class, 78 | NumberProcessor::class, 79 | QuotedProcessor::class 80 | ]; 81 | return; 82 | } 83 | 84 | foreach ($processors as $processor) { 85 | if (is_subclass_of($processor, AbstractProcessor::class)) { 86 | $this->processors[] = $processor; 87 | } 88 | } 89 | } 90 | 91 | 92 | /** 93 | * Process the value with the configured processors 94 | * 95 | * @param string $value The value to process 96 | * @return mixed 97 | */ 98 | private function processValue(string $value) 99 | { 100 | /** 101 | * First trim spaces and quotes if configured 102 | */ 103 | $trimmedValue = trim($value); 104 | 105 | foreach ($this->processors as $processor) { 106 | /** @var AbstractProcessor $processorInstance */ 107 | $processorInstance = new $processor($trimmedValue); 108 | 109 | if ($processorInstance->canBeProcessed()) { 110 | return $processorInstance->execute(); 111 | } 112 | } 113 | 114 | /** 115 | * Does not match any processor options, return as is 116 | */ 117 | return $trimmedValue; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Processor/AbstractProcessor.php: -------------------------------------------------------------------------------- 1 | value = $value; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Processor/BooleanProcessor.php: -------------------------------------------------------------------------------- 1 | value); 9 | 10 | return in_array($loweredValue, ['true', 'false'], true); 11 | } 12 | 13 | /** 14 | * Executes the PHP function and returns a boolean value indicating whether the value is 'true' in lowercase. 15 | * 16 | * @return bool The result of the comparison between the lowercase value of $this->value and 'true'. 17 | */ 18 | public function execute() 19 | { 20 | return strtolower($this->value) === 'true'; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Processor/IProcessor.php: -------------------------------------------------------------------------------- 1 | value, ['null', 'NULL'], true); 9 | } 10 | 11 | public function execute() 12 | { 13 | return null; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Processor/NumberProcessor.php: -------------------------------------------------------------------------------- 1 | value); 9 | } 10 | 11 | /** 12 | * Executes the function and returns an integer or float value based on the input. 13 | * 14 | * This function uses the `filter_var` function with the `FILTER_VALIDATE_INT` filter to check if the input value can be 15 | * converted to an integer. If the conversion is successful, the integer value is returned. Otherwise, the input value is 16 | * cast to a float and returned. 17 | * 18 | * @return int|float The converted integer or float value. 19 | */ 20 | public function execute() 21 | { 22 | $int = filter_var($this->value, FILTER_VALIDATE_INT); 23 | 24 | if ($int !== false) { 25 | return $int; 26 | } 27 | 28 | return (float) $this->value; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Processor/QuotedProcessor.php: -------------------------------------------------------------------------------- 1 | isWrappedByChar($this->value, '"'); 10 | 11 | if ($wrappedByDoubleQuotes) { 12 | return true; 13 | } 14 | 15 | return $this->isWrappedByChar($this->value, '\''); 16 | } 17 | /** 18 | * Executes the function and returns a substring of the value property of the current object, 19 | * excluding the first and last characters. 20 | * 21 | * @return string The substring of the value property. 22 | */ 23 | public function execute() 24 | { 25 | /** 26 | * Since this function is used for the quote removal 27 | * we don't need mb_substr 28 | */ 29 | return substr($this->value, 1, -1); 30 | } 31 | 32 | private function isWrappedByChar(string $value, string $char) : bool 33 | { 34 | return !empty($value) && $value[0] === $char && $value[-1] === $char; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/DotenvTest.php: -------------------------------------------------------------------------------- 1 | clearAllEnv(); 22 | } 23 | 24 | private function clearAllEnv(): void 25 | { 26 | foreach ($_ENV as $key => $value) { 27 | unset($_ENV[$key]); 28 | } 29 | foreach ($_SERVER as $key => $value) { 30 | unset($_SERVER[$key]); 31 | } 32 | } 33 | 34 | protected function execute(): void 35 | { 36 | $this->clearAllEnv(); 37 | $this->testLoad(); 38 | $this->clearAllEnv(); 39 | 40 | $this->testFileNotExist(); 41 | $this->clearAllEnv(); 42 | 43 | $this->testIncompatibleProcessors(); 44 | $this->clearAllEnv(); 45 | 46 | $this->testProcessBoolean(); 47 | $this->clearAllEnv(); 48 | 49 | $this->testDontProcessBoolean(); 50 | $this->clearAllEnv(); 51 | 52 | $this->testProcessQuotes(); 53 | $this->clearAllEnv(); 54 | 55 | $this->testDontProcessQuotes(); 56 | $this->clearAllEnv(); 57 | 58 | $this->testProcessNumbers(); 59 | } 60 | 61 | private function env(string $file) 62 | { 63 | return __DIR__ . DIRECTORY_SEPARATOR . 'env' . DIRECTORY_SEPARATOR . $file; 64 | } 65 | 66 | /** 67 | * @runInSeparateProcess 68 | */ 69 | public function testLoad() { 70 | 71 | (new DotEnv($this->env('.env.default')))->load(); 72 | 73 | $this->assertEquals('dev', getenv('APP_ENV')); 74 | $this->assertEquals('mysql:host=localhost;dbname=test;', getenv('DATABASE_DNS')); 75 | $this->assertEquals('root', getenv('DATABASE_USER')); 76 | $this->assertEquals('password', getenv('DATABASE_PASSWORD')); 77 | $this->assertFalse(getenv('GOOGLE_API')); 78 | $this->assertFalse(getenv('GOOGLE_MANAGER_KEY')); 79 | $this->assertEquals(true, getenv('BOOLEAN_LITERAL')); 80 | $this->assertEquals('true', getenv('BOOLEAN_QUOTED')); 81 | 82 | $this->assertEquals('dev', $_ENV['APP_ENV']); 83 | $this->assertEquals('password', $_ENV['DATABASE_PASSWORD']); 84 | $this->assertFalse(array_key_exists('GOOGLE_API', $_ENV)); 85 | $this->assertFalse(array_key_exists('GOOGLE_MANAGER_KEY', $_ENV)); 86 | 87 | $this->assertEquals(true, $_ENV['BOOLEAN_LITERAL']); 88 | $this->assertEquals('true', $_ENV['BOOLEAN_QUOTED']); 89 | 90 | $this->assertEquals('mysql:host=localhost;dbname=test;', $_SERVER['DATABASE_DNS']); 91 | $this->assertEquals('root', $_SERVER['DATABASE_USER']); 92 | $this->assertEquals('password', $_SERVER['DATABASE_PASSWORD']); 93 | $this->assertFalse(array_key_exists('GOOGLE_API', $_SERVER)); 94 | $this->assertFalse(array_key_exists('GOOGLE_MANAGER_KEY', $_SERVER)); 95 | $this->assertEquals(true, $_SERVER['BOOLEAN_LITERAL']); 96 | $this->assertEquals('true', $_SERVER['BOOLEAN_QUOTED']); 97 | 98 | $this->assertEquals('🪄', $_SERVER['EMOJI']); 99 | 100 | $this->assertTrue(is_int($_SERVER['ZERO_LITERAL'])); 101 | $this->assertEquals(0, $_SERVER['ZERO_LITERAL']); 102 | 103 | $this->assertTrue(is_string($_SERVER['ZERO_QUOTED'])); 104 | $this->assertEquals('0', $_SERVER['ZERO_QUOTED']); 105 | 106 | $this->assertTrue(is_int($_SERVER['NUMBER_LITERAL'])); 107 | $this->assertEquals(1111, $_SERVER['NUMBER_LITERAL']); 108 | 109 | $this->assertTrue(is_string($_SERVER['NUMBER_QUOTED'])); 110 | $this->assertEquals('1111', $_SERVER['NUMBER_QUOTED']); 111 | 112 | $this->assertNull($_SERVER['NULL_LITERAL']); 113 | $this->assertTrue(array_key_exists('NULL_LITERAL', $_SERVER)); 114 | 115 | $this->assertEquals('null', $_SERVER['NULL_QUOTED']); 116 | 117 | $this->assertEquals('', $_SERVER['EMPTY_LITERAL']); 118 | $this->assertEquals('', $_SERVER['EMPTY_QUOTED']); 119 | } 120 | 121 | public function testFileNotExist() { 122 | $this->expectException(\InvalidArgumentException::class, function () { 123 | (new DotEnv($this->env('.env.not-exists')))->load(); 124 | }); 125 | } 126 | 127 | /** 128 | * @runInSeparateProcess 129 | */ 130 | public function testIncompatibleProcessors() { 131 | $this->assertProcessors( 132 | [], 133 | [] 134 | ); 135 | 136 | $this->assertProcessors( 137 | null, 138 | [ 139 | NullProcessor::class, 140 | BooleanProcessor::class, 141 | NumberProcessor::class, 142 | QuotedProcessor::class 143 | ] 144 | ); 145 | 146 | $this->assertProcessors( 147 | [null], 148 | [] 149 | ); 150 | 151 | $this->assertProcessors( 152 | [new \stdClass()], 153 | [] 154 | ); 155 | 156 | $this->assertProcessors( 157 | [QuotedProcessor::class, null], 158 | [QuotedProcessor::class] 159 | ); 160 | } 161 | 162 | /** 163 | * @runInSeparateProcess 164 | */ 165 | private function assertProcessors(array $processorsToUse = null, array $expectedProcessors = []) 166 | { 167 | $dotEnv = new DotEnv($this->env('.env.default'), $processorsToUse); 168 | $dotEnv->load(); 169 | 170 | $this->assertEquals( 171 | $expectedProcessors, 172 | $this->getProtectedProperty($dotEnv) 173 | ); 174 | } 175 | 176 | private function getProtectedProperty(object $object) { 177 | $reflection = new \ReflectionClass($object); 178 | $reflectionProperty = $reflection->getProperty('processors'); 179 | $reflectionProperty->setAccessible(true); 180 | 181 | return $reflectionProperty->getValue($object); 182 | } 183 | 184 | /** 185 | * @runInSeparateProcess 186 | */ 187 | public function testProcessBoolean() 188 | { 189 | (new DotEnv($this->env('.env.boolean'), [ 190 | BooleanProcessor::class 191 | ]))->load(); 192 | 193 | $this->assertEquals(false, $_ENV['FALSE1']); 194 | $this->assertEquals(false, $_ENV['FALSE2']); 195 | $this->assertEquals(false, $_ENV['FALSE3']); 196 | $this->assertEquals("'false'", $_ENV['FALSE4']); // Since we don't have the QuotedProcessor::class this will be the result 197 | $this->assertEquals('0', $_ENV['FALSE5']); 198 | 199 | $this->assertEquals(true, $_ENV['TRUE1']); 200 | $this->assertEquals(true, $_ENV['TRUE2']); 201 | $this->assertEquals(true, $_ENV['TRUE3']); 202 | $this->assertEquals("'true'", $_ENV['TRUE4']); // Since we don't have the QuotedProcessor::class this will be the result 203 | $this->assertEquals('1', $_ENV['TRUE5']); 204 | } 205 | 206 | /** 207 | * @runInSeparateProcess 208 | */ 209 | public function testDontProcessBoolean() 210 | { 211 | (new DotEnv($this->env('.env.boolean'), []))->load(); 212 | 213 | $this->assertEquals('false', $_ENV['FALSE1']); 214 | $this->assertEquals('true', $_ENV['TRUE1']); 215 | } 216 | 217 | /** 218 | * @runInSeparateProcess 219 | */ 220 | public function testProcessQuotes() 221 | { 222 | (new DotEnv($this->env('.env.quotes'), [ 223 | QuotedProcessor::class 224 | ]))->load(); 225 | 226 | $this->assertEquals('q1', $_ENV['QUOTED1']); 227 | $this->assertEquals('q2', $_ENV['QUOTED2']); 228 | $this->assertEquals('"q3"', $_ENV['QUOTED3']); 229 | $this->assertEquals('This is a "sample" value', $_ENV['QUOTED4']); 230 | $this->assertEquals('\"This is a "sample" value\"', $_ENV['QUOTED5']); 231 | $this->assertEquals('"q6', $_ENV['QUOTED6']); 232 | $this->assertEquals('q7"', $_ENV['QUOTED7']); 233 | } 234 | 235 | /** 236 | * @runInSeparateProcess 237 | */ 238 | public function testDontProcessQuotes() 239 | { 240 | (new DotEnv($this->env('.env.quotes'), []))->load(); 241 | 242 | $this->assertEquals('"q1"', $_ENV['QUOTED1']); 243 | $this->assertEquals('\'q2\'', $_ENV['QUOTED2']); 244 | $this->assertEquals('""q3""', $_ENV['QUOTED3']); 245 | $this->assertEquals('"This is a "sample" value"', $_ENV['QUOTED4']); 246 | $this->assertEquals('\"This is a "sample" value\"', $_ENV['QUOTED5']); 247 | $this->assertEquals('"q6', $_ENV['QUOTED6']); 248 | $this->assertEquals('q7"', $_ENV['QUOTED7']); 249 | $this->assertEquals('0', $_ENV['ZERO_LITERAL']); 250 | $this->assertEquals('"0"', $_ENV['ZERO_QUOTED']); 251 | } 252 | 253 | /** 254 | * @runInSeparateProcess 255 | */ 256 | public function testProcessNumbers() 257 | { 258 | (new DotEnv($this->env('.env.number'), [ 259 | NumberProcessor::class 260 | ]))->load(); 261 | 262 | $this->assertEquals(0, $_ENV['NUMBER1']); 263 | $this->assertTrue(is_numeric($_ENV['NUMBER1'])); 264 | $this->assertEquals(0.0001, $_ENV['NUMBER2']); 265 | $this->assertEquals(123456789, $_ENV['NUMBER3']); 266 | $this->assertEquals(123456789.0, $_ENV['NUMBER4']); 267 | } 268 | } 269 | -------------------------------------------------------------------------------- /tests/env/.env.boolean: -------------------------------------------------------------------------------- 1 | FALSE1=false 2 | FALSE2= false 3 | FALSE3=FALSE 4 | FALSE4='false' 5 | FALSE5=0 6 | TRUE1=true 7 | TRUE2= true 8 | TRUE3=TRUE 9 | TRUE4='true' 10 | TRUE5=1 -------------------------------------------------------------------------------- /tests/env/.env.default: -------------------------------------------------------------------------------- 1 | # In all environments, the following files are loaded if they exist, 2 | APP_ENV=dev 3 | DATABASE_DNS=mysql:host=localhost;dbname=test; 4 | DATABASE_USER=root 5 | DATABASE_PASSWORD = password 6 | #GOOGLE_API=DJfa7czhKaJ0Pig6j9XpSjT6NpXZUZwK 7 | # GOOGLE_MANAGER_KEY=P7RkBUQHIkPUEPy3yCTT4gGYa2DjRth8 8 | BOOLEAN_LITERAL=true 9 | BOOLEAN_QUOTED="true" 10 | ZERO_LITERAL=0 11 | ZERO_QUOTED="0" 12 | NUMBER_LITERAL=1111 13 | NUMBER_QUOTED="1111" 14 | NULL_LITERAL=null 15 | NULL_QUOTED="null" 16 | EMPTY_LITERAL= 17 | EMPTY_QUOTED="" 18 | EMOJI=🪄 -------------------------------------------------------------------------------- /tests/env/.env.number: -------------------------------------------------------------------------------- 1 | NUMBER1=0 2 | NUMBER2=0.0001 3 | NUMBER3=123456789 4 | NUMBER4=123456789.0 -------------------------------------------------------------------------------- /tests/env/.env.quotes: -------------------------------------------------------------------------------- 1 | QUOTED1="q1" 2 | QUOTED2='q2' 3 | QUOTED3=""q3"" 4 | QUOTED4="This is a "sample" value" 5 | QUOTED5=\"This is a "sample" value\" 6 | QUOTED6="q6 7 | QUOTED7=q7" 8 | ZERO_LITERAL=0 9 | ZERO_QUOTED="0" 10 | --------------------------------------------------------------------------------