├── LICENSE ├── composer.json └── src ├── Dotenv.php ├── Exception ├── ExceptionInterface.php ├── InvalidEncodingException.php ├── InvalidFileException.php ├── InvalidPathException.php └── ValidationException.php ├── Loader ├── Loader.php ├── LoaderInterface.php └── Resolver.php ├── Parser ├── Entry.php ├── EntryParser.php ├── Lexer.php ├── Lines.php ├── Parser.php ├── ParserInterface.php └── Value.php ├── Repository ├── Adapter │ ├── AdapterInterface.php │ ├── ApacheAdapter.php │ ├── ArrayAdapter.php │ ├── EnvConstAdapter.php │ ├── GuardedWriter.php │ ├── ImmutableWriter.php │ ├── MultiReader.php │ ├── MultiWriter.php │ ├── PutenvAdapter.php │ ├── ReaderInterface.php │ ├── ReplacingWriter.php │ ├── ServerConstAdapter.php │ └── WriterInterface.php ├── AdapterRepository.php ├── RepositoryBuilder.php └── RepositoryInterface.php ├── Store ├── File │ ├── Paths.php │ └── Reader.php ├── FileStore.php ├── StoreBuilder.php ├── StoreInterface.php └── StringStore.php ├── Util ├── Regex.php └── Str.php └── Validator.php /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2014, Graham Campbell. 4 | Copyright (c) 2013, Vance Lucas. 5 | All rights reserved. 6 | 7 | Redistribution and use in source and binary forms, with or without 8 | modification, are permitted provided that the following conditions are met: 9 | 10 | 1. Redistributions of source code must retain the above copyright notice, this 11 | list of conditions and the following disclaimer. 12 | 13 | 2. Redistributions in binary form must reproduce the above copyright notice, 14 | this list of conditions and the following disclaimer in the documentation 15 | and/or other materials provided with the distribution. 16 | 17 | 3. Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from 19 | this software without specific prior written permission. 20 | 21 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 22 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 23 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 24 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 25 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 26 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 27 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 28 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 29 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 30 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 31 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vlucas/phpdotenv", 3 | "description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.", 4 | "keywords": ["env", "dotenv", "environment"], 5 | "license": "BSD-3-Clause", 6 | "authors": [ 7 | { 8 | "name": "Graham Campbell", 9 | "email": "hello@gjcampbell.co.uk", 10 | "homepage": "https://github.com/GrahamCampbell" 11 | }, 12 | { 13 | "name": "Vance Lucas", 14 | "email": "vance@vancelucas.com", 15 | "homepage": "https://github.com/vlucas" 16 | } 17 | ], 18 | "require": { 19 | "php": "^7.2.5 || ^8.0", 20 | "ext-pcre": "*", 21 | "graham-campbell/result-type": "^1.1.3", 22 | "phpoption/phpoption": "^1.9.3", 23 | "symfony/polyfill-ctype": "^1.24", 24 | "symfony/polyfill-mbstring": "^1.24", 25 | "symfony/polyfill-php80": "^1.24" 26 | }, 27 | "require-dev": { 28 | "ext-filter": "*", 29 | "bamarni/composer-bin-plugin": "^1.8.2", 30 | "phpunit/phpunit":"^8.5.34 || ^9.6.13 || ^10.4.2" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "Dotenv\\": "src/" 35 | } 36 | }, 37 | "autoload-dev": { 38 | "psr-4": { 39 | "Dotenv\\Tests\\": "tests/Dotenv/" 40 | } 41 | }, 42 | "suggest": { 43 | "ext-filter": "Required to use the boolean validator." 44 | }, 45 | "config": { 46 | "allow-plugins": { 47 | "bamarni/composer-bin-plugin": true 48 | }, 49 | "preferred-install": "dist" 50 | }, 51 | "extra": { 52 | "bamarni-bin": { 53 | "bin-links": true, 54 | "forward-command": false 55 | }, 56 | "branch-alias": { 57 | "dev-master": "5.6-dev" 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Dotenv.php: -------------------------------------------------------------------------------- 1 | store = $store; 67 | $this->parser = $parser; 68 | $this->loader = $loader; 69 | $this->repository = $repository; 70 | } 71 | 72 | /** 73 | * Create a new dotenv instance. 74 | * 75 | * @param \Dotenv\Repository\RepositoryInterface $repository 76 | * @param string|string[] $paths 77 | * @param string|string[]|null $names 78 | * @param bool $shortCircuit 79 | * @param string|null $fileEncoding 80 | * 81 | * @return \Dotenv\Dotenv 82 | */ 83 | public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) 84 | { 85 | $builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames(); 86 | 87 | foreach ((array) $paths as $path) { 88 | $builder = $builder->addPath($path); 89 | } 90 | 91 | foreach ((array) $names as $name) { 92 | $builder = $builder->addName($name); 93 | } 94 | 95 | if ($shortCircuit) { 96 | $builder = $builder->shortCircuit(); 97 | } 98 | 99 | return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository); 100 | } 101 | 102 | /** 103 | * Create a new mutable dotenv instance with default repository. 104 | * 105 | * @param string|string[] $paths 106 | * @param string|string[]|null $names 107 | * @param bool $shortCircuit 108 | * @param string|null $fileEncoding 109 | * 110 | * @return \Dotenv\Dotenv 111 | */ 112 | public static function createMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) 113 | { 114 | $repository = RepositoryBuilder::createWithDefaultAdapters()->make(); 115 | 116 | return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); 117 | } 118 | 119 | /** 120 | * Create a new mutable dotenv instance with default repository with the putenv adapter. 121 | * 122 | * @param string|string[] $paths 123 | * @param string|string[]|null $names 124 | * @param bool $shortCircuit 125 | * @param string|null $fileEncoding 126 | * 127 | * @return \Dotenv\Dotenv 128 | */ 129 | public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) 130 | { 131 | $repository = RepositoryBuilder::createWithDefaultAdapters() 132 | ->addAdapter(PutenvAdapter::class) 133 | ->make(); 134 | 135 | return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); 136 | } 137 | 138 | /** 139 | * Create a new immutable dotenv instance with default repository. 140 | * 141 | * @param string|string[] $paths 142 | * @param string|string[]|null $names 143 | * @param bool $shortCircuit 144 | * @param string|null $fileEncoding 145 | * 146 | * @return \Dotenv\Dotenv 147 | */ 148 | public static function createImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) 149 | { 150 | $repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make(); 151 | 152 | return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); 153 | } 154 | 155 | /** 156 | * Create a new immutable dotenv instance with default repository with the putenv adapter. 157 | * 158 | * @param string|string[] $paths 159 | * @param string|string[]|null $names 160 | * @param bool $shortCircuit 161 | * @param string|null $fileEncoding 162 | * 163 | * @return \Dotenv\Dotenv 164 | */ 165 | public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) 166 | { 167 | $repository = RepositoryBuilder::createWithDefaultAdapters() 168 | ->addAdapter(PutenvAdapter::class) 169 | ->immutable() 170 | ->make(); 171 | 172 | return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); 173 | } 174 | 175 | /** 176 | * Create a new dotenv instance with an array backed repository. 177 | * 178 | * @param string|string[] $paths 179 | * @param string|string[]|null $names 180 | * @param bool $shortCircuit 181 | * @param string|null $fileEncoding 182 | * 183 | * @return \Dotenv\Dotenv 184 | */ 185 | public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, ?string $fileEncoding = null) 186 | { 187 | $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); 188 | 189 | return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding); 190 | } 191 | 192 | /** 193 | * Parse the given content and resolve nested variables. 194 | * 195 | * This method behaves just like load(), only without mutating your actual 196 | * environment. We do this by using an array backed repository. 197 | * 198 | * @param string $content 199 | * 200 | * @throws \Dotenv\Exception\InvalidFileException 201 | * 202 | * @return array 203 | */ 204 | public static function parse(string $content) 205 | { 206 | $repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make(); 207 | 208 | $phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository); 209 | 210 | return $phpdotenv->load(); 211 | } 212 | 213 | /** 214 | * Read and load environment file(s). 215 | * 216 | * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException 217 | * 218 | * @return array 219 | */ 220 | public function load() 221 | { 222 | $entries = $this->parser->parse($this->store->read()); 223 | 224 | return $this->loader->load($this->repository, $entries); 225 | } 226 | 227 | /** 228 | * Read and load environment file(s), silently failing if no files can be read. 229 | * 230 | * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException 231 | * 232 | * @return array 233 | */ 234 | public function safeLoad() 235 | { 236 | try { 237 | return $this->load(); 238 | } catch (InvalidPathException $e) { 239 | // suppressing exception 240 | return []; 241 | } 242 | } 243 | 244 | /** 245 | * Required ensures that the specified variables exist, and returns a new validator object. 246 | * 247 | * @param string|string[] $variables 248 | * 249 | * @return \Dotenv\Validator 250 | */ 251 | public function required($variables) 252 | { 253 | return (new Validator($this->repository, (array) $variables))->required(); 254 | } 255 | 256 | /** 257 | * Returns a new validator object that won't check if the specified variables exist. 258 | * 259 | * @param string|string[] $variables 260 | * 261 | * @return \Dotenv\Validator 262 | */ 263 | public function ifPresent($variables) 264 | { 265 | return new Validator($this->repository, (array) $variables); 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /src/Exception/ExceptionInterface.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function load(RepositoryInterface $repository, array $entries) 25 | { 26 | /** @var array */ 27 | return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) { 28 | $name = $entry->getName(); 29 | 30 | $value = $entry->getValue()->map(static function (Value $value) use ($repository) { 31 | return Resolver::resolve($repository, $value); 32 | }); 33 | 34 | if ($value->isDefined()) { 35 | $inner = $value->get(); 36 | if ($repository->set($name, $inner)) { 37 | return \array_merge($vars, [$name => $inner]); 38 | } 39 | } else { 40 | if ($repository->clear($name)) { 41 | return \array_merge($vars, [$name => null]); 42 | } 43 | } 44 | 45 | return $vars; 46 | }, []); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Loader/LoaderInterface.php: -------------------------------------------------------------------------------- 1 | 18 | */ 19 | public function load(RepositoryInterface $repository, array $entries); 20 | } 21 | -------------------------------------------------------------------------------- /src/Loader/Resolver.php: -------------------------------------------------------------------------------- 1 | getVars(), static function (string $s, int $i) use ($repository) { 41 | return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i)); 42 | }, $value->getChars()); 43 | } 44 | 45 | /** 46 | * Resolve a single nested variable. 47 | * 48 | * @param \Dotenv\Repository\RepositoryInterface $repository 49 | * @param string $str 50 | * 51 | * @return string 52 | */ 53 | private static function resolveVariable(RepositoryInterface $repository, string $str) 54 | { 55 | return Regex::replaceCallback( 56 | '/\A\${([a-zA-Z0-9_.]+)}/', 57 | static function (array $matches) use ($repository) { 58 | /** @var string */ 59 | return Option::fromValue($repository->get($matches[1]))->getOrElse($matches[0]); 60 | }, 61 | $str, 62 | 1 63 | )->success()->getOrElse($str); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Parser/Entry.php: -------------------------------------------------------------------------------- 1 | name = $name; 36 | $this->value = $value; 37 | } 38 | 39 | /** 40 | * Get the entry name. 41 | * 42 | * @return string 43 | */ 44 | public function getName() 45 | { 46 | return $this->name; 47 | } 48 | 49 | /** 50 | * Get the entry value. 51 | * 52 | * @return \PhpOption\Option<\Dotenv\Parser\Value> 53 | */ 54 | public function getValue() 55 | { 56 | /** @var \PhpOption\Option<\Dotenv\Parser\Value> */ 57 | return Option::fromValue($this->value); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Parser/EntryParser.php: -------------------------------------------------------------------------------- 1 | 45 | */ 46 | public static function parse(string $entry) 47 | { 48 | return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) { 49 | [$name, $value] = $parts; 50 | 51 | return self::parseName($name)->flatMap(static function (string $name) use ($value) { 52 | /** @var Result */ 53 | $parsedValue = $value === null ? Success::create(null) : self::parseValue($value); 54 | 55 | return $parsedValue->map(static function (?Value $value) use ($name) { 56 | return new Entry($name, $value); 57 | }); 58 | }); 59 | }); 60 | } 61 | 62 | /** 63 | * Split the compound string into parts. 64 | * 65 | * @param string $line 66 | * 67 | * @return \GrahamCampbell\ResultType\Result 68 | */ 69 | private static function splitStringIntoParts(string $line) 70 | { 71 | /** @var array{string, string|null} */ 72 | $result = Str::pos($line, '=')->map(static function () use ($line) { 73 | return \array_map('trim', \explode('=', $line, 2)); 74 | })->getOrElse([$line, null]); 75 | 76 | if ($result[0] === '') { 77 | /** @var \GrahamCampbell\ResultType\Result */ 78 | return Error::create(self::getErrorMessage('an unexpected equals', $line)); 79 | } 80 | 81 | /** @var \GrahamCampbell\ResultType\Result */ 82 | return Success::create($result); 83 | } 84 | 85 | /** 86 | * Parse the given variable name. 87 | * 88 | * That is, strip the optional quotes and leading "export" from the 89 | * variable name. We wrap the answer in a result type. 90 | * 91 | * @param string $name 92 | * 93 | * @return \GrahamCampbell\ResultType\Result 94 | */ 95 | private static function parseName(string $name) 96 | { 97 | if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) { 98 | $name = \ltrim(Str::substr($name, 6)); 99 | } 100 | 101 | if (self::isQuotedName($name)) { 102 | $name = Str::substr($name, 1, -1); 103 | } 104 | 105 | if (!self::isValidName($name)) { 106 | /** @var \GrahamCampbell\ResultType\Result */ 107 | return Error::create(self::getErrorMessage('an invalid name', $name)); 108 | } 109 | 110 | /** @var \GrahamCampbell\ResultType\Result */ 111 | return Success::create($name); 112 | } 113 | 114 | /** 115 | * Is the given variable name quoted? 116 | * 117 | * @param string $name 118 | * 119 | * @return bool 120 | */ 121 | private static function isQuotedName(string $name) 122 | { 123 | if (Str::len($name) < 3) { 124 | return false; 125 | } 126 | 127 | $first = Str::substr($name, 0, 1); 128 | $last = Str::substr($name, -1, 1); 129 | 130 | return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\''); 131 | } 132 | 133 | /** 134 | * Is the given variable name valid? 135 | * 136 | * @param string $name 137 | * 138 | * @return bool 139 | */ 140 | private static function isValidName(string $name) 141 | { 142 | return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false); 143 | } 144 | 145 | /** 146 | * Parse the given variable value. 147 | * 148 | * This has the effect of stripping quotes and comments, dealing with 149 | * special characters, and locating nested variables, but not resolving 150 | * them. Formally, we run a finite state automaton with an output tape: a 151 | * transducer. We wrap the answer in a result type. 152 | * 153 | * @param string $value 154 | * 155 | * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> 156 | */ 157 | private static function parseValue(string $value) 158 | { 159 | if (\trim($value) === '') { 160 | /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ 161 | return Success::create(Value::blank()); 162 | } 163 | 164 | return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) { 165 | return $data->flatMap(static function (array $data) use ($token) { 166 | return self::processToken($data[1], $token)->map(static function (array $val) use ($data) { 167 | return [$data[0]->append($val[0], $val[1]), $val[2]]; 168 | }); 169 | }); 170 | }, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) { 171 | /** @psalm-suppress DocblockTypeContradiction */ 172 | if (in_array($result[1], self::REJECT_STATES, true)) { 173 | /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ 174 | return Error::create('a missing closing quote'); 175 | } 176 | 177 | /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value, string> */ 178 | return Success::create($result[0]); 179 | })->mapError(static function (string $err) use ($value) { 180 | return self::getErrorMessage($err, $value); 181 | }); 182 | } 183 | 184 | /** 185 | * Process the given token. 186 | * 187 | * @param int $state 188 | * @param string $token 189 | * 190 | * @return \GrahamCampbell\ResultType\Result 191 | */ 192 | private static function processToken(int $state, string $token) 193 | { 194 | switch ($state) { 195 | case self::INITIAL_STATE: 196 | if ($token === '\'') { 197 | /** @var \GrahamCampbell\ResultType\Result */ 198 | return Success::create(['', false, self::SINGLE_QUOTED_STATE]); 199 | } elseif ($token === '"') { 200 | /** @var \GrahamCampbell\ResultType\Result */ 201 | return Success::create(['', false, self::DOUBLE_QUOTED_STATE]); 202 | } elseif ($token === '#') { 203 | /** @var \GrahamCampbell\ResultType\Result */ 204 | return Success::create(['', false, self::COMMENT_STATE]); 205 | } elseif ($token === '$') { 206 | /** @var \GrahamCampbell\ResultType\Result */ 207 | return Success::create([$token, true, self::UNQUOTED_STATE]); 208 | } else { 209 | /** @var \GrahamCampbell\ResultType\Result */ 210 | return Success::create([$token, false, self::UNQUOTED_STATE]); 211 | } 212 | case self::UNQUOTED_STATE: 213 | if ($token === '#') { 214 | /** @var \GrahamCampbell\ResultType\Result */ 215 | return Success::create(['', false, self::COMMENT_STATE]); 216 | } elseif (\ctype_space($token)) { 217 | /** @var \GrahamCampbell\ResultType\Result */ 218 | return Success::create(['', false, self::WHITESPACE_STATE]); 219 | } elseif ($token === '$') { 220 | /** @var \GrahamCampbell\ResultType\Result */ 221 | return Success::create([$token, true, self::UNQUOTED_STATE]); 222 | } else { 223 | /** @var \GrahamCampbell\ResultType\Result */ 224 | return Success::create([$token, false, self::UNQUOTED_STATE]); 225 | } 226 | case self::SINGLE_QUOTED_STATE: 227 | if ($token === '\'') { 228 | /** @var \GrahamCampbell\ResultType\Result */ 229 | return Success::create(['', false, self::WHITESPACE_STATE]); 230 | } else { 231 | /** @var \GrahamCampbell\ResultType\Result */ 232 | return Success::create([$token, false, self::SINGLE_QUOTED_STATE]); 233 | } 234 | case self::DOUBLE_QUOTED_STATE: 235 | if ($token === '"') { 236 | /** @var \GrahamCampbell\ResultType\Result */ 237 | return Success::create(['', false, self::WHITESPACE_STATE]); 238 | } elseif ($token === '\\') { 239 | /** @var \GrahamCampbell\ResultType\Result */ 240 | return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]); 241 | } elseif ($token === '$') { 242 | /** @var \GrahamCampbell\ResultType\Result */ 243 | return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]); 244 | } else { 245 | /** @var \GrahamCampbell\ResultType\Result */ 246 | return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); 247 | } 248 | case self::ESCAPE_SEQUENCE_STATE: 249 | if ($token === '"' || $token === '\\') { 250 | /** @var \GrahamCampbell\ResultType\Result */ 251 | return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); 252 | } elseif ($token === '$') { 253 | /** @var \GrahamCampbell\ResultType\Result */ 254 | return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]); 255 | } else { 256 | $first = Str::substr($token, 0, 1); 257 | if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) { 258 | /** @var \GrahamCampbell\ResultType\Result */ 259 | return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]); 260 | } else { 261 | /** @var \GrahamCampbell\ResultType\Result */ 262 | return Error::create('an unexpected escape sequence'); 263 | } 264 | } 265 | case self::WHITESPACE_STATE: 266 | if ($token === '#') { 267 | /** @var \GrahamCampbell\ResultType\Result */ 268 | return Success::create(['', false, self::COMMENT_STATE]); 269 | } elseif (!\ctype_space($token)) { 270 | /** @var \GrahamCampbell\ResultType\Result */ 271 | return Error::create('unexpected whitespace'); 272 | } else { 273 | /** @var \GrahamCampbell\ResultType\Result */ 274 | return Success::create(['', false, self::WHITESPACE_STATE]); 275 | } 276 | case self::COMMENT_STATE: 277 | /** @var \GrahamCampbell\ResultType\Result */ 278 | return Success::create(['', false, self::COMMENT_STATE]); 279 | default: 280 | throw new \Error('Parser entered invalid state.'); 281 | } 282 | } 283 | 284 | /** 285 | * Generate a friendly error message. 286 | * 287 | * @param string $cause 288 | * @param string $subject 289 | * 290 | * @return string 291 | */ 292 | private static function getErrorMessage(string $cause, string $subject) 293 | { 294 | return \sprintf( 295 | 'Encountered %s at [%s].', 296 | $cause, 297 | \strtok($subject, "\n") 298 | ); 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/Parser/Lexer.php: -------------------------------------------------------------------------------- 1 | 37 | */ 38 | public static function lex(string $content) 39 | { 40 | static $regex; 41 | 42 | if ($regex === null) { 43 | $regex = '(('.\implode(')|(', self::PATTERNS).'))A'; 44 | } 45 | 46 | $offset = 0; 47 | 48 | while (isset($content[$offset])) { 49 | if (!\preg_match($regex, $content, $matches, 0, $offset)) { 50 | throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset])); 51 | } 52 | 53 | $offset += \strlen($matches[0]); 54 | 55 | yield $matches[0]; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Parser/Lines.php: -------------------------------------------------------------------------------- 1 | map(static function () use ($line) { 91 | return self::looksLikeMultilineStop($line, true) === false; 92 | })->getOrElse(false); 93 | } 94 | 95 | /** 96 | * Determine if the given line can be the start of a multiline variable. 97 | * 98 | * @param string $line 99 | * @param bool $started 100 | * 101 | * @return bool 102 | */ 103 | private static function looksLikeMultilineStop(string $line, bool $started) 104 | { 105 | if ($line === '"') { 106 | return true; 107 | } 108 | 109 | return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) { 110 | return $started ? $count > 1 : $count >= 1; 111 | })->success()->getOrElse(false); 112 | } 113 | 114 | /** 115 | * Determine if the line in the file is a comment or whitespace. 116 | * 117 | * @param string $line 118 | * 119 | * @return bool 120 | */ 121 | private static function isCommentOrWhitespace(string $line) 122 | { 123 | $line = \trim($line); 124 | 125 | return $line === '' || (isset($line[0]) && $line[0] === '#'); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/Parser/Parser.php: -------------------------------------------------------------------------------- 1 | mapError(static function () { 26 | return 'Could not split into separate lines.'; 27 | })->flatMap(static function (array $lines) { 28 | return self::process(Lines::process($lines)); 29 | })->mapError(static function (string $error) { 30 | throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error)); 31 | })->success()->get(); 32 | } 33 | 34 | /** 35 | * Convert the raw entries into proper entries. 36 | * 37 | * @param string[] $entries 38 | * 39 | * @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> 40 | */ 41 | private static function process(array $entries) 42 | { 43 | /** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[], string> */ 44 | return \array_reduce($entries, static function (Result $result, string $raw) { 45 | return $result->flatMap(static function (array $entries) use ($raw) { 46 | return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) { 47 | /** @var \Dotenv\Parser\Entry[] */ 48 | return \array_merge($entries, [$entry]); 49 | }); 50 | }); 51 | }, Success::create([])); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Parser/ParserInterface.php: -------------------------------------------------------------------------------- 1 | chars = $chars; 36 | $this->vars = $vars; 37 | } 38 | 39 | /** 40 | * Create an empty value instance. 41 | * 42 | * @return \Dotenv\Parser\Value 43 | */ 44 | public static function blank() 45 | { 46 | return new self('', []); 47 | } 48 | 49 | /** 50 | * Create a new value instance, appending the characters. 51 | * 52 | * @param string $chars 53 | * @param bool $var 54 | * 55 | * @return \Dotenv\Parser\Value 56 | */ 57 | public function append(string $chars, bool $var) 58 | { 59 | return new self( 60 | $this->chars.$chars, 61 | $var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars 62 | ); 63 | } 64 | 65 | /** 66 | * Get the string representation of the parsed value. 67 | * 68 | * @return string 69 | */ 70 | public function getChars() 71 | { 72 | return $this->chars; 73 | } 74 | 75 | /** 76 | * Get the locations of the variables in the value. 77 | * 78 | * @return int[] 79 | */ 80 | public function getVars() 81 | { 82 | $vars = $this->vars; 83 | 84 | \rsort($vars); 85 | 86 | return $vars; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Repository/Adapter/AdapterInterface.php: -------------------------------------------------------------------------------- 1 | 13 | */ 14 | public static function create(); 15 | } 16 | -------------------------------------------------------------------------------- /src/Repository/Adapter/ApacheAdapter.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public static function create() 29 | { 30 | if (self::isSupported()) { 31 | /** @var \PhpOption\Option */ 32 | return Some::create(new self()); 33 | } 34 | 35 | return None::create(); 36 | } 37 | 38 | /** 39 | * Determines if the adapter is supported. 40 | * 41 | * This happens if PHP is running as an Apache module. 42 | * 43 | * @return bool 44 | */ 45 | private static function isSupported() 46 | { 47 | return \function_exists('apache_getenv') && \function_exists('apache_setenv'); 48 | } 49 | 50 | /** 51 | * Read an environment variable, if it exists. 52 | * 53 | * @param non-empty-string $name 54 | * 55 | * @return \PhpOption\Option 56 | */ 57 | public function read(string $name) 58 | { 59 | /** @var \PhpOption\Option */ 60 | return Option::fromValue(apache_getenv($name))->filter(static function ($value) { 61 | return \is_string($value) && $value !== ''; 62 | }); 63 | } 64 | 65 | /** 66 | * Write to an environment variable, if possible. 67 | * 68 | * @param non-empty-string $name 69 | * @param string $value 70 | * 71 | * @return bool 72 | */ 73 | public function write(string $name, string $value) 74 | { 75 | return apache_setenv($name, $value); 76 | } 77 | 78 | /** 79 | * Delete an environment variable, if possible. 80 | * 81 | * @param non-empty-string $name 82 | * 83 | * @return bool 84 | */ 85 | public function delete(string $name) 86 | { 87 | return apache_setenv($name, ''); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Repository/Adapter/ArrayAdapter.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | private $variables; 18 | 19 | /** 20 | * Create a new array adapter instance. 21 | * 22 | * @return void 23 | */ 24 | private function __construct() 25 | { 26 | $this->variables = []; 27 | } 28 | 29 | /** 30 | * Create a new instance of the adapter, if it is available. 31 | * 32 | * @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface> 33 | */ 34 | public static function create() 35 | { 36 | /** @var \PhpOption\Option */ 37 | return Some::create(new self()); 38 | } 39 | 40 | /** 41 | * Read an environment variable, if it exists. 42 | * 43 | * @param non-empty-string $name 44 | * 45 | * @return \PhpOption\Option 46 | */ 47 | public function read(string $name) 48 | { 49 | return Option::fromArraysValue($this->variables, $name); 50 | } 51 | 52 | /** 53 | * Write to an environment variable, if possible. 54 | * 55 | * @param non-empty-string $name 56 | * @param string $value 57 | * 58 | * @return bool 59 | */ 60 | public function write(string $name, string $value) 61 | { 62 | $this->variables[$name] = $value; 63 | 64 | return true; 65 | } 66 | 67 | /** 68 | * Delete an environment variable, if possible. 69 | * 70 | * @param non-empty-string $name 71 | * 72 | * @return bool 73 | */ 74 | public function delete(string $name) 75 | { 76 | unset($this->variables[$name]); 77 | 78 | return true; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Repository/Adapter/EnvConstAdapter.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public static function create() 28 | { 29 | /** @var \PhpOption\Option */ 30 | return Some::create(new self()); 31 | } 32 | 33 | /** 34 | * Read an environment variable, if it exists. 35 | * 36 | * @param non-empty-string $name 37 | * 38 | * @return \PhpOption\Option 39 | */ 40 | public function read(string $name) 41 | { 42 | /** @var \PhpOption\Option */ 43 | return Option::fromArraysValue($_ENV, $name) 44 | ->filter(static function ($value) { 45 | return \is_scalar($value); 46 | }) 47 | ->map(static function ($value) { 48 | if ($value === false) { 49 | return 'false'; 50 | } 51 | 52 | if ($value === true) { 53 | return 'true'; 54 | } 55 | 56 | /** @psalm-suppress PossiblyInvalidCast */ 57 | return (string) $value; 58 | }); 59 | } 60 | 61 | /** 62 | * Write to an environment variable, if possible. 63 | * 64 | * @param non-empty-string $name 65 | * @param string $value 66 | * 67 | * @return bool 68 | */ 69 | public function write(string $name, string $value) 70 | { 71 | $_ENV[$name] = $value; 72 | 73 | return true; 74 | } 75 | 76 | /** 77 | * Delete an environment variable, if possible. 78 | * 79 | * @param non-empty-string $name 80 | * 81 | * @return bool 82 | */ 83 | public function delete(string $name) 84 | { 85 | unset($_ENV[$name]); 86 | 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Repository/Adapter/GuardedWriter.php: -------------------------------------------------------------------------------- 1 | writer = $writer; 34 | $this->allowList = $allowList; 35 | } 36 | 37 | /** 38 | * Write to an environment variable, if possible. 39 | * 40 | * @param non-empty-string $name 41 | * @param string $value 42 | * 43 | * @return bool 44 | */ 45 | public function write(string $name, string $value) 46 | { 47 | // Don't set non-allowed variables 48 | if (!$this->isAllowed($name)) { 49 | return false; 50 | } 51 | 52 | // Set the value on the inner writer 53 | return $this->writer->write($name, $value); 54 | } 55 | 56 | /** 57 | * Delete an environment variable, if possible. 58 | * 59 | * @param non-empty-string $name 60 | * 61 | * @return bool 62 | */ 63 | public function delete(string $name) 64 | { 65 | // Don't clear non-allowed variables 66 | if (!$this->isAllowed($name)) { 67 | return false; 68 | } 69 | 70 | // Set the value on the inner writer 71 | return $this->writer->delete($name); 72 | } 73 | 74 | /** 75 | * Determine if the given variable is allowed. 76 | * 77 | * @param non-empty-string $name 78 | * 79 | * @return bool 80 | */ 81 | private function isAllowed(string $name) 82 | { 83 | return \in_array($name, $this->allowList, true); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/Repository/Adapter/ImmutableWriter.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | private $loaded; 29 | 30 | /** 31 | * Create a new immutable writer instance. 32 | * 33 | * @param \Dotenv\Repository\Adapter\WriterInterface $writer 34 | * @param \Dotenv\Repository\Adapter\ReaderInterface $reader 35 | * 36 | * @return void 37 | */ 38 | public function __construct(WriterInterface $writer, ReaderInterface $reader) 39 | { 40 | $this->writer = $writer; 41 | $this->reader = $reader; 42 | $this->loaded = []; 43 | } 44 | 45 | /** 46 | * Write to an environment variable, if possible. 47 | * 48 | * @param non-empty-string $name 49 | * @param string $value 50 | * 51 | * @return bool 52 | */ 53 | public function write(string $name, string $value) 54 | { 55 | // Don't overwrite existing environment variables 56 | // Ruby's dotenv does this with `ENV[key] ||= value` 57 | if ($this->isExternallyDefined($name)) { 58 | return false; 59 | } 60 | 61 | // Set the value on the inner writer 62 | if (!$this->writer->write($name, $value)) { 63 | return false; 64 | } 65 | 66 | // Record that we have loaded the variable 67 | $this->loaded[$name] = ''; 68 | 69 | return true; 70 | } 71 | 72 | /** 73 | * Delete an environment variable, if possible. 74 | * 75 | * @param non-empty-string $name 76 | * 77 | * @return bool 78 | */ 79 | public function delete(string $name) 80 | { 81 | // Don't clear existing environment variables 82 | if ($this->isExternallyDefined($name)) { 83 | return false; 84 | } 85 | 86 | // Clear the value on the inner writer 87 | if (!$this->writer->delete($name)) { 88 | return false; 89 | } 90 | 91 | // Leave the variable as fair game 92 | unset($this->loaded[$name]); 93 | 94 | return true; 95 | } 96 | 97 | /** 98 | * Determine if the given variable is externally defined. 99 | * 100 | * That is, is it an "existing" variable. 101 | * 102 | * @param non-empty-string $name 103 | * 104 | * @return bool 105 | */ 106 | private function isExternallyDefined(string $name) 107 | { 108 | return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/Repository/Adapter/MultiReader.php: -------------------------------------------------------------------------------- 1 | readers = $readers; 28 | } 29 | 30 | /** 31 | * Read an environment variable, if it exists. 32 | * 33 | * @param non-empty-string $name 34 | * 35 | * @return \PhpOption\Option 36 | */ 37 | public function read(string $name) 38 | { 39 | foreach ($this->readers as $reader) { 40 | $result = $reader->read($name); 41 | if ($result->isDefined()) { 42 | return $result; 43 | } 44 | } 45 | 46 | return None::create(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Repository/Adapter/MultiWriter.php: -------------------------------------------------------------------------------- 1 | writers = $writers; 26 | } 27 | 28 | /** 29 | * Write to an environment variable, if possible. 30 | * 31 | * @param non-empty-string $name 32 | * @param string $value 33 | * 34 | * @return bool 35 | */ 36 | public function write(string $name, string $value) 37 | { 38 | foreach ($this->writers as $writers) { 39 | if (!$writers->write($name, $value)) { 40 | return false; 41 | } 42 | } 43 | 44 | return true; 45 | } 46 | 47 | /** 48 | * Delete an environment variable, if possible. 49 | * 50 | * @param non-empty-string $name 51 | * 52 | * @return bool 53 | */ 54 | public function delete(string $name) 55 | { 56 | foreach ($this->writers as $writers) { 57 | if (!$writers->delete($name)) { 58 | return false; 59 | } 60 | } 61 | 62 | return true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Repository/Adapter/PutenvAdapter.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | public static function create() 29 | { 30 | if (self::isSupported()) { 31 | /** @var \PhpOption\Option */ 32 | return Some::create(new self()); 33 | } 34 | 35 | return None::create(); 36 | } 37 | 38 | /** 39 | * Determines if the adapter is supported. 40 | * 41 | * @return bool 42 | */ 43 | private static function isSupported() 44 | { 45 | return \function_exists('getenv') && \function_exists('putenv'); 46 | } 47 | 48 | /** 49 | * Read an environment variable, if it exists. 50 | * 51 | * @param non-empty-string $name 52 | * 53 | * @return \PhpOption\Option 54 | */ 55 | public function read(string $name) 56 | { 57 | /** @var \PhpOption\Option */ 58 | return Option::fromValue(\getenv($name), false)->filter(static function ($value) { 59 | return \is_string($value); 60 | }); 61 | } 62 | 63 | /** 64 | * Write to an environment variable, if possible. 65 | * 66 | * @param non-empty-string $name 67 | * @param string $value 68 | * 69 | * @return bool 70 | */ 71 | public function write(string $name, string $value) 72 | { 73 | \putenv("$name=$value"); 74 | 75 | return true; 76 | } 77 | 78 | /** 79 | * Delete an environment variable, if possible. 80 | * 81 | * @param non-empty-string $name 82 | * 83 | * @return bool 84 | */ 85 | public function delete(string $name) 86 | { 87 | \putenv($name); 88 | 89 | return true; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Repository/Adapter/ReaderInterface.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | public function read(string $name); 17 | } 18 | -------------------------------------------------------------------------------- /src/Repository/Adapter/ReplacingWriter.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | private $seen; 29 | 30 | /** 31 | * Create a new replacement writer instance. 32 | * 33 | * @param \Dotenv\Repository\Adapter\WriterInterface $writer 34 | * @param \Dotenv\Repository\Adapter\ReaderInterface $reader 35 | * 36 | * @return void 37 | */ 38 | public function __construct(WriterInterface $writer, ReaderInterface $reader) 39 | { 40 | $this->writer = $writer; 41 | $this->reader = $reader; 42 | $this->seen = []; 43 | } 44 | 45 | /** 46 | * Write to an environment variable, if possible. 47 | * 48 | * @param non-empty-string $name 49 | * @param string $value 50 | * 51 | * @return bool 52 | */ 53 | public function write(string $name, string $value) 54 | { 55 | if ($this->exists($name)) { 56 | return $this->writer->write($name, $value); 57 | } 58 | 59 | // succeed if nothing to do 60 | return true; 61 | } 62 | 63 | /** 64 | * Delete an environment variable, if possible. 65 | * 66 | * @param non-empty-string $name 67 | * 68 | * @return bool 69 | */ 70 | public function delete(string $name) 71 | { 72 | if ($this->exists($name)) { 73 | return $this->writer->delete($name); 74 | } 75 | 76 | // succeed if nothing to do 77 | return true; 78 | } 79 | 80 | /** 81 | * Does the given environment variable exist. 82 | * 83 | * Returns true if it currently exists, or existed at any point in the past 84 | * that we are aware of. 85 | * 86 | * @param non-empty-string $name 87 | * 88 | * @return bool 89 | */ 90 | private function exists(string $name) 91 | { 92 | if (isset($this->seen[$name])) { 93 | return true; 94 | } 95 | 96 | if ($this->reader->read($name)->isDefined()) { 97 | $this->seen[$name] = ''; 98 | 99 | return true; 100 | } 101 | 102 | return false; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/Repository/Adapter/ServerConstAdapter.php: -------------------------------------------------------------------------------- 1 | 26 | */ 27 | public static function create() 28 | { 29 | /** @var \PhpOption\Option */ 30 | return Some::create(new self()); 31 | } 32 | 33 | /** 34 | * Read an environment variable, if it exists. 35 | * 36 | * @param non-empty-string $name 37 | * 38 | * @return \PhpOption\Option 39 | */ 40 | public function read(string $name) 41 | { 42 | /** @var \PhpOption\Option */ 43 | return Option::fromArraysValue($_SERVER, $name) 44 | ->filter(static function ($value) { 45 | return \is_scalar($value); 46 | }) 47 | ->map(static function ($value) { 48 | if ($value === false) { 49 | return 'false'; 50 | } 51 | 52 | if ($value === true) { 53 | return 'true'; 54 | } 55 | 56 | /** @psalm-suppress PossiblyInvalidCast */ 57 | return (string) $value; 58 | }); 59 | } 60 | 61 | /** 62 | * Write to an environment variable, if possible. 63 | * 64 | * @param non-empty-string $name 65 | * @param string $value 66 | * 67 | * @return bool 68 | */ 69 | public function write(string $name, string $value) 70 | { 71 | $_SERVER[$name] = $value; 72 | 73 | return true; 74 | } 75 | 76 | /** 77 | * Delete an environment variable, if possible. 78 | * 79 | * @param non-empty-string $name 80 | * 81 | * @return bool 82 | */ 83 | public function delete(string $name) 84 | { 85 | unset($_SERVER[$name]); 86 | 87 | return true; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Repository/Adapter/WriterInterface.php: -------------------------------------------------------------------------------- 1 | reader = $reader; 38 | $this->writer = $writer; 39 | } 40 | 41 | /** 42 | * Determine if the given environment variable is defined. 43 | * 44 | * @param string $name 45 | * 46 | * @return bool 47 | */ 48 | public function has(string $name) 49 | { 50 | return '' !== $name && $this->reader->read($name)->isDefined(); 51 | } 52 | 53 | /** 54 | * Get an environment variable. 55 | * 56 | * @param string $name 57 | * 58 | * @throws \InvalidArgumentException 59 | * 60 | * @return string|null 61 | */ 62 | public function get(string $name) 63 | { 64 | if ('' === $name) { 65 | throw new InvalidArgumentException('Expected name to be a non-empty string.'); 66 | } 67 | 68 | return $this->reader->read($name)->getOrElse(null); 69 | } 70 | 71 | /** 72 | * Set an environment variable. 73 | * 74 | * @param string $name 75 | * @param string $value 76 | * 77 | * @throws \InvalidArgumentException 78 | * 79 | * @return bool 80 | */ 81 | public function set(string $name, string $value) 82 | { 83 | if ('' === $name) { 84 | throw new InvalidArgumentException('Expected name to be a non-empty string.'); 85 | } 86 | 87 | return $this->writer->write($name, $value); 88 | } 89 | 90 | /** 91 | * Clear an environment variable. 92 | * 93 | * @param string $name 94 | * 95 | * @throws \InvalidArgumentException 96 | * 97 | * @return bool 98 | */ 99 | public function clear(string $name) 100 | { 101 | if ('' === $name) { 102 | throw new InvalidArgumentException('Expected name to be a non-empty string.'); 103 | } 104 | 105 | return $this->writer->delete($name); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Repository/RepositoryBuilder.php: -------------------------------------------------------------------------------- 1 | readers = $readers; 71 | $this->writers = $writers; 72 | $this->immutable = $immutable; 73 | $this->allowList = $allowList; 74 | } 75 | 76 | /** 77 | * Create a new repository builder instance with no adapters added. 78 | * 79 | * @return \Dotenv\Repository\RepositoryBuilder 80 | */ 81 | public static function createWithNoAdapters() 82 | { 83 | return new self(); 84 | } 85 | 86 | /** 87 | * Create a new repository builder instance with the default adapters added. 88 | * 89 | * @return \Dotenv\Repository\RepositoryBuilder 90 | */ 91 | public static function createWithDefaultAdapters() 92 | { 93 | $adapters = \iterator_to_array(self::defaultAdapters()); 94 | 95 | return new self($adapters, $adapters); 96 | } 97 | 98 | /** 99 | * Return the array of default adapters. 100 | * 101 | * @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface> 102 | */ 103 | private static function defaultAdapters() 104 | { 105 | foreach (self::DEFAULT_ADAPTERS as $adapter) { 106 | $instance = $adapter::create(); 107 | if ($instance->isDefined()) { 108 | yield $instance->get(); 109 | } 110 | } 111 | } 112 | 113 | /** 114 | * Determine if the given name if of an adapterclass. 115 | * 116 | * @param string $name 117 | * 118 | * @return bool 119 | */ 120 | private static function isAnAdapterClass(string $name) 121 | { 122 | if (!\class_exists($name)) { 123 | return false; 124 | } 125 | 126 | return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class); 127 | } 128 | 129 | /** 130 | * Creates a repository builder with the given reader added. 131 | * 132 | * Accepts either a reader instance, or a class-string for an adapter. If 133 | * the adapter is not supported, then we silently skip adding it. 134 | * 135 | * @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader 136 | * 137 | * @throws \InvalidArgumentException 138 | * 139 | * @return \Dotenv\Repository\RepositoryBuilder 140 | */ 141 | public function addReader($reader) 142 | { 143 | if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) { 144 | throw new InvalidArgumentException( 145 | \sprintf( 146 | 'Expected either an instance of %s or a class-string implementing %s', 147 | ReaderInterface::class, 148 | AdapterInterface::class 149 | ) 150 | ); 151 | } 152 | 153 | $optional = Some::create($reader)->flatMap(static function ($reader) { 154 | return \is_string($reader) ? $reader::create() : Some::create($reader); 155 | }); 156 | 157 | $readers = \array_merge($this->readers, \iterator_to_array($optional)); 158 | 159 | return new self($readers, $this->writers, $this->immutable, $this->allowList); 160 | } 161 | 162 | /** 163 | * Creates a repository builder with the given writer added. 164 | * 165 | * Accepts either a writer instance, or a class-string for an adapter. If 166 | * the adapter is not supported, then we silently skip adding it. 167 | * 168 | * @param \Dotenv\Repository\Adapter\WriterInterface|string $writer 169 | * 170 | * @throws \InvalidArgumentException 171 | * 172 | * @return \Dotenv\Repository\RepositoryBuilder 173 | */ 174 | public function addWriter($writer) 175 | { 176 | if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) { 177 | throw new InvalidArgumentException( 178 | \sprintf( 179 | 'Expected either an instance of %s or a class-string implementing %s', 180 | WriterInterface::class, 181 | AdapterInterface::class 182 | ) 183 | ); 184 | } 185 | 186 | $optional = Some::create($writer)->flatMap(static function ($writer) { 187 | return \is_string($writer) ? $writer::create() : Some::create($writer); 188 | }); 189 | 190 | $writers = \array_merge($this->writers, \iterator_to_array($optional)); 191 | 192 | return new self($this->readers, $writers, $this->immutable, $this->allowList); 193 | } 194 | 195 | /** 196 | * Creates a repository builder with the given adapter added. 197 | * 198 | * Accepts either an adapter instance, or a class-string for an adapter. If 199 | * the adapter is not supported, then we silently skip adding it. We will 200 | * add the adapter as both a reader and a writer. 201 | * 202 | * @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter 203 | * 204 | * @throws \InvalidArgumentException 205 | * 206 | * @return \Dotenv\Repository\RepositoryBuilder 207 | */ 208 | public function addAdapter($adapter) 209 | { 210 | if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) { 211 | throw new InvalidArgumentException( 212 | \sprintf( 213 | 'Expected either an instance of %s or a class-string implementing %s', 214 | WriterInterface::class, 215 | AdapterInterface::class 216 | ) 217 | ); 218 | } 219 | 220 | $optional = Some::create($adapter)->flatMap(static function ($adapter) { 221 | return \is_string($adapter) ? $adapter::create() : Some::create($adapter); 222 | }); 223 | 224 | $readers = \array_merge($this->readers, \iterator_to_array($optional)); 225 | $writers = \array_merge($this->writers, \iterator_to_array($optional)); 226 | 227 | return new self($readers, $writers, $this->immutable, $this->allowList); 228 | } 229 | 230 | /** 231 | * Creates a repository builder with mutability enabled. 232 | * 233 | * @return \Dotenv\Repository\RepositoryBuilder 234 | */ 235 | public function immutable() 236 | { 237 | return new self($this->readers, $this->writers, true, $this->allowList); 238 | } 239 | 240 | /** 241 | * Creates a repository builder with the given allow list. 242 | * 243 | * @param string[]|null $allowList 244 | * 245 | * @return \Dotenv\Repository\RepositoryBuilder 246 | */ 247 | public function allowList(?array $allowList = null) 248 | { 249 | return new self($this->readers, $this->writers, $this->immutable, $allowList); 250 | } 251 | 252 | /** 253 | * Creates a new repository instance. 254 | * 255 | * @return \Dotenv\Repository\RepositoryInterface 256 | */ 257 | public function make() 258 | { 259 | $reader = new MultiReader($this->readers); 260 | $writer = new MultiWriter($this->writers); 261 | 262 | if ($this->immutable) { 263 | $writer = new ImmutableWriter($writer, $reader); 264 | } 265 | 266 | if ($this->allowList !== null) { 267 | $writer = new GuardedWriter($writer, $this->allowList); 268 | } 269 | 270 | return new AdapterRepository($reader, $writer); 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /src/Repository/RepositoryInterface.php: -------------------------------------------------------------------------------- 1 | 42 | */ 43 | public static function read(array $filePaths, bool $shortCircuit = true, ?string $fileEncoding = null) 44 | { 45 | $output = []; 46 | 47 | foreach ($filePaths as $filePath) { 48 | $content = self::readFromFile($filePath, $fileEncoding); 49 | if ($content->isDefined()) { 50 | $output[$filePath] = $content->get(); 51 | if ($shortCircuit) { 52 | break; 53 | } 54 | } 55 | } 56 | 57 | return $output; 58 | } 59 | 60 | /** 61 | * Read the given file. 62 | * 63 | * @param string $path 64 | * @param string|null $encoding 65 | * 66 | * @throws \Dotenv\Exception\InvalidEncodingException 67 | * 68 | * @return \PhpOption\Option 69 | */ 70 | private static function readFromFile(string $path, ?string $encoding = null) 71 | { 72 | /** @var Option */ 73 | $content = Option::fromValue(@\file_get_contents($path), false); 74 | 75 | return $content->flatMap(static function (string $content) use ($encoding) { 76 | return Str::utf8($content, $encoding)->mapError(static function (string $error) { 77 | throw new InvalidEncodingException($error); 78 | })->success(); 79 | }); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Store/FileStore.php: -------------------------------------------------------------------------------- 1 | filePaths = $filePaths; 45 | $this->shortCircuit = $shortCircuit; 46 | $this->fileEncoding = $fileEncoding; 47 | } 48 | 49 | /** 50 | * Read the content of the environment file(s). 51 | * 52 | * @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException 53 | * 54 | * @return string 55 | */ 56 | public function read() 57 | { 58 | if ($this->filePaths === []) { 59 | throw new InvalidPathException('At least one environment file path must be provided.'); 60 | } 61 | 62 | $contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding); 63 | 64 | if (\count($contents) > 0) { 65 | return \implode("\n", $contents); 66 | } 67 | 68 | throw new InvalidPathException( 69 | \sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths)) 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Store/StoreBuilder.php: -------------------------------------------------------------------------------- 1 | paths = $paths; 57 | $this->names = $names; 58 | $this->shortCircuit = $shortCircuit; 59 | $this->fileEncoding = $fileEncoding; 60 | } 61 | 62 | /** 63 | * Create a new store builder instance with no names. 64 | * 65 | * @return \Dotenv\Store\StoreBuilder 66 | */ 67 | public static function createWithNoNames() 68 | { 69 | return new self(); 70 | } 71 | 72 | /** 73 | * Create a new store builder instance with the default name. 74 | * 75 | * @return \Dotenv\Store\StoreBuilder 76 | */ 77 | public static function createWithDefaultName() 78 | { 79 | return new self([], [self::DEFAULT_NAME]); 80 | } 81 | 82 | /** 83 | * Creates a store builder with the given path added. 84 | * 85 | * @param string $path 86 | * 87 | * @return \Dotenv\Store\StoreBuilder 88 | */ 89 | public function addPath(string $path) 90 | { 91 | return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding); 92 | } 93 | 94 | /** 95 | * Creates a store builder with the given name added. 96 | * 97 | * @param string $name 98 | * 99 | * @return \Dotenv\Store\StoreBuilder 100 | */ 101 | public function addName(string $name) 102 | { 103 | return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding); 104 | } 105 | 106 | /** 107 | * Creates a store builder with short circuit mode enabled. 108 | * 109 | * @return \Dotenv\Store\StoreBuilder 110 | */ 111 | public function shortCircuit() 112 | { 113 | return new self($this->paths, $this->names, true, $this->fileEncoding); 114 | } 115 | 116 | /** 117 | * Creates a store builder with the specified file encoding. 118 | * 119 | * @param string|null $fileEncoding 120 | * 121 | * @return \Dotenv\Store\StoreBuilder 122 | */ 123 | public function fileEncoding(?string $fileEncoding = null) 124 | { 125 | return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding); 126 | } 127 | 128 | /** 129 | * Creates a new store instance. 130 | * 131 | * @return \Dotenv\Store\StoreInterface 132 | */ 133 | public function make() 134 | { 135 | return new FileStore( 136 | Paths::filePaths($this->paths, $this->names), 137 | $this->shortCircuit, 138 | $this->fileEncoding 139 | ); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/Store/StoreInterface.php: -------------------------------------------------------------------------------- 1 | content = $content; 26 | } 27 | 28 | /** 29 | * Read the content of the environment file(s). 30 | * 31 | * @return string 32 | */ 33 | public function read() 34 | { 35 | return $this->content; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Util/Regex.php: -------------------------------------------------------------------------------- 1 | 34 | */ 35 | public static function matches(string $pattern, string $subject) 36 | { 37 | return self::pregAndWrap(static function (string $subject) use ($pattern) { 38 | return @\preg_match($pattern, $subject) === 1; 39 | }, $subject); 40 | } 41 | 42 | /** 43 | * Perform a preg match all, wrapping up the result. 44 | * 45 | * @param string $pattern 46 | * @param string $subject 47 | * 48 | * @return \GrahamCampbell\ResultType\Result 49 | */ 50 | public static function occurrences(string $pattern, string $subject) 51 | { 52 | return self::pregAndWrap(static function (string $subject) use ($pattern) { 53 | return (int) @\preg_match_all($pattern, $subject); 54 | }, $subject); 55 | } 56 | 57 | /** 58 | * Perform a preg replace callback, wrapping up the result. 59 | * 60 | * @param string $pattern 61 | * @param callable(string[]): string $callback 62 | * @param string $subject 63 | * @param int|null $limit 64 | * 65 | * @return \GrahamCampbell\ResultType\Result 66 | */ 67 | public static function replaceCallback(string $pattern, callable $callback, string $subject, ?int $limit = null) 68 | { 69 | return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) { 70 | return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1); 71 | }, $subject); 72 | } 73 | 74 | /** 75 | * Perform a preg split, wrapping up the result. 76 | * 77 | * @param string $pattern 78 | * @param string $subject 79 | * 80 | * @return \GrahamCampbell\ResultType\Result 81 | */ 82 | public static function split(string $pattern, string $subject) 83 | { 84 | return self::pregAndWrap(static function (string $subject) use ($pattern) { 85 | /** @var string[] */ 86 | return (array) @\preg_split($pattern, $subject); 87 | }, $subject); 88 | } 89 | 90 | /** 91 | * Perform a preg operation, wrapping up the result. 92 | * 93 | * @template V 94 | * 95 | * @param callable(string): V $operation 96 | * @param string $subject 97 | * 98 | * @return \GrahamCampbell\ResultType\Result 99 | */ 100 | private static function pregAndWrap(callable $operation, string $subject) 101 | { 102 | $result = $operation($subject); 103 | 104 | if (\preg_last_error() !== \PREG_NO_ERROR) { 105 | /** @var \GrahamCampbell\ResultType\Result */ 106 | return Error::create(\preg_last_error_msg()); 107 | } 108 | 109 | /** @var \GrahamCampbell\ResultType\Result */ 110 | return Success::create($result); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/Util/Str.php: -------------------------------------------------------------------------------- 1 | 35 | */ 36 | public static function utf8(string $input, ?string $encoding = null) 37 | { 38 | if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) { 39 | /** @var \GrahamCampbell\ResultType\Result */ 40 | return Error::create( 41 | \sprintf('Illegal character encoding [%s] specified.', $encoding) 42 | ); 43 | } 44 | 45 | $converted = $encoding === null ? 46 | @\mb_convert_encoding($input, 'UTF-8') : 47 | @\mb_convert_encoding($input, 'UTF-8', $encoding); 48 | 49 | if (!is_string($converted)) { 50 | /** @var \GrahamCampbell\ResultType\Result */ 51 | return Error::create( 52 | \sprintf('Conversion from encoding [%s] failed.', $encoding ?? 'NULL') 53 | ); 54 | } 55 | 56 | /** 57 | * this is for support UTF-8 with BOM encoding 58 | * @see https://en.wikipedia.org/wiki/Byte_order_mark 59 | * @see https://github.com/vlucas/phpdotenv/issues/500 60 | */ 61 | if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") { 62 | $converted = \substr($converted, 3); 63 | } 64 | 65 | /** @var \GrahamCampbell\ResultType\Result */ 66 | return Success::create($converted); 67 | } 68 | 69 | /** 70 | * Search for a given substring of the input. 71 | * 72 | * @param string $haystack 73 | * @param string $needle 74 | * 75 | * @return \PhpOption\Option 76 | */ 77 | public static function pos(string $haystack, string $needle) 78 | { 79 | /** @var \PhpOption\Option */ 80 | return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false); 81 | } 82 | 83 | /** 84 | * Grab the specified substring of the input. 85 | * 86 | * @param string $input 87 | * @param int $start 88 | * @param int|null $length 89 | * 90 | * @return string 91 | */ 92 | public static function substr(string $input, int $start, ?int $length = null) 93 | { 94 | return \mb_substr($input, $start, $length, 'UTF-8'); 95 | } 96 | 97 | /** 98 | * Compute the length of the given string. 99 | * 100 | * @param string $input 101 | * 102 | * @return int 103 | */ 104 | public static function len(string $input) 105 | { 106 | return \mb_strlen($input, 'UTF-8'); 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/Validator.php: -------------------------------------------------------------------------------- 1 | repository = $repository; 39 | $this->variables = $variables; 40 | } 41 | 42 | /** 43 | * Assert that each variable is present. 44 | * 45 | * @throws \Dotenv\Exception\ValidationException 46 | * 47 | * @return \Dotenv\Validator 48 | */ 49 | public function required() 50 | { 51 | return $this->assert( 52 | static function (?string $value) { 53 | return $value !== null; 54 | }, 55 | 'is missing' 56 | ); 57 | } 58 | 59 | /** 60 | * Assert that each variable is not empty. 61 | * 62 | * @throws \Dotenv\Exception\ValidationException 63 | * 64 | * @return \Dotenv\Validator 65 | */ 66 | public function notEmpty() 67 | { 68 | return $this->assertNullable( 69 | static function (string $value) { 70 | return Str::len(\trim($value)) > 0; 71 | }, 72 | 'is empty' 73 | ); 74 | } 75 | 76 | /** 77 | * Assert that each specified variable is an integer. 78 | * 79 | * @throws \Dotenv\Exception\ValidationException 80 | * 81 | * @return \Dotenv\Validator 82 | */ 83 | public function isInteger() 84 | { 85 | return $this->assertNullable( 86 | static function (string $value) { 87 | return \ctype_digit($value); 88 | }, 89 | 'is not an integer' 90 | ); 91 | } 92 | 93 | /** 94 | * Assert that each specified variable is a boolean. 95 | * 96 | * @throws \Dotenv\Exception\ValidationException 97 | * 98 | * @return \Dotenv\Validator 99 | */ 100 | public function isBoolean() 101 | { 102 | return $this->assertNullable( 103 | static function (string $value) { 104 | if ($value === '') { 105 | return false; 106 | } 107 | 108 | return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null; 109 | }, 110 | 'is not a boolean' 111 | ); 112 | } 113 | 114 | /** 115 | * Assert that each variable is amongst the given choices. 116 | * 117 | * @param string[] $choices 118 | * 119 | * @throws \Dotenv\Exception\ValidationException 120 | * 121 | * @return \Dotenv\Validator 122 | */ 123 | public function allowedValues(array $choices) 124 | { 125 | return $this->assertNullable( 126 | static function (string $value) use ($choices) { 127 | return \in_array($value, $choices, true); 128 | }, 129 | \sprintf('is not one of [%s]', \implode(', ', $choices)) 130 | ); 131 | } 132 | 133 | /** 134 | * Assert that each variable matches the given regular expression. 135 | * 136 | * @param string $regex 137 | * 138 | * @throws \Dotenv\Exception\ValidationException 139 | * 140 | * @return \Dotenv\Validator 141 | */ 142 | public function allowedRegexValues(string $regex) 143 | { 144 | return $this->assertNullable( 145 | static function (string $value) use ($regex) { 146 | return Regex::matches($regex, $value)->success()->getOrElse(false); 147 | }, 148 | \sprintf('does not match "%s"', $regex) 149 | ); 150 | } 151 | 152 | /** 153 | * Assert that the callback returns true for each variable. 154 | * 155 | * @param callable(?string):bool $callback 156 | * @param string $message 157 | * 158 | * @throws \Dotenv\Exception\ValidationException 159 | * 160 | * @return \Dotenv\Validator 161 | */ 162 | public function assert(callable $callback, string $message) 163 | { 164 | $failing = []; 165 | 166 | foreach ($this->variables as $variable) { 167 | if ($callback($this->repository->get($variable)) === false) { 168 | $failing[] = \sprintf('%s %s', $variable, $message); 169 | } 170 | } 171 | 172 | if (\count($failing) > 0) { 173 | throw new ValidationException(\sprintf( 174 | 'One or more environment variables failed assertions: %s.', 175 | \implode(', ', $failing) 176 | )); 177 | } 178 | 179 | return $this; 180 | } 181 | 182 | /** 183 | * Assert that the callback returns true for each variable. 184 | * 185 | * Skip checking null variable values. 186 | * 187 | * @param callable(string):bool $callback 188 | * @param string $message 189 | * 190 | * @throws \Dotenv\Exception\ValidationException 191 | * 192 | * @return \Dotenv\Validator 193 | */ 194 | public function assertNullable(callable $callback, string $message) 195 | { 196 | return $this->assert( 197 | static function (?string $value) use ($callback) { 198 | if ($value === null) { 199 | return true; 200 | } 201 | 202 | return $callback($value); 203 | }, 204 | $message 205 | ); 206 | } 207 | } 208 | --------------------------------------------------------------------------------