Skip to content

Commit 48d01dd

Browse files
Fixed bugs when using symfony/dotenv on Windows
Improved Github Actions test coverage Removed momentary usage of putenv() during the .env load process when using vlucas/phpdotenv Removed momentary usage of putenv() during the .env load process when using symfony/dotenv >= 5.1.0 Improved the Adapter code that uses vlucas/phpdotenv and symfony/dotenv
1 parent efd01a9 commit 48d01dd

26 files changed

+1192
-917
lines changed

.github/workflows/run-tests.yml

Lines changed: 282 additions & 496 deletions
Large diffs are not rendered by default.

composer.json

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,14 @@
2626
"require": {
2727
"php": "7.0.* | 7.1.* | 7.2.* | 7.3.* | 7.4.* | 8.0.* | 8.1.* | 8.2.* | 8.3.*",
2828
"ext-mbstring": "*",
29-
"vlucas/phpdotenv": "^1.0 | ^2.0 | ^3.0 | ^4.0 | ^5.0"
29+
"vlucas/phpdotenv": "^1.1.0 | ^2.0 | ^3.0 | ^4.0 | ^5.0"
3030
},
3131
"require-dev": {
32+
"infection/infection": "^0.10 | ^0.11 | ^0.12 | ^0.13 | ^0.14 | ^0.15 | ^0.16 | ^0.17 | ^0.18 | ^0.19 | ^0.20 | ^0.21 | ^0.22 | ^0.23 | ^0.24 | ^0.25 | ^0.26 | ^0.27",
3233
"jchook/phpunit-assert-throws": "^1.0",
34+
"phpcsstandards/php_codesniffer": "^3.7.2",
3335
"phpstan/phpstan": "^0.9 | ^0.10 | ^0.11 | ^0.12 | ^1.0",
34-
"phpunit/phpunit": "~4.8 | ^5.0 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
35-
"squizlabs/php_codesniffer": "^3.5"
36+
"phpunit/phpunit": "~4.8 | ^5.0 | ^6.0 | ^7.0 | ^8.4 | ^9.0 | ^10.0"
3637
},
3738
"autoload": {
3839
"psr-4": {
@@ -45,14 +46,24 @@
4546
}
4647
},
4748
"scripts": {
48-
"test": "vendor/bin/phpunit",
49-
"phpstan": "vendor/bin/phpstan analyse -c phpstan.neon --level=max",
50-
"phpcs": "vendor/bin/phpcs"
49+
"infection": "vendor/bin/infection --threads=max --show-mutations",
50+
"phpcbf": "vendor/bin/phpcbf",
51+
"phpcs": "vendor/bin/phpcs",
52+
"phpstan": "vendor/bin/phpstan.phar analyse --level=max",
53+
"test": "vendor/bin/phpunit"
54+
},
55+
"scripts-descriptions": {
56+
"infection": "Run Infection tests",
57+
"phpcbf": "Run PHP Code Beautifier and Fixer against your application",
58+
"phpcs": "Run PHP CodeSniffer against your application",
59+
"phpstan": "Run PHPStan static analysis against your application",
60+
"test": "Run PHPUnit tests"
5161
},
5262
"config": {
5363
"sort-packages": true,
5464
"allow-plugins": {
55-
"ocramius/package-versions": true
65+
"ocramius/package-versions": true,
66+
"infection/extension-installer": true
5667
}
5768
}
5869
}

