Skip to content

Commit

Permalink
Fixed bugs when using symfony/dotenv on Windows
Browse files Browse the repository at this point in the history
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
  • Loading branch information
code-distortion committed Dec 15, 2023
1 parent efd01a9 commit 48d01dd
Show file tree
Hide file tree
Showing 26 changed files with 1,192 additions and 917 deletions.
778 changes: 282 additions & 496 deletions .github/workflows/run-tests.yml

Large diffs are not rendered by default.

25 changes: 18 additions & 7 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@
"require": {
"php": "7.0.* | 7.1.* | 7.2.* | 7.3.* | 7.4.* | 8.0.* | 8.1.* | 8.2.* | 8.3.*",
"ext-mbstring": "*",
"vlucas/phpdotenv": "^1.0 | ^2.0 | ^3.0 | ^4.0 | ^5.0"
"vlucas/phpdotenv": "^1.1.0 | ^2.0 | ^3.0 | ^4.0 | ^5.0"
},
"require-dev": {
"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",
"jchook/phpunit-assert-throws": "^1.0",
"phpcsstandards/php_codesniffer": "^3.7.2",

This comment has been minimized.

Copy link
@jrfnl

jrfnl Dec 23, 2023

Thank you for your support and your enthousiasm embracing the take-over of the PHP_CodeSniffer package.

In contrast to earlier information, arrangements have been made to allow the package to continue under its original name on Packagist. The commit (in the new repo) to rename the package has been reverted.

I'd recommend reverting the Composer reference changes and keeping the changes which refer to the repo URL on GitHub.

Sorry for the confusion and thank you for understanding. I hope you'll enjoy the 3.8.0 release, which was released two weeks ago.

This comment has been minimized.

Copy link
@code-distortion

code-distortion Dec 26, 2023

Author Owner

Hi jrfnl, thanks for the update. I've released a new version swapping this back

"phpstan/phpstan": "^0.9 | ^0.10 | ^0.11 | ^0.12 | ^1.0",
"phpunit/phpunit": "~4.8 | ^5.0 | ^6.0 | ^7.0 | ^8.0 | ^9.0 | ^10.0",
"squizlabs/php_codesniffer": "^3.5"
"phpunit/phpunit": "~4.8 | ^5.0 | ^6.0 | ^7.0 | ^8.4 | ^9.0 | ^10.0"
},
"autoload": {
"psr-4": {
Expand All @@ -45,14 +46,24 @@
}
},
"scripts": {
"test": "vendor/bin/phpunit",
"phpstan": "vendor/bin/phpstan analyse -c phpstan.neon --level=max",
"phpcs": "vendor/bin/phpcs"
"infection": "vendor/bin/infection --threads=max --show-mutations",
"phpcbf": "vendor/bin/phpcbf",
"phpcs": "vendor/bin/phpcs",
"phpstan": "vendor/bin/phpstan.phar analyse --level=max",
"test": "vendor/bin/phpunit"
},
"scripts-descriptions": {
"infection": "Run Infection tests",
"phpcbf": "Run PHP Code Beautifier and Fixer against your application",
"phpcs": "Run PHP CodeSniffer against your application",
"phpstan": "Run PHPStan static analysis against your application",
"test": "Run PHPUnit tests"
},
"config": {
"sort-packages": true,
"allow-plugins": {
"ocramius/package-versions": true
"ocramius/package-versions": true,
"infection/extension-installer": true
}
}
}
2 changes: 1 addition & 1 deletion phpunit.github-actions.up-to-9.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
backupGlobals="true"
backupStaticAttributes="true"
beStrictAboutOutputDuringTests="true"
beStrictAboutOutputDuringTests="false"
bootstrap="vendor/autoload.php"
colors="false"
convertDeprecationsToExceptions="false"
Expand Down
214 changes: 214 additions & 0 deletions src/DotEnvAdapters/AbstractDotEnvAdapter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,214 @@
<?php

namespace CodeDistortion\FluentDotEnv\DotEnvAdapters;

use CodeDistortion\FluentDotEnv\Exceptions\GeneralException;
use CodeDistortion\FluentDotEnv\Exceptions\InvalidPathException;
use CodeDistortion\FluentDotEnv\Misc\GetenvSupport;
use CodeDistortion\FluentDotEnv\Misc\ValueStore;
use Throwable;

