From 021d72b66d4dd581c8c209a03a76fc2c9e725d5e Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sat, 30 Nov 2019 18:27:26 +0000 Subject: [PATCH] Refactoring --- README.md | 79 +++-- UPGRADING.md | 70 ++++- src/Dotenv.php | 158 ++++++---- src/Environment/Adapter/AdapterInterface.php | 41 --- src/Environment/DotenvFactory.php | 58 ---- src/Environment/DotenvVariables.php | 78 ----- src/Environment/FactoryInterface.php | 26 -- src/Exception/ExceptionInterface.php | 3 - src/Exception/InvalidFileException.php | 3 - src/Exception/InvalidPathException.php | 3 - src/Exception/ValidationException.php | 3 - src/Loader.php | 266 ----------------- src/{ => Loader}/Lines.php | 2 +- src/Loader/Loader.php | 97 +++++++ src/Loader/LoaderInterface.php | 20 ++ src/{ => Loader}/Parser.php | 4 +- src/{ => Loader}/Value.php | 6 +- src/{ => Regex}/Regex.php | 2 +- .../AbstractRepository.php} | 27 +- .../Adapter/ApacheAdapter.php | 4 +- .../Adapter/ArrayAdapter.php | 4 +- .../Adapter/AvailabilityInterface.php | 13 + .../Adapter/EnvConstAdapter.php | 4 +- .../Adapter/PutenvAdapter.php | 6 +- src/Repository/Adapter/ReaderInterface.php | 15 + .../Adapter/ServerConstAdapter.php | 4 +- src/Repository/Adapter/WriterInterface.php | 25 ++ src/Repository/AdapterRepository.php | 84 ++++++ src/Repository/RepositoryBuilder.php | 146 ++++++++++ .../RepositoryInterface.php} | 14 +- src/Validator.php | 31 +- tests/Dotenv/DotenvTest.php | 75 ++--- tests/Dotenv/EnvironmentVariablesTest.php | 150 ---------- tests/Dotenv/FactoryTest.php | 44 --- tests/Dotenv/LinesTest.php | 2 +- tests/Dotenv/LoaderTest.php | 148 ++-------- tests/Dotenv/ParserTest.php | 23 +- tests/Dotenv/RepositoryTest.php | 272 ++++++++++++++++++ tests/Dotenv/ValidatorTest.php | 58 ++-- 39 files changed, 1046 insertions(+), 1022 deletions(-) delete mode 100644 src/Environment/Adapter/AdapterInterface.php delete mode 100644 src/Environment/DotenvFactory.php delete mode 100644 src/Environment/DotenvVariables.php delete mode 100644 src/Environment/FactoryInterface.php delete mode 100644 src/Loader.php rename src/{ => Loader}/Lines.php (99%) create mode 100644 src/Loader/Loader.php create mode 100644 src/Loader/LoaderInterface.php rename src/{ => Loader}/Parser.php (99%) rename src/{ => Loader}/Value.php (92%) rename src/{ => Regex}/Regex.php (99%) rename src/{Environment/AbstractVariables.php => Repository/AbstractRepository.php} (83%) rename src/{Environment => Repository}/Adapter/ApacheAdapter.php (91%) rename src/{Environment => Repository}/Adapter/ArrayAdapter.php (90%) create mode 100644 src/Repository/Adapter/AvailabilityInterface.php rename src/{Environment => Repository}/Adapter/EnvConstAdapter.php (88%) rename src/{Environment => Repository}/Adapter/PutenvAdapter.php (81%) create mode 100644 src/Repository/Adapter/ReaderInterface.php rename src/{Environment => Repository}/Adapter/ServerConstAdapter.php (88%) create mode 100644 src/Repository/Adapter/WriterInterface.php create mode 100644 src/Repository/AdapterRepository.php create mode 100644 src/Repository/RepositoryBuilder.php rename src/{Environment/VariablesInterface.php => Repository/RepositoryInterface.php} (76%) delete mode 100644 tests/Dotenv/EnvironmentVariablesTest.php delete mode 100644 tests/Dotenv/FactoryTest.php create mode 100644 tests/Dotenv/RepositoryTest.php diff --git a/README.md b/README.md index 2f8c9b2f..91941b9c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,21 @@ dotenv](https://github.com/bkeepers/dotenv). [![Build Status](https://travis-ci.org/vlucas/phpdotenv.svg?branch=master)](https://travis-ci.org/vlucas/phpdotenv) +UPGRADING FROM V3 +----------------- + +Version 4 seems some refactoring, and support for escaping dollars in values +(https://github.com/vlucas/phpdotenv/pull/380). It is no longer possible to +change immutability on the fly, and the `Loader` no longer is responsible for +tracking immutability. It is now the responsibility of "repositories" to track +this. One must explicitly decide if they want (im)mutability when constructing +an instance of `Dotenv\Dotenv`. + +For more details, please see the +[release notes](https://github.com/vlucas/phpdotenv/releases/tag/v4.0.0) and +the [upgrading guide](UPGRADING.md). + + UPGRADING FROM V2 ----------------- @@ -100,14 +115,14 @@ SECRET_KEY="abc123" You can then load `.env` in your application with: ```php -$dotenv = Dotenv\Dotenv::create(__DIR__); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__); $dotenv->load(); ``` Optionally you can pass in a filename as the second parameter, if you would like to use something other than `.env` ```php -$dotenv = Dotenv\Dotenv::create(__DIR__, 'myconfig'); +$dotenv = Dotenv\Dotenv::createImmutable(__DIR__, 'myconfig'); $dotenv->load(); ``` @@ -143,34 +158,42 @@ CACHE_DIR="${BASE_DIR}/cache" TMP_DIR="${BASE_DIR}/tmp" ``` -### Immutability +### Immutability and Repository Customization -By default, Dotenv will NOT overwrite existing environment variables that are -already set in the environment. - -If you want Dotenv to overwrite existing environment variables, use `overload` -instead of `load`: +Immutability refers to if Dotenv is allowed to overwrite existing environment +variables. If you want Dotenv to overwrite existing environment variables, +use `createMutable` instead of `createImmutable`: ```php -$dotenv = Dotenv\Dotenv::create(__DIR__); -$dotenv->overload(); +$dotenv = Dotenv\Dotenv::createMutable(__DIR__); +$dotenv->load(); ``` -### Loader Customization - -Need us to not set `$_ENV` but not `$_SERVER`, or have other custom requirements? No problem! Simply pass a custom implementation of `Dotenv\Environment\FactoryInterface` to `Dotenv\Loader` on construction. In practice, you may not even need a custom implementation, since our default implementation allows you provide an array of `Dotenv\Environment\Adapter\AdapterInterface` for proxing the underlying calls to. - -For example, if you want us to only ever fiddle with `$_ENV` and `putenv`, then you can setup Dotenv as follows: +Behind the scenes, this is instructing the "repository" to allow immutability +or not. By default, the repository is configured to allow overwriting existing +values by default, which is relevent if one is calling the "create" method +using the `RepositoryBuilder` to construct a more custom repository: ```php -$factory = new Dotenv\Environment\DotenvFactory([ - new Dotenv\Environment\Adapter\EnvConstAdapter(), - new Dotenv\Environment\Adapter\PutenvAdapter(), -]); - -$dotenv = Dotenv\Dotenv::create(__DIR__, null, $factory); +$repository = Dotenv\Repository\RepositoryBuilder::create() + ->withReaders([ + new Dotenv\Repository\Adapter\EnvConstAdapter(), + ]) + ->withWriters([ + new Dotenv\Repository\Adapter\EnvConstAdapter(), + new Dotenv\Repository\Adapter\PutenvAdapter(), + ]) + ->immutable() + ->get(); + +$dotenv = Dotenv\Dotenv::create($repository, __DIR__); +$dotenv->load(); ``` +The above example will write loaded values to `$_ENV` and `putenv`, but when +interpolating environment variables, we'll only read from `$_ENV`. Moreover, it +will never replace any variables already set before loading the file. + Requiring Variables to be Set ----------------------------- @@ -305,11 +328,11 @@ PHP dotenv is licensed under [The BSD 3-Clause License](LICENSE). ---
- - Get professional support for PHP dotenv with a Tidelift subscription - -
- - Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. -
+ + Get professional support for PHP dotenv with a Tidelift subscription + +
+ + Tidelift helps make open source sustainable for maintainers while giving companies
assurances about security, maintenance, and licensing for their dependencies. +
diff --git a/UPGRADING.md b/UPGRADING.md index 2210ccb6..f5c23a62 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,69 @@ # Upgrading Guide +## V3 to V4 + +V4 has again changed the way you initialize the `Dotenv` class. If you want immutable loading of environment variables, then replace `Dotenv::create` with `Dotenv::createImmutable`, and if you want mutable loading, replace `Dotenv::create` with `Dotenv::createMmutable` and `->overload()` with `->load()`. The `overload` method has been removed in faviour of specifying mutability at object construction. + +The behaviour when parsing single quoted strings has now changed, to mimic the behaviour of bash. It is no longer possible to escape characters in single quoted strings, and everything is treated literally. As soon as the first single quote character is read, after the initial one, then the variable is treated as ending immediately at that point. When parsing unquoted or double quoted strings, it is now possible to escape dollar signs, to forcefully avoid variable interpolation. Escaping dollars is not mandated, in the sense that if a dollar is present, and not following by variable interpolation sytnax, this is allowed, and the dollar will be treated as a literal dollar. Finally, interpolation of variables is now performed right to left, instead of left to right, so it is possible to nest interpolations to allow using the value of a variable as the name of another for further interpolation. + +The `getEnvironmentVariableNames` method is no longer available. This is because calls to `load()` (since v3.0.0) return an associative array of what was loaded, so `$dotenv->getEnvironmentVariableNames()` can be replaced with `array_keys($dotenv->load())`. + +There have been various internal refactorings. Appart from what has already been mentioned, the only other changes likely to affect developers is: + +1. The `Dotenv\Environment` namespace has been moved to `Dotenv\Repository`, the `Dotenv\Environment\Adapter\AdapterInterface` interface has been replaced by `Dotenv\Repository\Adapter\ReaderInterface` and `Dotenv\Repository\Adapter\WriterInterface`. +2. The `Dotenv\Environment\DotenvFactory` has been (roughly) replaced by `Dotenv\Environment\RepositoryBuilder`, and `Dotenv\Environment\FactoryInterface` has been deleted. +3. `Dotenv\Environment\AbstractVariables` has been replaced by `Dotenv\Repository\AbstractRepository`, `Dotenv\Environment\DotenvVariables` has been replaced by `Dotenv\Repository\AdapterRepository`, and `Dotenv\Environment\VariablesInterface` has been replaced by `Dotenv\Repository\RepositoryInterface`. +4. The `Dotenv\Loader` class has been moved to `Dotenv\Loader\Loader`, and now has a different public interface. It no longer expects any parameters at construction, and implements only the new interface `Dotenv\Loader\LoaderInterface`. Its reponsibility has changed to purely taking raw env file content, and handing it off to the parser, dealing with variable interpolation, and sending off instructions to the repository to set variables. No longer can it be used as a way to read the environment by callers, and nor does it track immutability. +5. The `Dotenv\Parser` and `Dotenv\Lines` classes have moved to `Dotenv\Loader\Parser` and `Dotenv\Loader\Lines`, respectively. `Dotenv\Loader\Parser::parse` now return has either `null` or `Dotenv\Loader\Value` objects as values, instead of `string`s. This is to support the new variable interpolation and dollar escaping features. +6. The `Dotenv\Validator` constructor has changed from `__construct(array $variables, Loader $loader, $required = true)` to `__construct(RepositoryInterface $repository, array $variables, $required = true)`. + +The example at the bottom of the below upgrading guide, in V4 now looks like: + +```php +withReaders($adapters) + ->withWriters($adapters) + ->immutable() + ->get(); + +Dotenv::create($repository, $path, null)->load(); +``` + +Since v3.2.0, it was easily possible to read a file and process variable interpolations, without actually "loading" the variables. This is still possible in v4.0.0. Example code that does this is as follows: + +```php +withReaders($adapters) + ->withWriters($adapters) + ->get(); + +$variables = (new Loader())->load($repository, $content); +``` + +Notice, that compared to v3, the loader no longer expects file paths in the constructor. Reading of the files is now managed by the `Dotenv\Dotenv` class. The loader is geuinely just loading the content into the repository. + +Finally, we note that the minimum supported version of PHP has increased to 5.5.9, up from 5.4.0 in V3 and 5.3.9 in V2. + ## V2 to V3 V3 has changed the way you initialize the `Dotenv` class. Consequently, you will need to replace any occurrences of new Dotenv(...) with Dotenv::create(...), since our new native constructor takes a `Loader` instance now. @@ -24,17 +88,19 @@ Value parsing has been modified in the following ways: In double quoted strings, double quotes and backslashes need escaping with a backslash, and in single quoted strings, single quote and backslashes need escaping with a backslash. In v2.5.2, forgetting an escape can lead to odd results due to the regex running out of stack, but this was fixed in 2.6 and 3.3, with 2.6 allowing you to continue after an unescaped backslash, but 3.3 not. -Finally, it's possible to use phpdotenv V3 in a threaded environment, instructing it to not call any functions that are not tread-safe: +It's possible to use phpdotenv V3 in a threaded environment, instructing it to not call any functions that are not tread-safe: ```php load(); ``` + +Finally, we note that the minimum supported version of PHP has increased from 5.3.9 to 5.4.0. diff --git a/src/Dotenv.php b/src/Dotenv.php index 41a2d3d5..d3e1b2cd 100644 --- a/src/Dotenv.php +++ b/src/Dotenv.php @@ -2,70 +2,94 @@ namespace Dotenv; -use Dotenv\Environment\DotenvFactory; -use Dotenv\Environment\FactoryInterface; +use Dotenv\Loader\Loader; +use Dotenv\Loader\LoaderInterface; +use Dotenv\Repository\RepositoryBuilder; +use Dotenv\Repository\RepositoryInterface; use Dotenv\Exception\InvalidPathException; +use PhpOption\Option; -/** - * This is the dotenv class. - * - * It's responsible for loading a `.env` file in the given directory and - * setting the environment variables. - */ class Dotenv { /** * The loader instance. * - * @var \Dotenv\Loader + * @var \Dotenv\Loader\LoaderInterface */ protected $loader; + /** + * The repository instance. + * + * @var \Dotenv\Repository\RepositoryInterface + */ + protected $repository; + + /** + * The file paths. + * + * @var string[] + */ + protected $filePaths; + /** * Create a new dotenv instance. * - * @param \Dotenv\Loader $loader + * @param \Dotenv\Loader\LoaderInterface $loader + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string[] $filePaths * * @return void */ - public function __construct(Loader $loader) + public function __construct(LoaderInterface $loader, RepositoryInterface $repository, array $filePaths) { $this->loader = $loader; + $this->repository = $repository; + $this->filePaths = $filePaths; } /** * Create a new dotenv instance. * - * @param string|string[] $paths - * @param string|null $file - * @param \Dotenv\Environment\FactoryInterface|null $envFactory + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string|string[] $paths + * @param string|null $file * * @return \Dotenv\Dotenv */ - public static function create($paths, $file = null, FactoryInterface $envFactory = null) + public static function create(RepositoryInterface $repository, $paths, $file = null) { - $loader = new Loader( - self::getFilePaths((array) $paths, $file ?: '.env'), - $envFactory ?: new DotenvFactory(), - true - ); + return new self(new Loader(), $repository, self::getFilePaths((array) $paths, $file ?: '.env')); + } + + /** + * Create a new mutable dotenv instance with default repository. + * + * @param string|string[] $paths + * @param string|null $file + * + * @return \Dotenv\Dotenv + */ + public static function createMutable($paths, $file = null) + { + $repository = RepositoryBuilder::create()->make(); - return new self($loader); + return self::create($repository, $paths, $file); } /** - * Returns the full paths to the files. + * Create a new mutable dotenv instance with default repository. * - * @param string[] $paths - * @param string $file + * @param string|string[] $paths + * @param string|null $file * - * @return string[] + * @return \Dotenv\Dotenv */ - private static function getFilePaths(array $paths, $file) + public static function createImmutable($paths, $file = null) { - return array_map(function ($path) use ($file) { - return rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$file; - }, $paths); + $repository = RepositoryBuilder::create()->immutable()->make(); + + return self::create($repository, $paths, $file); } /** @@ -77,7 +101,7 @@ private static function getFilePaths(array $paths, $file) */ public function load() { - return $this->loadData(); + return $this->loader->load($this->repository, self::findAndRead($this->filePaths)); } /** @@ -90,7 +114,7 @@ public function load() public function safeLoad() { try { - return $this->loadData(); + return $this->load(); } catch (InvalidPathException $e) { // suppressing exception return []; @@ -98,62 +122,82 @@ public function safeLoad() } /** - * Load environment file in given directory. + * Required ensures that the specified variables exist, and returns a new validator object. * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException + * @param string|string[] $variables * - * @return array + * @return \Dotenv\Validator */ - public function overload() + public function required($variables) { - return $this->loadData(true); + return new Validator($this->repository, (array) $variables); } /** - * Actually load the data. - * - * @param bool $overload + * Returns a new validator object that won't check if the specified variables exist. * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException + * @param string|string[] $variables * - * @return array + * @return \Dotenv\Validator */ - protected function loadData($overload = false) + public function ifPresent($variables) { - return $this->loader->setImmutable(!$overload)->load(); + return new Validator($this->repository, (array) $variables, false); } /** - * Required ensures that the specified variables exist, and returns a new validator object. + * Returns the full paths to the files. * - * @param string|string[] $variables + * @param string[] $paths + * @param string $file * - * @return \Dotenv\Validator + * @return string[] */ - public function required($variables) + private static function getFilePaths(array $paths, $file) { - return new Validator((array) $variables, $this->loader); + return array_map(function ($path) use ($file) { + return rtrim($path, DIRECTORY_SEPARATOR).DIRECTORY_SEPARATOR.$file; + }, $paths); } /** - * Returns a new validator object that won't check if the specified variables exist. + * Attempt to read the files in order. * - * @param string|string[] $variables + * @param string[] $filePaths * - * @return \Dotenv\Validator + * @throws \Dotenv\Exception\InvalidPathException + * + * @return string[] */ - public function ifPresent($variables) + private static function findAndRead(array $filePaths) { - return new Validator((array) $variables, $this->loader, false); + if ($filePaths === []) { + throw new InvalidPathException('At least one environment file path must be provided.'); + } + + foreach ($filePaths as $filePath) { + $lines = self::readFromFile($filePath); + if ($lines->isDefined()) { + return $lines->get(); + } + } + + throw new InvalidPathException( + sprintf('Unable to read any of the environment file(s) at [%s].', implode(', ', $filePaths)) + ); } /** - * Get the list of environment variables declared inside the 'env' file. + * Read the given file. * - * @return string[] + * @param string $filePath + * + * @return \PhpOption\Option */ - public function getEnvironmentVariableNames() + private static function readFromFile($filePath) { - return $this->loader->getEnvironmentVariableNames(); + $content = @file_get_contents($filePath); + + return Option::fromValue($content, false); } } diff --git a/src/Environment/Adapter/AdapterInterface.php b/src/Environment/Adapter/AdapterInterface.php deleted file mode 100644 index 21ef29a1..00000000 --- a/src/Environment/Adapter/AdapterInterface.php +++ /dev/null @@ -1,41 +0,0 @@ -adapters = array_filter($adapters === null ? [new ApacheAdapter(), new EnvConstAdapter(), new ServerConstAdapter(), new PutenvAdapter()] : $adapters, function (AdapterInterface $adapter) { - return $adapter->isSupported(); - }); - } - - /** - * Creates a new mutable environment variables instance. - * - * @return \Dotenv\Environment\VariablesInterface - */ - public function create() - { - return new DotenvVariables($this->adapters, false); - } - - /** - * Creates a new immutable environment variables instance. - * - * @return \Dotenv\Environment\VariablesInterface - */ - public function createImmutable() - { - return new DotenvVariables($this->adapters, true); - } -} diff --git a/src/Environment/DotenvVariables.php b/src/Environment/DotenvVariables.php deleted file mode 100644 index 486f0cdd..00000000 --- a/src/Environment/DotenvVariables.php +++ /dev/null @@ -1,78 +0,0 @@ -adapters = $adapters; - parent::__construct($immutable); - } - - /** - * Get an environment variable. - * - * We do this by querying our adapters sequentially. - * - * @param string $name - * - * @return string|null - */ - protected function getInternal($name) - { - foreach ($this->adapters as $adapter) { - $result = $adapter->get($name); - if ($result->isDefined()) { - return $result->get(); - } - } - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @return void - */ - protected function setInternal($name, $value = null) - { - foreach ($this->adapters as $adapter) { - $adapter->set($name, $value); - } - } - - /** - * Clear an environment variable. - * - * @param string $name - * - * @return void - */ - protected function clearInternal($name) - { - foreach ($this->adapters as $adapter) { - $adapter->clear($name); - } - } -} diff --git a/src/Environment/FactoryInterface.php b/src/Environment/FactoryInterface.php deleted file mode 100644 index 3d9f489b..00000000 --- a/src/Environment/FactoryInterface.php +++ /dev/null @@ -1,26 +0,0 @@ -filePaths = $filePaths; - $this->envFactory = $envFactory; - $this->setImmutable($immutable); - } - - /** - * Set immutable value. - * - * @param bool $immutable - * - * @return $this - */ - public function setImmutable($immutable = false) - { - $this->envVariables = $immutable - ? $this->envFactory->createImmutable() - : $this->envFactory->create(); - - return $this; - } - - /** - * Load the environment file from disk. - * - * @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidFileException - * - * @return array - */ - public function load() - { - return $this->loadDirect( - self::findAndRead($this->filePaths) - ); - } - - /** - * Directly load the given string. - * - * @param string $content - * - * @throws \Dotenv\Exception\InvalidFileException - * - * @return array - */ - public function loadDirect($content) - { - return $this->processEntries( - Lines::process(preg_split("/(\r\n|\n|\r)/", $content)) - ); - } - - /** - * Attempt to read the files in order. - * - * @param string[] $filePaths - * - * @throws \Dotenv\Exception\InvalidPathException - * - * @return string[] - */ - private static function findAndRead(array $filePaths) - { - if ($filePaths === []) { - throw new InvalidPathException('At least one environment file path must be provided.'); - } - - foreach ($filePaths as $filePath) { - $lines = self::readFromFile($filePath); - if ($lines->isDefined()) { - return $lines->get(); - } - } - - throw new InvalidPathException( - sprintf('Unable to read any of the environment file(s) at [%s].', implode(', ', $filePaths)) - ); - } - - /** - * Read the given file. - * - * @param string $filePath - * - * @return \PhpOption\Option - */ - private static function readFromFile($filePath) - { - $content = @file_get_contents($filePath); - - return Option::fromValue($content, false); - } - - /** - * Process the environment variable entries. - * - * We'll fill out any nested variables, and acually set the variable using - * the underlying environment variables instance. - * - * @param string[] $entries - * - * @throws \Dotenv\Exception\InvalidFileException - * - * @return array - */ - private function processEntries(array $entries) - { - $vars = []; - - foreach ($entries as $entry) { - list($name, $value) = Parser::parse($entry); - $vars[$name] = $this->resolveNestedVariables($value); - $this->setEnvironmentVariable($name, $vars[$name]); - } - - return $vars; - } - - /** - * Resolve the nested variables. - * - * Look for ${varname} patterns in the variable value and replace with an - * existing environment variable. - * - * @param \Dotenv\Value|null $value - * - * @return string|null - */ - private function resolveNestedVariables(Value $value = null) - { - return Option::fromValue($value) - ->map(function ($v) { - return array_reduce($v->getVars(), function ($s, $i) { - return substr($s, 0, $i).$this->resolveNestedVariable(substr($s, $i)); - }, $v->getChars()); - }) - ->getOrElse(null); - } - - /** - * Resolve a single nested variable. - * - * @param string $str - * - * @return string - */ - private function resolveNestedVariable($str) - { - return Regex::replaceCallback( - '/\A\${([a-zA-Z0-9_.]+)}/', - function (array $matches) { - return Option::fromValue($this->getEnvironmentVariable($matches[1])) - ->getOrElse($matches[0]); - }, - $str, - 1 - )->success()->getOrElse($str); - } - - /** - * Search the different places for environment variables and return first value found. - * - * @param string $name - * - * @return string|null - */ - public function getEnvironmentVariable($name) - { - return $this->envVariables->get($name); - } - - /** - * Set an environment variable. - * - * @param string $name - * @param string|null $value - * - * @return void - */ - public function setEnvironmentVariable($name, $value = null) - { - $this->variableNames[] = $name; - $this->envVariables->set($name, $value); - } - - /** - * Clear an environment variable. - * - * This method only expects names in normal form. - * - * @param string $name - * - * @return void - */ - public function clearEnvironmentVariable($name) - { - $this->envVariables->clear($name); - } - - /** - * Get the list of environment variables names. - * - * @return string[] - */ - public function getEnvironmentVariableNames() - { - return $this->variableNames; - } -} diff --git a/src/Lines.php b/src/Loader/Lines.php similarity index 99% rename from src/Lines.php rename to src/Loader/Lines.php index 1fa6c325..e925e089 100644 --- a/src/Lines.php +++ b/src/Loader/Lines.php @@ -1,6 +1,6 @@ + */ + public function load(RepositoryInterface $repository, $content) + { + return self::processEntries( + $repository, + Lines::process(preg_split("/(\r\n|\n|\r)/", $content)) + ); + } + + /** + * Process the environment variable entries. + * + * We'll fill out any nested variables, and acually set the variable using + * the underlying environment variables instance. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string[] $entries + * + * @throws \Dotenv\Exception\InvalidFileException + * + * @return array + */ + private static function processEntries(RepositoryInterface $repository, array $entries) + { + $vars = []; + + foreach ($entries as $entry) { + list($name, $value) = Parser::parse($entry); + $vars[$name] = self::resolveNestedVariables($repository, $value); + $repository->set($name, $vars[$name]); + } + + return $vars; + } + + /** + * Resolve the nested variables. + * + * Look for ${varname} patterns in the variable value and replace with an + * existing environment variable. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param \Dotenv\Loader\Value|null $value + * + * @return string|null + */ + private static function resolveNestedVariables(RepositoryInterface $repository, Value $value = null) + { + return Option::fromValue($value) + ->map(function ($v) use ($repository) { + return array_reduce($v->getVars(), function ($s, $i) use ($repository) { + return substr($s, 0, $i).self::resolveNestedVariable($repository, substr($s, $i)); + }, $v->getChars()); + }) + ->getOrElse(null); + } + + /** + * Resolve a single nested variable. + * + * @param \Dotenv\Repository\RepositoryInterface $repository + * @param string $str + * + * @return string + */ + private static function resolveNestedVariable(RepositoryInterface $repository, $str) + { + return Regex::replaceCallback( + '/\A\${([a-zA-Z0-9_.]+)}/', + function (array $matches) use ($repository) { + return Option::fromValue($repository->get($matches[1])) + ->getOrElse($matches[0]); + }, + $str, + 1 + )->success()->getOrElse($str); + } +} diff --git a/src/Loader/LoaderInterface.php b/src/Loader/LoaderInterface.php new file mode 100644 index 00000000..6400f849 --- /dev/null +++ b/src/Loader/LoaderInterface.php @@ -0,0 +1,20 @@ + + */ + public function load(RepositoryInterface $repository, $content); +} diff --git a/src/Parser.php b/src/Loader/Parser.php similarity index 99% rename from src/Parser.php rename to src/Loader/Parser.php index bee0f53b..152e11fe 100644 --- a/src/Parser.php +++ b/src/Loader/Parser.php @@ -1,6 +1,6 @@ isImmutable() && $this->get($name) !== null && $this->loaded->get($name)->isEmpty()) { + if ($this->immutable && $this->get($name) !== null && $this->loaded->get($name)->isEmpty()) { return; } @@ -118,7 +113,7 @@ public function clear($name) } // Don't clear anything if we're immutable. - if ($this->isImmutable()) { + if ($this->immutable) { return; } @@ -134,16 +129,6 @@ public function clear($name) */ protected abstract function clearInternal($name); - /** - * Determine if the environment is immutable. - * - * @return bool - */ - public function isImmutable() - { - return $this->immutable; - } - /** * Tells whether environment variable has been defined. * diff --git a/src/Environment/Adapter/ApacheAdapter.php b/src/Repository/Adapter/ApacheAdapter.php similarity index 91% rename from src/Environment/Adapter/ApacheAdapter.php rename to src/Repository/Adapter/ApacheAdapter.php index be9e09e0..4441a003 100644 --- a/src/Environment/Adapter/ApacheAdapter.php +++ b/src/Repository/Adapter/ApacheAdapter.php @@ -1,10 +1,10 @@ readers = $readers; + $this->writers = $writers; + parent::__construct($immutable); + } + + /** + * Get an environment variable. + * + * We do this by querying our readers sequentially. + * + * @param string $name + * + * @return string|null + */ + protected function getInternal($name) + { + foreach ($this->readers as $reader) { + $result = $reader->get($name); + if ($result->isDefined()) { + return $result->get(); + } + } + } + + /** + * Set an environment variable. + * + * @param string $name + * @param string|null $value + * + * @return void + */ + protected function setInternal($name, $value = null) + { + foreach ($this->writers as $writers) { + $writers->set($name, $value); + } + } + + /** + * Clear an environment variable. + * + * @param string $name + * + * @return void + */ + protected function clearInternal($name) + { + foreach ($this->writers as $writers) { + $writers->clear($name); + } + } +} diff --git a/src/Repository/RepositoryBuilder.php b/src/Repository/RepositoryBuilder.php new file mode 100644 index 00000000..f5150aaa --- /dev/null +++ b/src/Repository/RepositoryBuilder.php @@ -0,0 +1,146 @@ +readers = $readers; + $this->writers = $writers; + $this->immutable = $immutable; + } + + /** + * Create a new repository builder instance. + * + * @return void + */ + public static function create() + { + return new RepositoryBuilder(); + } + + /** + * Creates a repository builder with the given readers. + * + * @param \Dotenv\Repository\Adapter\ReaderInterface[]|null + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function withReaders(array $readers = null) + { + $readers = $readers === null ? null : self::filterByAvailability($readers); + + return new RepositoryBuilder($readers, $this->writers, $this->immutable); + } + + /** + * Creates a repository builder with the given writers. + * + * @param \Dotenv\Repository\Adapter\WriterInterface[]|null + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function withWriters(array $writers = null) + { + $writers = $writers === null ? null : self::filterByAvailability($writers); + + return new RepositoryBuilder($this->readers, $writers, $this->immutable); + } + + /** + * Creates a repository builder with mutability enabled. + * + * @return \Dotenv\Repository\RepositoryBuilder + */ + public function immutable() + { + return new RepositoryBuilder($this->readers, $this->writers, true); + } + + /** + * Creates a new repository instance. + * + * @return \Dotenv\Repository\RepositoryInterface + */ + public function make() + { + if ($this->readers === null || $this->writers === null) { + $defaults = self::defaultAdapters(); + } + + return new AdapterRepository( + $this->readers === null ? $defaults : $this->readers, + $this->writers === null ? $defaults : $this->writers, + $this->immutable + ); + } + + /** + * Return the array of default adapters. + * + * @return \Dotenv\Repository\Adapter\AvailabilityInterface[] + */ + private static function defaultAdapters() + { + return self::filterByAvailability([ + new ApacheAdapter(), + new EnvConstAdapter(), + new ServerConstAdapter(), + new PutenvAdapter(), + ]); + } + + /** + * Filter an array of adapters to only those that are supported. + * + * @param \Dotenv\Repository\Adapter\AvailabilityInterface[] $adapters + * + * @return \Dotenv\Repository\Adapter\AvailabilityInterface[] + */ + private static function filterByAvailability(array $adapters) + { + return array_filter($adapters, function (AvailabilityInterface $adapter) { + return $adapter->isSupported(); + }); + } +} diff --git a/src/Environment/VariablesInterface.php b/src/Repository/RepositoryInterface.php similarity index 76% rename from src/Environment/VariablesInterface.php rename to src/Repository/RepositoryInterface.php index de2a71fb..7a75e18b 100644 --- a/src/Environment/VariablesInterface.php +++ b/src/Repository/RepositoryInterface.php @@ -1,21 +1,11 @@ repository = $repository; $this->variables = $variables; - $this->loader = $loader; if ($required) { $this->assertCallback( @@ -177,7 +174,7 @@ protected function assertCallback(callable $callback, $message = 'failed callbac $failing = []; foreach ($this->variables as $variable) { - if ($callback($this->loader->getEnvironmentVariable($variable)) === false) { + if ($callback($this->repository->get($variable)) === false) { $failing[] = sprintf('%s %s', $variable, $message); } } diff --git a/tests/Dotenv/DotenvTest.php b/tests/Dotenv/DotenvTest.php index d8bb6931..d77bfd6f 100644 --- a/tests/Dotenv/DotenvTest.php +++ b/tests/Dotenv/DotenvTest.php @@ -8,11 +8,11 @@ class DotenvTest extends TestCase /** * @var string */ - private $fixturesFolder; + private $folder; public function setUp() { - $this->fixturesFolder = dirname(__DIR__).'/fixtures/env'; + $this->folder = dirname(__DIR__).'/fixtures/env'; } /** @@ -21,7 +21,7 @@ public function setUp() */ public function testDotenvThrowsExceptionIfUnableToLoadFile() { - $dotenv = Dotenv::create(__DIR__); + $dotenv = Dotenv::createImmutable(__DIR__); $dotenv->load(); } @@ -31,25 +31,36 @@ public function testDotenvThrowsExceptionIfUnableToLoadFile() */ public function testDotenvThrowsExceptionIfUnableToLoadFiles() { - $dotenv = Dotenv::create([__DIR__, __DIR__.'/foo/bar']); + $dotenv = Dotenv::createImmutable([__DIR__, __DIR__.'/foo/bar']); + $dotenv->load(); + } + + /** + * @expectedException \Dotenv\Exception\InvalidPathException + * @expectedExceptionMessage At least one environment file path must be provided. + */ + public function testDotenvThrowsExceptionWhenNoFiles() + { + $dotenv = Dotenv::createImmutable([]); $dotenv->load(); } public function testDotenvTriesPathsToLoad() { - $dotenv = Dotenv::create([__DIR__, $this->fixturesFolder]); + $dotenv = Dotenv::createImmutable([__DIR__, $this->folder]); $this->assertCount(4, $dotenv->load()); } + public function testDotenvSkipsLoadingIfFileIsMissing() { - $dotenv = Dotenv::create(__DIR__); + $dotenv = Dotenv::createImmutable(__DIR__); $this->assertSame([], $dotenv->safeLoad()); } public function testDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->folder); $this->assertSame( ['FOO' => 'bar', 'BAR' => 'baz', 'SPACED' => 'with spaces', 'NULL' => ''], $dotenv->load() @@ -62,7 +73,7 @@ public function testDotenvLoadsEnvironmentVars() public function testCommentedDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'commented.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'commented.env'); $dotenv->load(); $this->assertSame('bar', getenv('CFOO')); $this->assertFalse(getenv('CBAR')); @@ -78,7 +89,7 @@ public function testCommentedDotenvLoadsEnvironmentVars() public function testQuotedDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'quoted.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'quoted.env'); $dotenv->load(); $this->assertSame('bar', getenv('QFOO')); $this->assertSame('baz', getenv('QBAR')); @@ -93,14 +104,14 @@ public function testQuotedDotenvLoadsEnvironmentVars() public function testLargeDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'large.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'large.env'); $dotenv->load(); $this->assertNotEmpty(getenv('LARGE')); } public function testMultipleDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'multiple.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'multiple.env'); $dotenv->load(); $this->assertSame('bar', getenv('MULTI1')); $this->assertSame('foo', getenv('MULTI2')); @@ -108,7 +119,7 @@ public function testMultipleDotenvLoadsEnvironmentVars() public function testExportedDotenvLoadsEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'exported.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'exported.env'); $dotenv->load(); $this->assertSame('bar', getenv('EFOO')); $this->assertSame('baz', getenv('EBAR')); @@ -118,7 +129,7 @@ public function testExportedDotenvLoadsEnvironmentVars() public function testDotenvLoadsEnvGlobals() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->folder); $dotenv->load(); $this->assertSame('bar', $_SERVER['FOO']); $this->assertSame('baz', $_SERVER['BAR']); @@ -128,7 +139,7 @@ public function testDotenvLoadsEnvGlobals() public function testDotenvLoadsServerGlobals() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->folder); $dotenv->load(); $this->assertSame('bar', $_ENV['FOO']); $this->assertSame('baz', $_ENV['BAR']); @@ -138,7 +149,7 @@ public function testDotenvLoadsServerGlobals() public function testDotenvNestedEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder, 'nested.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'nested.env'); $dotenv->load(); $this->assertSame('{$NVAR1} {$NVAR2}', $_ENV['NVAR3']); // not resolved $this->assertSame('Hello World!', $_ENV['NVAR4']); @@ -157,7 +168,7 @@ public function testDotenvNestedEnvironmentVars() public function testDotenvNullFileArgumentUsesDefault() { - $dotenv = Dotenv::create($this->fixturesFolder, null); + $dotenv = Dotenv::createImmutable($this->folder, null); $dotenv->load(); $this->assertSame('bar', getenv('FOO')); } @@ -169,7 +180,7 @@ public function testDotenvNullFileArgumentUsesDefault() */ public function testDotenvTrimmedKeys() { - $dotenv = Dotenv::create($this->fixturesFolder, 'quoted.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'quoted.env'); $dotenv->load(); $this->assertSame('no space', getenv('QWHITESPACE')); } @@ -177,7 +188,7 @@ public function testDotenvTrimmedKeys() public function testDotenvLoadDoesNotOverwriteEnv() { putenv('IMMUTABLE=true'); - $dotenv = Dotenv::create($this->fixturesFolder, 'immutable.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'immutable.env'); $dotenv->load(); $this->assertSame('true', getenv('IMMUTABLE')); } @@ -185,37 +196,29 @@ public function testDotenvLoadDoesNotOverwriteEnv() public function testDotenvLoadAfterOverload() { putenv('IMMUTABLE=true'); - $dotenv = Dotenv::create($this->fixturesFolder, 'immutable.env'); - $dotenv->overload(); - $this->assertSame('false', getenv('IMMUTABLE')); - - putenv('IMMUTABLE=true'); + $dotenv = Dotenv::createMutable($this->folder, 'immutable.env'); $dotenv->load(); - $this->assertSame('true', getenv('IMMUTABLE')); + $this->assertSame('false', getenv('IMMUTABLE')); } public function testDotenvOverloadAfterLoad() { putenv('IMMUTABLE=true'); - $dotenv = Dotenv::create($this->fixturesFolder, 'immutable.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'immutable.env'); $dotenv->load(); $this->assertSame('true', getenv('IMMUTABLE')); - - putenv('IMMUTABLE=true'); - $dotenv->overload(); - $this->assertSame('false', getenv('IMMUTABLE')); } public function testDotenvOverloadDoesOverwriteEnv() { - $dotenv = Dotenv::create($this->fixturesFolder, 'mutable.env'); - $dotenv->overload(); + $dotenv = Dotenv::createMutable($this->folder, 'mutable.env'); + $dotenv->load(); $this->assertSame('true', getenv('MUTABLE')); } public function testDotenvAllowsSpecialCharacters() { - $dotenv = Dotenv::create($this->fixturesFolder, 'specialchars.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'specialchars.env'); $dotenv->load(); $this->assertSame('$a6^C7k%zs+e^.jvjXk', getenv('SPVAR1')); $this->assertSame('?BUty3koaV3%GA*hMAwH}B', getenv('SPVAR2')); @@ -229,7 +232,7 @@ public function testDotenvAllowsSpecialCharacters() public function testMutlilineLoading() { - $dotenv = Dotenv::create($this->fixturesFolder, 'multiline.env'); + $dotenv = Dotenv::createImmutable($this->folder, 'multiline.env'); $dotenv->load(); $this->assertSame("test\n test\"test\"\n test", getenv('TEST')); $this->assertSame("test\ntest", getenv('TEST_ND')); @@ -241,8 +244,8 @@ public function testMutlilineLoading() public function testGetEnvironmentVariablesList() { - $dotenv = Dotenv::create($this->fixturesFolder); - $dotenv->load(); - $this->assertSame(['FOO', 'BAR', 'SPACED', 'NULL'], $dotenv->getEnvironmentVariableNames()); + $dotenv = Dotenv::createImmutable($this->folder); + $names = array_keys($dotenv->load()); + $this->assertSame(['FOO', 'BAR', 'SPACED', 'NULL'], $names); } } diff --git a/tests/Dotenv/EnvironmentVariablesTest.php b/tests/Dotenv/EnvironmentVariablesTest.php deleted file mode 100644 index beba2cb4..00000000 --- a/tests/Dotenv/EnvironmentVariablesTest.php +++ /dev/null @@ -1,150 +0,0 @@ -envFactory = new DotenvFactory(); - (new Loader([dirname(__DIR__).'/fixtures/env/.env'], $this->envFactory))->load(); - } - - public function testCheckingWhetherVariableExists() - { - $envVars = $this->envFactory->create(); - - $this->assertTrue($envVars->has('FOO')); - $this->assertFalse($envVars->has('NON_EXISTING_VARIABLE')); - } - - public function testCheckingHasWithBadType() - { - $envVars = $this->envFactory->create(); - - $this->assertFalse($envVars->has(123)); - $this->assertFalse($envVars->has(null)); - } - - public function testGettingVariableByName() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars->get('FOO')); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Expected name to be a string. - */ - public function testGettingBadVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->get(null); - } - - public function testSettingVariable() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars->get('FOO')); - - $envVars->set('FOO', 'new'); - - $this->assertSame('new', $envVars->get('FOO')); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Expected name to be a string. - */ - public function testSettingBadVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->set(null, 'foo'); - } - - public function testClearingVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->clear('FOO'); - - $this->assertFalse($envVars->has('FOO')); - } - - /** - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Expected name to be a string. - */ - public function testClearingBadVariable() - { - $envVars = $this->envFactory->create(); - - $envVars->clear(null); - } - - public function testCannotSetVariableOnImmutableInstance() - { - $envVars = $this->envFactory->createImmutable(); - - $this->assertSame('bar', $envVars->get('FOO')); - - $envVars->set('FOO', 'new'); - - $this->assertSame('bar', $envVars->get('FOO')); - } - - public function testCannotClearVariableOnImmutableInstance() - { - $envVars = $this->envFactory->createImmutable(); - - $envVars->clear('FOO'); - - $this->assertTrue($envVars->has('FOO')); - } - - public function testCheckingWhetherVariableExistsUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - $this->assertTrue(isset($envVars['FOO'])); - $this->assertFalse(isset($envVars['NON_EXISTING_VARIABLE'])); - } - - public function testGettingVariableByNameUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars['FOO']); - } - - public function testSettingVariableUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - $this->assertSame('bar', $envVars['FOO']); - - $envVars['FOO'] = 'new'; - - $this->assertSame('new', $envVars['FOO']); - } - - public function testClearingVariableUsingArrayNotation() - { - $envVars = $this->envFactory->create(); - - unset($envVars['FOO']); - - $this->assertFalse(isset($envVars['FOO'])); - } -} diff --git a/tests/Dotenv/FactoryTest.php b/tests/Dotenv/FactoryTest.php deleted file mode 100644 index 44bac3bb..00000000 --- a/tests/Dotenv/FactoryTest.php +++ /dev/null @@ -1,44 +0,0 @@ -getProperty('adapters'); - - $prop->setAccessible(true); - - return $prop->getValue($obj); - } - - public function testDefaults() - { - $f = new DotenvFactory(); - - $this->assertInstanceOf('Dotenv\Environment\FactoryInterface', $f); - $this->assertCount(3, self::getAdapters($f->create())); - $this->assertCount(3, self::getAdapters($f->createImmutable())); - } - - public function testSingle() - { - $f = new DotenvFactory([new EnvConstAdapter()]); - - $this->assertInstanceOf('Dotenv\Environment\FactoryInterface', $f); - $this->assertCount(1, self::getAdapters($f->create())); - $this->assertCount(1, self::getAdapters($f->createImmutable())); - } - - public function testNone() - { - $f = new DotenvFactory([]); - - $this->assertInstanceOf('Dotenv\Environment\FactoryInterface', $f); - $this->assertCount(0, self::getAdapters($f->create())); - $this->assertCount(0, self::getAdapters($f->createImmutable())); - } -} diff --git a/tests/Dotenv/LinesTest.php b/tests/Dotenv/LinesTest.php index 5a0fb349..04d69e5b 100644 --- a/tests/Dotenv/LinesTest.php +++ b/tests/Dotenv/LinesTest.php @@ -1,6 +1,6 @@ folder = dirname(__DIR__).'/fixtures/env'; - $this->keyVal(true); - } - - /** - * Generates a new key/value pair or returns the previous one. - * - * Since most of our functionality revolves around setting/retrieving keys - * and values, we have this utility function to help generate new, unique - * key/value pairs. - * - * @param bool $reset - * - * @return array - */ - protected function keyVal($reset = false) - { - if (!isset($this->keyVal) || $reset) { - $this->keyVal = [uniqid() => uniqid()]; - } - - return $this->keyVal; - } - - /** - * Returns the key from keyVal(), without reset. - * - * @return string - */ - protected function key() + public function testLoaderWithNoReaders() { - $keyVal = $this->keyVal(); + $repository = RepositoryBuilder::create()->withReaders([])->make(); + $loader = new Loader(); - return key($keyVal); - } - - /** - * Returns the value from keyVal(), without reset. - * - * @return string - */ - protected function value() - { - $keyVal = $this->keyVal(); - - return reset($keyVal); - } - - public function testMutableLoaderClearsEnvironmentVars() - { - $loader = new Loader(["{$this->folder}/.env"], new DotenvFactory(), false); - - // Set an environment variable. - $loader->setEnvironmentVariable($this->key(), $this->value()); - - // Clear the set environment variable. - $loader->clearEnvironmentVariable($this->key()); - $this->assertSame(null, $loader->getEnvironmentVariable($this->key())); - $this->assertSame(false, getenv($this->key())); - $this->assertSame(false, isset($_ENV[$this->key()])); - $this->assertSame(false, isset($_SERVER[$this->key()])); - $this->assertSame([$this->key()], $loader->getEnvironmentVariableNames()); - } - - public function testImmutableLoaderCannotClearEnvironmentVars() - { - $loader = new Loader(["{$this->folder}/.env"], new DotenvFactory(), false); - - $loader->setImmutable(true); - - // Set an environment variable. - $loader->setEnvironmentVariable($this->key(), $this->value()); + $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; + $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => '${NVAR1} ${NVAR2}']; - // Attempt to clear the environment variable, check that it fails. - $loader->clearEnvironmentVariable($this->key()); - $this->assertSame($this->value(), $loader->getEnvironmentVariable($this->key())); - $this->assertSame($this->value(), getenv($this->key())); - $this->assertSame(true, isset($_ENV[$this->key()])); - $this->assertSame(true, isset($_SERVER[$this->key()])); - $this->assertSame([$this->key()], $loader->getEnvironmentVariableNames()); + $this->assertSame($expected, $loader->load($repository, $content)); } - /** - * @expectedException \Dotenv\Exception\InvalidPathException - * @expectedExceptionMessage At least one environment file path must be provided. - */ - public function testLoaderWithNoPaths() + public function providesAdapters() { - (new Loader([], new DotenvFactory(), false))->load(); + return [ + [null], + [[new ArrayAdapter()]], + [[new EnvConstAdapter()]], + [[new ServerConstAdapter()]], + ]; } /** - * @expectedException \Dotenv\Exception\InvalidPathException - * @expectedExceptionMessage Unable to read any of the environment file(s) at + * @dataProvider providesAdapters */ - public function testLoaderWithBadPaths() - { - (new Loader(["{$this->folder}/BAD1", "{$this->folder}/BAD2"], new DotenvFactory(), false))->load(); - } - - public function testLoaderWithOneGoodPath() - { - $loader = (new Loader(["{$this->folder}/BAD1", "{$this->folder}/.env"], new DotenvFactory(), false)); - - $this->assertCount(4, $loader->load()); - } - - public function testLoaderWithNoAdapters() - { - $loader = (new Loader([], new DotenvFactory([]))); - - $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; - $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => '${NVAR1} ${NVAR2}']; - - $this->assertSame($expected, $loader->loadDirect($content)); - } - - public function testLoaderWithArrayAdapter() + public function testLoaderWithSpecificAdapter($adapters) { - $loader = (new Loader([], new DotenvFactory([new ArrayAdapter()]))); + $repository = RepositoryBuilder::create()->withReaders($adapters)->withWriters($adapters)->make(); + $loader = new Loader(); $content = "NVAR1=\"Hello\"\nNVAR2=\"World!\"\nNVAR3=\"{\$NVAR1} {\$NVAR2}\"\nNVAR4=\"\${NVAR1} \${NVAR2}\""; $expected = ['NVAR1' => 'Hello', 'NVAR2' => 'World!', 'NVAR3' => '{$NVAR1} {$NVAR2}', 'NVAR4' => 'Hello World!']; - $this->assertSame($expected, $loader->loadDirect($content)); + $this->assertSame($expected, $loader->load($repository, $content)); } } diff --git a/tests/Dotenv/ParserTest.php b/tests/Dotenv/ParserTest.php index a0c65ae0..7bae5d8a 100644 --- a/tests/Dotenv/ParserTest.php +++ b/tests/Dotenv/ParserTest.php @@ -1,6 +1,6 @@ assertSame([], $output[1]->getVars()); } + public function testNullParse() + { + $output = Parser::parse('FOO'); + + $this->assertInternalType('array', $output); + $this->assertCount(2, $output); + $this->assertSame('FOO', $output[0]); + $this->assertNull($output[1]); + } + public function testQuotesParse() { $output = Parser::parse("FOO=\"BAR \n\""); @@ -102,6 +112,17 @@ public function testInlineVariable() $this->assertSame([0], $output[1]->getVars()); } + public function testInlineVariableOffset() + { + $output = Parser::parse('FOO=AAA$BAR'); + + $this->assertInternalType('array', $output); + $this->assertCount(2, $output); + $this->assertSame('FOO', $output[0]); + $this->assertSame('AAA$BAR', $output[1]->getChars()); + $this->assertSame([3], $output[1]->getVars()); + } + public function testInlineVariables() { $output = Parser::parse('FOO="TEST $BAR $$BAZ"'); diff --git a/tests/Dotenv/RepositoryTest.php b/tests/Dotenv/RepositoryTest.php new file mode 100644 index 00000000..390346bf --- /dev/null +++ b/tests/Dotenv/RepositoryTest.php @@ -0,0 +1,272 @@ + +folder = dirname(__DIR__).'/fixtures/env'; + $this->keyVal(true); + } + + protected function load() + { + $dotenv = Dotenv::createImmutable($this->folder); + $dotenv->load(); + } + + /** + * Generates a new key/value pair or returns the previous one. + * + * Since most of our functionality revolves around setting/retrieving keys + * and values, we have this utility function to help generate new, unique + * key/value pairs. + * + * @param bool $reset + * + * @return array + */ + protected function keyVal($reset = false) + { + if (!isset($this->keyVal) || $reset) { + $this->keyVal = [uniqid() => uniqid()]; + } + + return $this->keyVal; + } + + /** + * Returns the key from keyVal(), without reset. + * + * @return string + */ + protected function key() + { + $keyVal = $this->keyVal(); + + return key($keyVal); + } + + /** + * Returns the value from keyVal(), without reset. + * + * @return string + */ + protected function value() + { + $keyVal = $this->keyVal(); + + return reset($keyVal); + } + + public function testMutableLoaderClearsEnvironmentVars() + { + $repository = RepositoryBuilder::create()->make(); + + // Set an environment variable. + $repository->set($this->key(), $this->value()); + + // Clear the set environment variable. + $repository->clear($this->key()); + $this->assertSame(null, $repository->get($this->key())); + $this->assertSame(false, getenv($this->key())); + $this->assertSame(false, isset($_ENV[$this->key()])); + $this->assertSame(false, isset($_SERVER[$this->key()])); + } + + public function testImmutableLoaderCannotClearEnvironmentVars() + { + $this->load(); + + $repository = RepositoryBuilder::create()->immutable()->make(); + + // Set an environment variable. + $repository->set($this->key(), $this->value()); + + // Attempt to clear the environment variable, check that it fails. + $repository->clear($this->key()); + $this->assertSame($this->value(), $repository->get($this->key())); + $this->assertSame($this->value(), getenv($this->key())); + $this->assertSame(true, isset($_ENV[$this->key()])); + $this->assertSame(true, isset($_SERVER[$this->key()])); + } + + public function testCheckingWhetherVariableExists() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertTrue($repo->has('FOO')); + $this->assertFalse($repo->has('NON_EXISTING_VARIABLE')); + } + + public function testCheckingHasWithBadType() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertFalse($repo->has(123)); + $this->assertFalse($repo->has(null)); + } + + public function testGettingVariableByName() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertSame('bar', $repo->get('FOO')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Expected name to be a string. + */ + public function testGettingBadVariable() + { + $repo = RepositoryBuilder::create()->make(); + + $repo->get(null); + } + + public function testSettingVariable() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertSame('bar', $repo->get('FOO')); + $repo->set('FOO', 'new'); + $this->assertSame('new', $repo->get('FOO')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Expected name to be a string. + */ + public function testSettingBadVariable() + { + $repo = RepositoryBuilder::create()->make(); + + $repo->set(null, 'foo'); + } + + public function testClearingVariable() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertTrue($repo->has('FOO')); + $repo->clear('FOO'); + $this->assertFalse($repo->has('FOO')); + } + + public function testClearingVariableWithArrayAdapter() + { + $adapters = [new ArrayAdapter()]; + $repo = RepositoryBuilder::create()->withReaders($adapters)->withWriters($adapters)->make(); + + $this->assertFalse($repo->has('FOO')); + $repo->set('FOO', 'BAR'); + $this->assertTrue($repo->has('FOO')); + $repo->clear('FOO'); + $this->assertFalse($repo->has('FOO')); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Expected name to be a string. + */ + public function testClearingBadVariable() + { + $repo = RepositoryBuilder::create()->make(); + + $repo->clear(null); + } + + public function testCannotSetVariableOnImmutableInstance() + { + $this->load(); + + $repo = RepositoryBuilder::create()->immutable()->make(); + + $this->assertSame('bar', $repo->get('FOO')); + + $repo->set('FOO', 'new'); + + $this->assertSame('bar', $repo->get('FOO')); + } + + public function testCannotClearVariableOnImmutableInstance() + { + $this->load(); + + $repo = RepositoryBuilder::create()->immutable()->make(); + + $repo->clear('FOO'); + + $this->assertTrue($repo->has('FOO')); + } + + public function testCheckingWhetherVariableExistsUsingArrayNotation() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertTrue(isset($repo['FOO'])); + $this->assertFalse(isset($repo['NON_EXISTING_VARIABLE'])); + } + + public function testGettingVariableByNameUsingArrayNotation() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertSame('bar', $repo['FOO']); + } + + public function testSettingVariableUsingArrayNotation() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + $this->assertSame('bar', $repo['FOO']); + + $repo['FOO'] = 'new'; + + $this->assertSame('new', $repo['FOO']); + } + + public function testClearingVariableUsingArrayNotation() + { + $this->load(); + + $repo = RepositoryBuilder::create()->make(); + + unset($repo['FOO']); + + $this->assertFalse(isset($repo['FOO'])); + } +} diff --git a/tests/Dotenv/ValidatorTest.php b/tests/Dotenv/ValidatorTest.php index bf0edc01..0cb841d4 100644 --- a/tests/Dotenv/ValidatorTest.php +++ b/tests/Dotenv/ValidatorTest.php @@ -17,7 +17,7 @@ public function setUp() public function testDotenvRequiredStringEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required('FOO'); $this->assertTrue(true); @@ -25,7 +25,7 @@ public function testDotenvRequiredStringEnvironmentVars() public function testDotenvAllowedValues() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required('FOO')->allowedValues(['bar', 'baz']); $this->assertTrue(true); @@ -33,7 +33,7 @@ public function testDotenvAllowedValues() public function testDotenvAllowedValuesIfPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->ifPresent('FOO')->allowedValues(['bar', 'baz']); $this->assertTrue(true); @@ -45,7 +45,7 @@ public function testDotenvAllowedValuesIfPresent() */ public function testDotenvProhibitedValues() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required('FOO')->allowedValues(['buzz', 'buz']); } @@ -56,7 +56,7 @@ public function testDotenvProhibitedValues() */ public function testDotenvProhibitedValuesIfPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->ifPresent('FOO')->allowedValues(['buzz', 'buz']); } @@ -67,7 +67,7 @@ public function testDotenvProhibitedValuesIfPresent() */ public function testDotenvRequiredThrowsRuntimeException() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $this->assertFalse(getenv('FOOX')); $this->assertFalse(getenv('NOPE')); @@ -76,7 +76,7 @@ public function testDotenvRequiredThrowsRuntimeException() public function testDotenvRequiredArrayEnvironmentVars() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required(['FOO', 'BAR']); $this->assertTrue(true); @@ -84,7 +84,7 @@ public function testDotenvRequiredArrayEnvironmentVars() public function testDotenvAssertions() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'assertions.env'); $dotenv->load(); $this->assertSame('val1', getenv('ASSERTVAR1')); $this->assertEmpty(getenv('ASSERTVAR2')); @@ -130,7 +130,7 @@ public function testDotenvAssertions() */ public function testDotenvEmptyThrowsRuntimeException() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'assertions.env'); $dotenv->load(); $this->assertEmpty(getenv('ASSERTVAR2')); @@ -139,7 +139,7 @@ public function testDotenvEmptyThrowsRuntimeException() public function testDotenvEmptyWhenNotPresent() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'assertions.env'); $dotenv->load(); $dotenv->ifPresent('ASSERTVAR2_NO_SUCH_VARIABLE')->notEmpty(); @@ -152,7 +152,7 @@ public function testDotenvEmptyWhenNotPresent() */ public function testDotenvStringOfSpacesConsideredEmpty() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'assertions.env'); $dotenv->load(); $dotenv->required('ASSERTVAR9')->notEmpty(); } @@ -163,14 +163,14 @@ public function testDotenvStringOfSpacesConsideredEmpty() */ public function testDotenvValidateRequiredWithoutLoading() { - $dotenv = Dotenv::create($this->fixturesFolder, 'assertions.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'assertions.env'); $dotenv->required('foo'); } public function testDotenvRequiredCanBeUsedWithoutLoadingFile() { putenv('REQUIRED_VAR=1'); - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->required('REQUIRED_VAR')->notEmpty(); $this->assertTrue(true); } @@ -214,7 +214,7 @@ public function validBooleanValuesDataProvider() */ public function testCanValidateBooleans($boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'booleans.env'); $dotenv->load(); $dotenv->required($boolean)->isBoolean(); @@ -226,7 +226,7 @@ public function testCanValidateBooleans($boolean) */ public function testCanValidateBooleansIfPresent($boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'booleans.env'); $dotenv->load(); $dotenv->ifPresent($boolean)->isBoolean(); @@ -260,7 +260,7 @@ public function invalidBooleanValuesDataProvider() */ public function testCanInvalidateNonBooleans($boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'booleans.env'); $dotenv->load(); $dotenv->required($boolean)->isBoolean(); @@ -273,7 +273,7 @@ public function testCanInvalidateNonBooleans($boolean) */ public function testCanInvalidateNonBooleansIfPresent($boolean) { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'booleans.env'); $dotenv->load(); $dotenv->ifPresent($boolean)->isBoolean(); @@ -285,7 +285,7 @@ public function testCanInvalidateNonBooleansIfPresent($boolean) */ public function testCanInvalidateBooleanNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'booleans.env'); $dotenv->load(); $dotenv->required(['VAR_DOES_NOT_EXIST_234782462764'])->isBoolean(); @@ -293,7 +293,7 @@ public function testCanInvalidateBooleanNonExist() public function testIfPresentBooleanNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'booleans.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'booleans.env'); $dotenv->load(); $dotenv->ifPresent(['VAR_DOES_NOT_EXIST_234782462764'])->isBoolean(); @@ -322,7 +322,7 @@ public function validIntegerValuesDataProvider() */ public function testCanValidateIntegers($integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'integers.env'); $dotenv->load(); $dotenv->required($integer)->isInteger(); @@ -334,7 +334,7 @@ public function testCanValidateIntegers($integer) */ public function testCanValidateIntegersIfPresent($integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'integers.env'); $dotenv->load(); $dotenv->ifPresent($integer)->isInteger(); @@ -369,7 +369,7 @@ public function invalidIntegerValuesDataProvider() */ public function testCanInvalidateNonIntegers($integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'integers.env'); $dotenv->load(); $dotenv->required($integer)->isInteger(); @@ -382,7 +382,7 @@ public function testCanInvalidateNonIntegers($integer) */ public function testCanInvalidateNonIntegersIfExist($integer) { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'integers.env'); $dotenv->load(); $dotenv->ifPresent($integer)->isInteger(); @@ -394,7 +394,7 @@ public function testCanInvalidateNonIntegersIfExist($integer) */ public function testCanInvalidateIntegerNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'integers.env'); $dotenv->load(); $dotenv->required(['VAR_DOES_NOT_EXIST_234782462764'])->isInteger(); @@ -402,7 +402,7 @@ public function testCanInvalidateIntegerNonExist() public function testIfPresentIntegerNonExist() { - $dotenv = Dotenv::create($this->fixturesFolder, 'integers.env'); + $dotenv = Dotenv::createImmutable($this->fixturesFolder, 'integers.env'); $dotenv->load(); $dotenv->ifPresent(['VAR_DOES_NOT_EXIST_234782462764'])->isInteger(); @@ -411,7 +411,7 @@ public function testIfPresentIntegerNonExist() public function testDotenvRegexMatchPass() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required('FOO')->allowedRegexValues('([[:lower:]]{3})'); $this->assertTrue(true); @@ -423,7 +423,7 @@ public function testDotenvRegexMatchPass() */ public function testDotenvRegexMatchFail() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required('FOO')->allowedRegexValues('/^([[:lower:]]{1})$/'); } @@ -434,14 +434,14 @@ public function testDotenvRegexMatchFail() */ public function testDotenvRegexMatchError() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->required('FOO')->allowedRegexValues('/([[:lower:]{1{'); } public function testDotenvRegexMatchNotPresent() { - $dotenv = Dotenv::create($this->fixturesFolder); + $dotenv = Dotenv::createImmutable($this->fixturesFolder); $dotenv->load(); $dotenv->ifPresent('FOOOOOOOOOOO')->allowedRegexValues('([[:lower:]]{3})'); $this->assertTrue(true);