phpunit.github-actions.up-to-9.xml.dist

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
55
backupGlobals="true"
66
backupStaticAttributes="true"
7-
beStrictAboutOutputDuringTests="true"
7+
beStrictAboutOutputDuringTests="false"
88
bootstrap="vendor/autoload.php"
99
colors="false"
1010
convertDeprecationsToExceptions="false"
Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
<?php
2+
3+
namespace CodeDistortion\FluentDotEnv\DotEnvAdapters;
4+
5+
use CodeDistortion\FluentDotEnv\Exceptions\GeneralException;
6+
use CodeDistortion\FluentDotEnv\Exceptions\InvalidPathException;
7+
use CodeDistortion\FluentDotEnv\Misc\GetenvSupport;
8+
use CodeDistortion\FluentDotEnv\Misc\ValueStore;
9+
use Throwable;
10+
11+
/**
12+
* Abstract adapter to read .env files.
13+
*/
14+
abstract class AbstractDotEnvAdapter implements DotEnvAdapterInterface
15+
{
16+
/** @var array<string, string> The original set of getenv() values. */
17+
private $origGetEnv = [];
18+
19+
/** @var array<string, string> The original set of $_ENV values. */
20+
private $origEnv = [];
21+
22+
/** @var array<string, string> The original set of $_SERVER values. */
23+
private $origServer = [];
24+
25+
26+
27+
/**
28+
* Work out if the import process will update the getenv() values.
29+
*
30+
* If it doesn't then the process of backing up and clearing the getenv() values can be skipped.
31+
*
32+
* @return boolean
33+
*/
34+
abstract protected function importWillUpdateGetenvValues(): bool;
35+
36+
/**
37+
* When going through the process of backing up and clearing the getenv() values, work out if the code should touch
38+
* only the variables defined in the .env file (which requires it to be loaded an extra time beforehand).
39+
*
40+
* PHP 7.0 and below can't get a list of the current env vars using getenv() (with no arguments).
41+
*
42+
* So getting the keys from the .env file allows us to override those values and replace them after without needing
43+
* to know the full list.
44+
*
45+
* @return boolean
46+
*/
47+
protected function shouldOnlyWorkWithVariablesDefinedInEnvFile(): bool
48+
{
49+
// look for PHP 7.0 or below
50+
return (bool) version_compare(PHP_VERSION, '7.1.0', '<');
51+
}
52+
53+
54+
55+
/**
56+
* Load the values from the given .env file, and return them.
57+
*
58+
* NOTE: This MUST leave the getenv(), $_ENV, $_SERVER etc values as they were to begin with.
59+
*
60+
* @param string $path The path to the .env file.
61+
* @return ValueStore
62+
* @throws InvalidPathException When the directory or file could not be used.
63+
* @throws Throwable Rethrows any other Throwable exception.
64+
*/
65+
public function import(string $path): ValueStore
66+
{
67+
try {
68+
69+
$this->recordCurrentEnvValues($path);
70+
$this->removeCurrentEnvValues();
71+
$valueStore = $this->importValuesFromEnvFile($path);
72+
73+
} catch (Throwable $e) {
74+
75+
throw $this->exceptionIsBecauseFileCantBeOpened($e)
76+
? InvalidPathException::invalidPath($path, $e)
77+
: $e;
78+
79+
} finally {
80+
81+
$valueStore = $valueStore ?? new ValueStore();
82+
83+
$keysJustOverridden = array_keys($valueStore->all());
84+
$this->restoreOriginalEnvValues($keysJustOverridden);
85+
}
86+
87+
return $valueStore;
88+
}
89+
90+
91+
92+
/**
93+
* Record the current environment values, to be restored later.
94+
*
95+
* @param string $path The path to the .env file.
96+
* @return void
97+
*/
98+
private function recordCurrentEnvValues(string $path)
99+
{
100+
$this->origEnv = $_ENV;
101+
$this->origServer = $_SERVER;
102+
103+
if (!$this->importWillUpdateGetenvValues()) {
104+
return;
105+
}
106+
107+
$this->origGetEnv = $this->shouldOnlyWorkWithVariablesDefinedInEnvFile()
108+
? $this->resolveCurrentEnvVarsBasedOnKeysDefinedInEnvFile($path)
109+
: GetenvSupport::getAllGetenvVariables();
110+
}
111+
112+
/**
113+
* Generate an array of the current env variables, based on the keys defined in the .env file.
114+
*
115+
* @param string $path The path to the .env file.
116+
* @return array<string, string>
117+
*/
118+
private function resolveCurrentEnvVarsBasedOnKeysDefinedInEnvFile(string $path): array
119+
{
120+
$keys = $this->determineKeysDefinedInEnvFile($path);
121+
return GetenvSupport::getParticularGetenvVariables($keys);
122+
}
123+
124+
/**
125+
* Look into a dotenv file, and find out which keys it defines.
126+
*
127+
* @param string $path The path to the .env file.
128+
* @return string[]
129+
*/
130+
private function determineKeysDefinedInEnvFile(string $path): array
131+
{
132+
$envFileValues = $this->parseEnvFileForValues($path);
133+
134+
return array_keys($envFileValues);
135+
}
136+
137+
/**
138+
* Parse the contents of a .env file for key value pairs.
139+
*
140+
* Used when using PHP 7.0 or below, to determine which keys are in the .env file.
141+
*
142+
* @param string $path The path to the .env file.
143+
* @return array<string, mixed>
144+
*/
145+
protected function parseEnvFileForValues(string $path): array
146+
{
147+
throw GeneralException::pleaseOverrideMethodInChildClass(static::class, __FUNCTION__);
148+
}
149+
150+
151+
152+
/**
153+
* Remove all current environment values.
154+
*
155+
* @return void
156+
*/
157+
private function removeCurrentEnvValues()
158+
{
159+
$_ENV = $_SERVER = [];
160+
161+
if (!$this->importWillUpdateGetenvValues()) {
162+
return;
163+
}
164+
165+
$origGetEnvKeys = array_keys($this->origGetEnv);
166+
GetenvSupport::removeGetenvVariables($origGetEnvKeys);
167+
}
168+
169+
170+
171+
/**
172+
* Read the data from the given .env path.
173+
*
174+
* @param string $path The path to the .env file.
175+
* @return ValueStore
176+
*/
177+
abstract protected function importValuesFromEnvFile(string $path): ValueStore;
178+
179+
180+
181+
/**
182+
* Check if the given exception is because the .env file could not be opened.
183+
*
184+
* @param Throwable $e The exception to check.
185+
* @return boolean
186+
*/
187+
abstract protected function exceptionIsBecauseFileCantBeOpened(Throwable $e): bool;
188+
189+
190+
191+
/**
192+
* Restore the original environment values.
193+
*
194+
* @param string[] $keysJustOverridden The keys that were just overridden.
195+
* @return void
196+
*/
197+
private function restoreOriginalEnvValues(array $keysJustOverridden)
198+
{
199+
$_ENV = $this->origEnv;
200+
$_SERVER = $this->origServer;
201+
202+
if (!$this->importWillUpdateGetenvValues()) {
203+
return;
204+
}
205+
206+
// PHP 7.1 and 7.2 on Windows don't pick up keys with empty values
207+
// so explicitly remove the values here in case any were empty
208+
GetenvSupport::removeGetenvVariables($keysJustOverridden);
209+
210+
$this->shouldOnlyWorkWithVariablesDefinedInEnvFile()
211+
? GetenvSupport::addGetenvVariables($this->origGetEnv)
212+
: GetenvSupport::replaceAllGetenvVariables($this->origGetEnv);
213+
}
214+
}