/**
* Abstract adapter to read .env files.
*/
abstract class AbstractDotEnvAdapter implements DotEnvAdapterInterface
{
/** @var array<string, string> The original set of getenv() values. */
private $origGetEnv = [];

/** @var array<string, string> The original set of $_ENV values. */
private $origEnv = [];

/** @var array<string, string> The original set of $_SERVER values. */
private $origServer = [];



/**
* Work out if the import process will update the getenv() values.
*
* If it doesn't then the process of backing up and clearing the getenv() values can be skipped.
*
* @return boolean
*/
abstract protected function importWillUpdateGetenvValues(): bool;

/**
* When going through the process of backing up and clearing the getenv() values, work out if the code should touch
* only the variables defined in the .env file (which requires it to be loaded an extra time beforehand).
*
* PHP 7.0 and below can't get a list of the current env vars using getenv() (with no arguments).
*
* So getting the keys from the .env file allows us to override those values and replace them after without needing
* to know the full list.
*
* @return boolean
*/
protected function shouldOnlyWorkWithVariablesDefinedInEnvFile(): bool
{
// look for PHP 7.0 or below
return (bool) version_compare(PHP_VERSION, '7.1.0', '<');
}



/**
* Load the values from the given .env file, and return them.
*
* NOTE: This MUST leave the getenv(), $_ENV, $_SERVER etc values as they were to begin with.
*
* @param string $path The path to the .env file.
* @return ValueStore
* @throws InvalidPathException When the directory or file could not be used.
* @throws Throwable Rethrows any other Throwable exception.
*/
public function import(string $path): ValueStore
{
try {

$this->recordCurrentEnvValues($path);
$this->removeCurrentEnvValues();
$valueStore = $this->importValuesFromEnvFile($path);

} catch (Throwable $e) {

throw $this->exceptionIsBecauseFileCantBeOpened($e)
? InvalidPathException::invalidPath($path, $e)
: $e;

} finally {

$valueStore = $valueStore ?? new ValueStore();

$keysJustOverridden = array_keys($valueStore->all());
$this->restoreOriginalEnvValues($keysJustOverridden);
}

return $valueStore;
}



/**
* Record the current environment values, to be restored later.
*
* @param string $path The path to the .env file.
* @return void
*/
private function recordCurrentEnvValues(string $path)
{
$this->origEnv = $_ENV;
$this->origServer = $_SERVER;

if (!$this->importWillUpdateGetenvValues()) {
return;
}

$this->origGetEnv = $this->shouldOnlyWorkWithVariablesDefinedInEnvFile()
? $this->resolveCurrentEnvVarsBasedOnKeysDefinedInEnvFile($path)
: GetenvSupport::getAllGetenvVariables();
}

/**
* Generate an array of the current env variables, based on the keys defined in the .env file.
*
* @param string $path The path to the .env file.
* @return array<string, string>
*/
private function resolveCurrentEnvVarsBasedOnKeysDefinedInEnvFile(string $path): array
{
$keys = $this->determineKeysDefinedInEnvFile($path);
return GetenvSupport::getParticularGetenvVariables($keys);
}

/**
* Look into a dotenv file, and find out which keys it defines.
*
* @param string $path The path to the .env file.
* @return string[]
*/
private function determineKeysDefinedInEnvFile(string $path): array
{
$envFileValues = $this->parseEnvFileForValues($path);

return array_keys($envFileValues);
}

/**
* Parse the contents of a .env file for key value pairs.
*
* Used when using PHP 7.0 or below, to determine which keys are in the .env file.
*
* @param string $path The path to the .env file.
* @return array<string, mixed>
*/
protected function parseEnvFileForValues(string $path): array
{
throw GeneralException::pleaseOverrideMethodInChildClass(static::class, __FUNCTION__);
}



/**
* Remove all current environment values.
*
* @return void
*/
private function removeCurrentEnvValues()
{
$_ENV = $_SERVER = [];

if (!$this->importWillUpdateGetenvValues()) {
return;
}

$origGetEnvKeys = array_keys($this->origGetEnv);
GetenvSupport::removeGetenvVariables($origGetEnvKeys);
}



/**
* Read the data from the given .env path.
*
* @param string $path The path to the .env file.
* @return ValueStore
*/
abstract protected function importValuesFromEnvFile(string $path): ValueStore;



/**
* Check if the given exception is because the .env file could not be opened.
*
* @param Throwable $e The exception to check.
* @return boolean
*/
abstract protected function exceptionIsBecauseFileCantBeOpened(Throwable $e): bool;



/**
* Restore the original environment values.
*
* @param string[] $keysJustOverridden The keys that were just overridden.
* @return void
*/
private function restoreOriginalEnvValues(array $keysJustOverridden)
{
$_ENV = $this->origEnv;
$_SERVER = $this->origServer;

if (!$this->importWillUpdateGetenvValues()) {
return;
}

// PHP 7.1 and 7.2 on Windows don't pick up keys with empty values
// so explicitly remove the values here in case any were empty
GetenvSupport::removeGetenvVariables($keysJustOverridden);

$this->shouldOnlyWorkWithVariablesDefinedInEnvFile()
? GetenvSupport::addGetenvVariables($this->origGetEnv)
: GetenvSupport::replaceAllGetenvVariables($this->origGetEnv);
}
}
25 changes: 4 additions & 21 deletions src/DotEnvAdapters/DotEnvAdapterPicker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@