src/DotEnvAdapters/DotEnvAdapterPicker.php

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,7 @@
22

33
namespace CodeDistortion\FluentDotEnv\DotEnvAdapters;
44

5-
use CodeDistortion\FluentDotEnv\DotEnvAdapters\Symfony\SymfonyAdapter3;
6-
use CodeDistortion\FluentDotEnv\DotEnvAdapters\Symfony\SymfonyAdapter4Plus;
5+
use CodeDistortion\FluentDotEnv\DotEnvAdapters\Symfony\SymfonyAdapter;
76
use CodeDistortion\FluentDotEnv\DotEnvAdapters\VLucas\VLucasAdapterV1;
87
use CodeDistortion\FluentDotEnv\DotEnvAdapters\VLucas\VLucasAdapterV2;
98
use CodeDistortion\FluentDotEnv\DotEnvAdapters\VLucas\VLucasAdapterV3;
@@ -15,7 +14,6 @@
1514
use Dotenv\Dotenv as DotenvV4;
1615
use Dotenv\Dotenv as DotenvV5;
1716
use Dotenv\Environment\DotenvFactory as DotenvFactoryV3;
18-
use ReflectionMethod;
1917
use Symfony\Component\Dotenv\Dotenv as SymfonyDotenv;
2018

2119
/**
@@ -56,7 +54,6 @@ public static function pickAdapter(array $order = ['vlucas', 'symfony']): DotEnv
5654
* Detect the version of vlucas/phpdotenv installed.
5755
*
5856
* @return DotEnvAdapterInterface|null
59-
* @throws DependencyException When the vlucas/phpdotenv package cannot be found.
6057
*/
6158
public static function detectVLucasPhpDotEnv()
6259
{
@@ -78,25 +75,11 @@ public static function detectVLucasPhpDotEnv()
7875
* Detect the version of symfony/dotenv installed.
7976
*
8077
* @return DotEnvAdapterInterface|null
81-
* @throws DependencyException When the symfony/dotenv package cannot be found.
8278
*/
8379
public static function detectSymfonyDotEnv()
8480
{
85-
if (class_exists(SymfonyDotenv::class)) {
86-
87-
// before version 3.3.7, symfony/dotenv's Dotenv::populate(..) method checked to see if each value is
88-
// present in getenv(..) first before importing it. An extra step needs to be taken to compensate for this.
89-
90-
// to try and detect a change after this so the extra work can be removed, this code looks for the 'void'
91-
// return type in the Dotenv::load(..) method which was added in version 4.0.0
92-
$reflectionMethod = new ReflectionMethod(SymfonyDotenv::class, 'load');
93-
$returnType = $reflectionMethod->getReturnType();
94-
$returnTypeName = !is_null($returnType) ? $returnType->getName() : null;
95-
if ($returnTypeName == 'void') {
96-
return new SymfonyAdapter4Plus();
97-
}
98-
return new SymfonyAdapter3();
99-
}
100-
return null;
81+
return class_exists(SymfonyDotenv::class)
82+
? new SymfonyAdapter()
83+
: null;
10184
}
10285
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
namespace CodeDistortion\FluentDotEnv\DotEnvAdapters;
4+
5+
use Throwable;
6+
7+
/**
8+
* Methods sometimes used by the DotEnv adapters.
9+
*/
10+
trait DotEnvAdapterTrait
11+
{
12+
/**
13+
* Pick the directory from a path.
14+
*
15+
* @param string $path The path to the .env file.
16+
* @return string
17+
*/
18+
protected function getDir(string $path): string
19+
{
20+
$temp = explode('/', $path);
21+
array_pop($temp); // remove the filename
22+
return implode('/', $temp);
23+
}
24+
25+
/**
26+
* Pick the filename from a path.
27+
*
28+
* @param string $path The path to the .env file.
29+
* @return string
30+
*/
31+
protected function getFilename(string $path): string
32+
{
33+
$temp = explode('/', $path);
34+
return array_pop($temp);
35+
}
36+
37+
/**
38+
* Get the content of a file in the local filesystem.
39+
*
40+
* Suppresses any errors.
41+
*
42+
* @param string $path The path to the file.
43+
* @return string
44+
*/
45+
private function getFileContent(string $path): string
46+
{
47+
$content = false;
48+
try {
49+
$content = @file_get_contents($path);
50+
} catch (Throwable $e) {
51+
}
52+
53+
return is_string($content)
54+
? $content
55+
: '';
56+
}
57+
}

0 commit comments

Comments
 (0)