namespace CodeDistortion\FluentDotEnv\DotEnvAdapters;

use CodeDistortion\FluentDotEnv\DotEnvAdapters\Symfony\SymfonyAdapter3;
use CodeDistortion\FluentDotEnv\DotEnvAdapters\Symfony\SymfonyAdapter4Plus;
use CodeDistortion\FluentDotEnv\DotEnvAdapters\Symfony\SymfonyAdapter;
use CodeDistortion\FluentDotEnv\DotEnvAdapters\VLucas\VLucasAdapterV1;
use CodeDistortion\FluentDotEnv\DotEnvAdapters\VLucas\VLucasAdapterV2;
use CodeDistortion\FluentDotEnv\DotEnvAdapters\VLucas\VLucasAdapterV3;
Expand All @@ -15,7 +14,6 @@
use Dotenv\Dotenv as DotenvV4;
use Dotenv\Dotenv as DotenvV5;
use Dotenv\Environment\DotenvFactory as DotenvFactoryV3;
use ReflectionMethod;
use Symfony\Component\Dotenv\Dotenv as SymfonyDotenv;

/**
Expand Down Expand Up @@ -56,7 +54,6 @@ public static function pickAdapter(array $order = ['vlucas', 'symfony']): DotEnv
* Detect the version of vlucas/phpdotenv installed.
*
* @return DotEnvAdapterInterface|null
* @throws DependencyException When the vlucas/phpdotenv package cannot be found.
*/
public static function detectVLucasPhpDotEnv()
{
Expand All @@ -78,25 +75,11 @@ public static function detectVLucasPhpDotEnv()
* Detect the version of symfony/dotenv installed.
*
* @return DotEnvAdapterInterface|null
* @throws DependencyException When the symfony/dotenv package cannot be found.
*/
public static function detectSymfonyDotEnv()
{
if (class_exists(SymfonyDotenv::class)) {

// before version 3.3.7, symfony/dotenv's Dotenv::populate(..) method checked to see if each value is
// present in getenv(..) first before importing it. An extra step needs to be taken to compensate for this.

// to try and detect a change after this so the extra work can be removed, this code looks for the 'void'
// return type in the Dotenv::load(..) method which was added in version 4.0.0
$reflectionMethod = new ReflectionMethod(SymfonyDotenv::class, 'load');
$returnType = $reflectionMethod->getReturnType();
$returnTypeName = !is_null($returnType) ? $returnType->getName() : null;
if ($returnTypeName == 'void') {
return new SymfonyAdapter4Plus();
}
return new SymfonyAdapter3();
}
return null;
return class_exists(SymfonyDotenv::class)
? new SymfonyAdapter()
: null;
}
}
57 changes: 57 additions & 0 deletions src/DotEnvAdapters/DotEnvAdapterTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace CodeDistortion\FluentDotEnv\DotEnvAdapters;

use Throwable;

/**
* Methods sometimes used by the DotEnv adapters.
*/
trait DotEnvAdapterTrait
{
/**
* Pick the directory from a path.
*
* @param string $path The path to the .env file.
* @return string
*/
protected function getDir(string $path): string
{
$temp = explode('/', $path);
array_pop($temp); // remove the filename
return implode('/', $temp);
}

/**
* Pick the filename from a path.
*
* @param string $path The path to the .env file.
* @return string
*/
protected function getFilename(string $path): string
{
$temp = explode('/', $path);
return array_pop($temp);
}

/**
* Get the content of a file in the local filesystem.
*
* Suppresses any errors.
*
* @param string $path The path to the file.
* @return string
*/
private function getFileContent(string $path): string
{
$content = false;
try {
$content = @file_get_contents($path);
} catch (Throwable $e) {
}

return is_string($content)
? $content
: '';
}
}
Loading

0 comments on commit 48d01dd

Please sign in to comment.