first commit

This commit is contained in:
Sampanna Rimal
2024-08-27 17:48:06 +05:45
commit 53c0140f58
10839 changed files with 1125847 additions and 0 deletions

30
vendor/vlucas/phpdotenv/LICENSE vendored Normal file
View File

@ -0,0 +1,30 @@
BSD 3-Clause License
Copyright (c) 2014, Graham Campbell.
Copyright (c) 2013, Vance Lucas.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

60
vendor/vlucas/phpdotenv/composer.json vendored Normal file
View File

@ -0,0 +1,60 @@
{
"name": "vlucas/phpdotenv",
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": ["env", "dotenv", "environment"],
"license": "BSD-3-Clause",
"authors": [
{
"name": "Graham Campbell",
"email": "hello@gjcampbell.co.uk",
"homepage": "https://github.com/GrahamCampbell"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://github.com/vlucas"
}
],
"require": {
"php": "^7.2.5 || ^8.0",
"ext-pcre": "*",
"graham-campbell/result-type": "^1.1.2",
"phpoption/phpoption": "^1.9.2",
"symfony/polyfill-ctype": "^1.24",
"symfony/polyfill-mbstring": "^1.24",
"symfony/polyfill-php80": "^1.24"
},
"require-dev": {
"ext-filter": "*",
"bamarni/composer-bin-plugin": "^1.8.2",
"phpunit/phpunit":"^8.5.34 || ^9.6.13 || ^10.4.2"
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Dotenv\\Tests\\": "tests/Dotenv/"
}
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"config": {
"allow-plugins": {
"bamarni/composer-bin-plugin": true
},
"preferred-install": "dist"
},
"extra": {
"bamarni-bin": {
"bin-links": true,
"forward-command": true
},
"branch-alias": {
"dev-master": "5.6-dev"
}
}
}

267
vendor/vlucas/phpdotenv/src/Dotenv.php vendored Normal file
View File

@ -0,0 +1,267 @@
<?php
declare(strict_types=1);
namespace Dotenv;
use Dotenv\Exception\InvalidPathException;
use Dotenv\Loader\Loader;
use Dotenv\Loader\LoaderInterface;
use Dotenv\Parser\Parser;
use Dotenv\Parser\ParserInterface;
use Dotenv\Repository\Adapter\ArrayAdapter;
use Dotenv\Repository\Adapter\PutenvAdapter;
use Dotenv\Repository\RepositoryBuilder;
use Dotenv\Repository\RepositoryInterface;
use Dotenv\Store\StoreBuilder;
use Dotenv\Store\StoreInterface;
use Dotenv\Store\StringStore;
class Dotenv
{
/**
* The store instance.
*
* @var \Dotenv\Store\StoreInterface
*/
private $store;
/**
* The parser instance.
*
* @var \Dotenv\Parser\ParserInterface
*/
private $parser;
/**
* The loader instance.
*
* @var \Dotenv\Loader\LoaderInterface
*/
private $loader;
/**
* The repository instance.
*
* @var \Dotenv\Repository\RepositoryInterface
*/
private $repository;
/**
* Create a new dotenv instance.
*
* @param \Dotenv\Store\StoreInterface $store
* @param \Dotenv\Parser\ParserInterface $parser
* @param \Dotenv\Loader\LoaderInterface $loader
* @param \Dotenv\Repository\RepositoryInterface $repository
*
* @return void
*/
public function __construct(
StoreInterface $store,
ParserInterface $parser,
LoaderInterface $loader,
RepositoryInterface $repository
) {
$this->store = $store;
$this->parser = $parser;
$this->loader = $loader;
$this->repository = $repository;
}
/**
* Create a new dotenv instance.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function create(RepositoryInterface $repository, $paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{
$builder = $names === null ? StoreBuilder::createWithDefaultName() : StoreBuilder::createWithNoNames();
foreach ((array) $paths as $path) {
$builder = $builder->addPath($path);
}
foreach ((array) $names as $name) {
$builder = $builder->addName($name);
}
if ($shortCircuit) {
$builder = $builder->shortCircuit();
}
return new self($builder->fileEncoding($fileEncoding)->make(), new Parser(), new Loader(), $repository);
}
/**
* Create a new mutable dotenv instance with default repository.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new mutable dotenv instance with default repository with the putenv adapter.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createUnsafeMutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()
->addAdapter(PutenvAdapter::class)
->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new immutable dotenv instance with default repository.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()->immutable()->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new immutable dotenv instance with default repository with the putenv adapter.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createUnsafeImmutable($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithDefaultAdapters()
->addAdapter(PutenvAdapter::class)
->immutable()
->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Create a new dotenv instance with an array backed repository.
*
* @param string|string[] $paths
* @param string|string[]|null $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return \Dotenv\Dotenv
*/
public static function createArrayBacked($paths, $names = null, bool $shortCircuit = true, string $fileEncoding = null)
{
$repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make();
return self::create($repository, $paths, $names, $shortCircuit, $fileEncoding);
}
/**
* Parse the given content and resolve nested variables.
*
* This method behaves just like load(), only without mutating your actual
* environment. We do this by using an array backed repository.
*
* @param string $content
*
* @throws \Dotenv\Exception\InvalidFileException
*
* @return array<string,string|null>
*/
public static function parse(string $content)
{
$repository = RepositoryBuilder::createWithNoAdapters()->addAdapter(ArrayAdapter::class)->make();
$phpdotenv = new self(new StringStore($content), new Parser(), new Loader(), $repository);
return $phpdotenv->load();
}
/**
* Read and load environment file(s).
*
* @throws \Dotenv\Exception\InvalidPathException|\Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException
*
* @return array<string,string|null>
*/
public function load()
{
$entries = $this->parser->parse($this->store->read());
return $this->loader->load($this->repository, $entries);
}
/**
* Read and load environment file(s), silently failing if no files can be read.
*
* @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidFileException
*
* @return array<string,string|null>
*/
public function safeLoad()
{
try {
return $this->load();
} catch (InvalidPathException $e) {
// suppressing exception
return [];
}
}
/**
* Required ensures that the specified variables exist, and returns a new validator object.
*
* @param string|string[] $variables
*
* @return \Dotenv\Validator
*/
public function required($variables)
{
return (new Validator($this->repository, (array) $variables))->required();
}
/**
* Returns a new validator object that won't check if the specified variables exist.
*
* @param string|string[] $variables
*
* @return \Dotenv\Validator
*/
public function ifPresent($variables)
{
return new Validator($this->repository, (array) $variables);
}
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use Throwable;
interface ExceptionInterface extends Throwable
{
//
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use InvalidArgumentException;
final class InvalidEncodingException extends InvalidArgumentException implements ExceptionInterface
{
//
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use InvalidArgumentException;
final class InvalidFileException extends InvalidArgumentException implements ExceptionInterface
{
//
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use InvalidArgumentException;
final class InvalidPathException extends InvalidArgumentException implements ExceptionInterface
{
//
}

View File

@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace Dotenv\Exception;
use RuntimeException;
final class ValidationException extends RuntimeException implements ExceptionInterface
{
//
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Dotenv\Loader;
use Dotenv\Parser\Entry;
use Dotenv\Parser\Value;
use Dotenv\Repository\RepositoryInterface;
final class Loader implements LoaderInterface
{
/**
* Load the given entries into the repository.
*
* We'll substitute any nested variables, and send each variable to the
* repository, with the effect of actually mutating the environment.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param \Dotenv\Parser\Entry[] $entries
*
* @return array<string,string|null>
*/
public function load(RepositoryInterface $repository, array $entries)
{
return \array_reduce($entries, static function (array $vars, Entry $entry) use ($repository) {
$name = $entry->getName();
$value = $entry->getValue()->map(static function (Value $value) use ($repository) {
return Resolver::resolve($repository, $value);
});
if ($value->isDefined()) {
$inner = $value->get();
if ($repository->set($name, $inner)) {
return \array_merge($vars, [$name => $inner]);
}
} else {
if ($repository->clear($name)) {
return \array_merge($vars, [$name => null]);
}
}
return $vars;
}, []);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Dotenv\Loader;
use Dotenv\Repository\RepositoryInterface;
interface LoaderInterface
{
/**
* Load the given entries into the repository.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param \Dotenv\Parser\Entry[] $entries
*
* @return array<string,string|null>
*/
public function load(RepositoryInterface $repository, array $entries);
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Dotenv\Loader;
use Dotenv\Parser\Value;
use Dotenv\Repository\RepositoryInterface;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
use PhpOption\Option;
final class Resolver
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Resolve the nested variables in the given value.
*
* Replaces ${varname} patterns in the allowed positions in the variable
* value by an existing environment variable.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param \Dotenv\Parser\Value $value
*
* @return string
*/
public static function resolve(RepositoryInterface $repository, Value $value)
{
return \array_reduce($value->getVars(), static function (string $s, int $i) use ($repository) {
return Str::substr($s, 0, $i).self::resolveVariable($repository, Str::substr($s, $i));
}, $value->getChars());
}
/**
* Resolve a single nested variable.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param string $str
*
* @return string
*/
private static function resolveVariable(RepositoryInterface $repository, string $str)
{
return Regex::replaceCallback(
'/\A\${([a-zA-Z0-9_.]+)}/',
static function (array $matches) use ($repository) {
return Option::fromValue($repository->get($matches[1]))
->getOrElse($matches[0]);
},
$str,
1
)->success()->getOrElse($str);
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use PhpOption\Option;
final class Entry
{
/**
* The entry name.
*
* @var string
*/
private $name;
/**
* The entry value.
*
* @var \Dotenv\Parser\Value|null
*/
private $value;
/**
* Create a new entry instance.
*
* @param string $name
* @param \Dotenv\Parser\Value|null $value
*
* @return void
*/
public function __construct(string $name, Value $value = null)
{
$this->name = $name;
$this->value = $value;
}
/**
* Get the entry name.
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* Get the entry value.
*
* @return \PhpOption\Option<\Dotenv\Parser\Value>
*/
public function getValue()
{
/** @var \PhpOption\Option<\Dotenv\Parser\Value> */
return Option::fromValue($this->value);
}
}

View File

@ -0,0 +1,300 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
use GrahamCampbell\ResultType\Error;
use GrahamCampbell\ResultType\Result;
use GrahamCampbell\ResultType\Success;
final class EntryParser
{
private const INITIAL_STATE = 0;
private const UNQUOTED_STATE = 1;
private const SINGLE_QUOTED_STATE = 2;
private const DOUBLE_QUOTED_STATE = 3;
private const ESCAPE_SEQUENCE_STATE = 4;
private const WHITESPACE_STATE = 5;
private const COMMENT_STATE = 6;
private const REJECT_STATES = [self::SINGLE_QUOTED_STATE, self::DOUBLE_QUOTED_STATE, self::ESCAPE_SEQUENCE_STATE];
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Parse a raw entry into a proper entry.
*
* That is, turn a raw environment variable entry into a name and possibly
* a value. We wrap the answer in a result type.
*
* @param string $entry
*
* @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry,string>
*/
public static function parse(string $entry)
{
return self::splitStringIntoParts($entry)->flatMap(static function (array $parts) {
[$name, $value] = $parts;
return self::parseName($name)->flatMap(static function (string $name) use ($value) {
/** @var Result<Value|null,string> */
$parsedValue = $value === null ? Success::create(null) : self::parseValue($value);
return $parsedValue->map(static function (?Value $value) use ($name) {
return new Entry($name, $value);
});
});
});
}
/**
* Split the compound string into parts.
*
* @param string $line
*
* @return \GrahamCampbell\ResultType\Result<array{string,string|null},string>
*/
private static function splitStringIntoParts(string $line)
{
/** @var array{string,string|null} */
$result = Str::pos($line, '=')->map(static function () use ($line) {
return \array_map('trim', \explode('=', $line, 2));
})->getOrElse([$line, null]);
if ($result[0] === '') {
/** @var \GrahamCampbell\ResultType\Result<array{string,string|null},string> */
return Error::create(self::getErrorMessage('an unexpected equals', $line));
}
/** @var \GrahamCampbell\ResultType\Result<array{string,string|null},string> */
return Success::create($result);
}
/**
* Parse the given variable name.
*
* That is, strip the optional quotes and leading "export" from the
* variable name. We wrap the answer in a result type.
*
* @param string $name
*
* @return \GrahamCampbell\ResultType\Result<string,string>
*/
private static function parseName(string $name)
{
if (Str::len($name) > 8 && Str::substr($name, 0, 6) === 'export' && \ctype_space(Str::substr($name, 6, 1))) {
$name = \ltrim(Str::substr($name, 6));
}
if (self::isQuotedName($name)) {
$name = Str::substr($name, 1, -1);
}
if (!self::isValidName($name)) {
/** @var \GrahamCampbell\ResultType\Result<string,string> */
return Error::create(self::getErrorMessage('an invalid name', $name));
}
/** @var \GrahamCampbell\ResultType\Result<string,string> */
return Success::create($name);
}
/**
* Is the given variable name quoted?
*
* @param string $name
*
* @return bool
*/
private static function isQuotedName(string $name)
{
if (Str::len($name) < 3) {
return false;
}
$first = Str::substr($name, 0, 1);
$last = Str::substr($name, -1, 1);
return ($first === '"' && $last === '"') || ($first === '\'' && $last === '\'');
}
/**
* Is the given variable name valid?
*
* @param string $name
*
* @return bool
*/
private static function isValidName(string $name)
{
return Regex::matches('~(*UTF8)\A[\p{Ll}\p{Lu}\p{M}\p{N}_.]+\z~', $name)->success()->getOrElse(false);
}
/**
* Parse the given variable value.
*
* This has the effect of stripping quotes and comments, dealing with
* special characters, and locating nested variables, but not resolving
* them. Formally, we run a finite state automaton with an output tape: a
* transducer. We wrap the answer in a result type.
*
* @param string $value
*
* @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string>
*/
private static function parseValue(string $value)
{
if (\trim($value) === '') {
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */
return Success::create(Value::blank());
}
return \array_reduce(\iterator_to_array(Lexer::lex($value)), static function (Result $data, string $token) {
return $data->flatMap(static function (array $data) use ($token) {
return self::processToken($data[1], $token)->map(static function (array $val) use ($data) {
return [$data[0]->append($val[0], $val[1]), $val[2]];
});
});
}, Success::create([Value::blank(), self::INITIAL_STATE]))->flatMap(static function (array $result) {
/** @psalm-suppress DocblockTypeContradiction */
if (in_array($result[1], self::REJECT_STATES, true)) {
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */
return Error::create('a missing closing quote');
}
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Value,string> */
return Success::create($result[0]);
})->mapError(static function (string $err) use ($value) {
return self::getErrorMessage($err, $value);
});
}
/**
* Process the given token.
*
* @param int $state
* @param string $token
*
* @return \GrahamCampbell\ResultType\Result<array{string,bool,int},string>
*/
private static function processToken(int $state, string $token)
{
switch ($state) {
case self::INITIAL_STATE:
if ($token === '\'') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::SINGLE_QUOTED_STATE]);
} elseif ($token === '"') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::DOUBLE_QUOTED_STATE]);
} elseif ($token === '#') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::COMMENT_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, true, self::UNQUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, false, self::UNQUOTED_STATE]);
}
case self::UNQUOTED_STATE:
if ($token === '#') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::COMMENT_STATE]);
} elseif (\ctype_space($token)) {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, true, self::UNQUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, false, self::UNQUOTED_STATE]);
}
case self::SINGLE_QUOTED_STATE:
if ($token === '\'') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, false, self::SINGLE_QUOTED_STATE]);
}
case self::DOUBLE_QUOTED_STATE:
if ($token === '"') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
} elseif ($token === '\\') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::ESCAPE_SEQUENCE_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, true, self::DOUBLE_QUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
}
case self::ESCAPE_SEQUENCE_STATE:
if ($token === '"' || $token === '\\') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
} elseif ($token === '$') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([$token, false, self::DOUBLE_QUOTED_STATE]);
} else {
$first = Str::substr($token, 0, 1);
if (\in_array($first, ['f', 'n', 'r', 't', 'v'], true)) {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create([\stripcslashes('\\'.$first).Str::substr($token, 1), false, self::DOUBLE_QUOTED_STATE]);
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Error::create('an unexpected escape sequence');
}
}
case self::WHITESPACE_STATE:
if ($token === '#') {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::COMMENT_STATE]);
} elseif (!\ctype_space($token)) {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Error::create('unexpected whitespace');
} else {
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::WHITESPACE_STATE]);
}
case self::COMMENT_STATE:
/** @var \GrahamCampbell\ResultType\Result<array{string,bool,int},string> */
return Success::create(['', false, self::COMMENT_STATE]);
default:
throw new \Error('Parser entered invalid state.');
}
}
/**
* Generate a friendly error message.
*
* @param string $cause
* @param string $subject
*
* @return string
*/
private static function getErrorMessage(string $cause, string $subject)
{
return \sprintf(
'Encountered %s at [%s].',
$cause,
\strtok($subject, "\n")
);
}
}

View File

@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
final class Lexer
{
/**
* The regex for each type of token.
*/
private const PATTERNS = [
'[\r\n]{1,1000}', '[^\S\r\n]{1,1000}', '\\\\', '\'', '"', '\\#', '\\$', '([^(\s\\\\\'"\\#\\$)]|\\(|\\)){1,1000}',
];
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Convert content into a token stream.
*
* Multibyte string processing is not needed here, and nether is error
* handling, for performance reasons.
*
* @param string $content
*
* @return \Generator<string>
*/
public static function lex(string $content)
{
static $regex;
if ($regex === null) {
$regex = '(('.\implode(')|(', self::PATTERNS).'))A';
}
$offset = 0;
while (isset($content[$offset])) {
if (!\preg_match($regex, $content, $matches, 0, $offset)) {
throw new \Error(\sprintf('Lexer encountered unexpected character [%s].', $content[$offset]));
}
$offset += \strlen($matches[0]);
yield $matches[0];
}
}
}

View File

@ -0,0 +1,127 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
final class Lines
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Process the array of lines of environment variables.
*
* This will produce an array of raw entries, one per variable.
*
* @param string[] $lines
*
* @return string[]
*/
public static function process(array $lines)
{
$output = [];
$multiline = false;
$multilineBuffer = [];
foreach ($lines as $line) {
[$multiline, $line, $multilineBuffer] = self::multilineProcess($multiline, $line, $multilineBuffer);
if (!$multiline && !self::isCommentOrWhitespace($line)) {
$output[] = $line;
}
}
return $output;
}
/**
* Used to make all multiline variable process.
*
* @param bool $multiline
* @param string $line
* @param string[] $buffer
*
* @return array{bool,string,string[]}
*/
private static function multilineProcess(bool $multiline, string $line, array $buffer)
{
$startsOnCurrentLine = $multiline ? false : self::looksLikeMultilineStart($line);
// check if $line can be multiline variable
if ($startsOnCurrentLine) {
$multiline = true;
}
if ($multiline) {
\array_push($buffer, $line);
if (self::looksLikeMultilineStop($line, $startsOnCurrentLine)) {
$multiline = false;
$line = \implode("\n", $buffer);
$buffer = [];
}
}
return [$multiline, $line, $buffer];
}
/**
* Determine if the given line can be the start of a multiline variable.
*
* @param string $line
*
* @return bool
*/
private static function looksLikeMultilineStart(string $line)
{
return Str::pos($line, '="')->map(static function () use ($line) {
return self::looksLikeMultilineStop($line, true) === false;
})->getOrElse(false);
}
/**
* Determine if the given line can be the start of a multiline variable.
*
* @param string $line
* @param bool $started
*
* @return bool
*/
private static function looksLikeMultilineStop(string $line, bool $started)
{
if ($line === '"') {
return true;
}
return Regex::occurrences('/(?=([^\\\\]"))/', \str_replace('\\\\', '', $line))->map(static function (int $count) use ($started) {
return $started ? $count > 1 : $count >= 1;
})->success()->getOrElse(false);
}
/**
* Determine if the line in the file is a comment or whitespace.
*
* @param string $line
*
* @return bool
*/
private static function isCommentOrWhitespace(string $line)
{
$line = \trim($line);
return $line === '' || (isset($line[0]) && $line[0] === '#');
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Exception\InvalidFileException;
use Dotenv\Util\Regex;
use GrahamCampbell\ResultType\Result;
use GrahamCampbell\ResultType\Success;
final class Parser implements ParserInterface
{
/**
* Parse content into an entry array.
*
* @param string $content
*
* @throws \Dotenv\Exception\InvalidFileException
*
* @return \Dotenv\Parser\Entry[]
*/
public function parse(string $content)
{
return Regex::split("/(\r\n|\n|\r)/", $content)->mapError(static function () {
return 'Could not split into separate lines.';
})->flatMap(static function (array $lines) {
return self::process(Lines::process($lines));
})->mapError(static function (string $error) {
throw new InvalidFileException(\sprintf('Failed to parse dotenv file. %s', $error));
})->success()->get();
}
/**
* Convert the raw entries into proper entries.
*
* @param string[] $entries
*
* @return \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string>
*/
private static function process(array $entries)
{
/** @var \GrahamCampbell\ResultType\Result<\Dotenv\Parser\Entry[],string> */
return \array_reduce($entries, static function (Result $result, string $raw) {
return $result->flatMap(static function (array $entries) use ($raw) {
return EntryParser::parse($raw)->map(static function (Entry $entry) use ($entries) {
/** @var \Dotenv\Parser\Entry[] */
return \array_merge($entries, [$entry]);
});
});
}, Success::create([]));
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
interface ParserInterface
{
/**
* Parse content into an entry array.
*
* @param string $content
*
* @throws \Dotenv\Exception\InvalidFileException
*
* @return \Dotenv\Parser\Entry[]
*/
public function parse(string $content);
}

View File

@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace Dotenv\Parser;
use Dotenv\Util\Str;
final class Value
{
/**
* The string representation of the parsed value.
*
* @var string
*/
private $chars;
/**
* The locations of the variables in the value.
*
* @var int[]
*/
private $vars;
/**
* Internal constructor for a value.
*
* @param string $chars
* @param int[] $vars
*
* @return void
*/
private function __construct(string $chars, array $vars)
{
$this->chars = $chars;
$this->vars = $vars;
}
/**
* Create an empty value instance.
*
* @return \Dotenv\Parser\Value
*/
public static function blank()
{
return new self('', []);
}
/**
* Create a new value instance, appending the characters.
*
* @param string $chars
* @param bool $var
*
* @return \Dotenv\Parser\Value
*/
public function append(string $chars, bool $var)
{
return new self(
$this->chars.$chars,
$var ? \array_merge($this->vars, [Str::len($this->chars)]) : $this->vars
);
}
/**
* Get the string representation of the parsed value.
*
* @return string
*/
public function getChars()
{
return $this->chars;
}
/**
* Get the locations of the variables in the value.
*
* @return int[]
*/
public function getVars()
{
$vars = $this->vars;
\rsort($vars);
return $vars;
}
}

View File

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
interface AdapterInterface extends ReaderInterface, WriterInterface
{
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create();
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\None;
use PhpOption\Option;
use PhpOption\Some;
final class ApacheAdapter implements AdapterInterface
{
/**
* Create a new apache adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
if (self::isSupported()) {
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
return None::create();
}
/**
* Determines if the adapter is supported.
*
* This happens if PHP is running as an Apache module.
*
* @return bool
*/
private static function isSupported()
{
return \function_exists('apache_getenv') && \function_exists('apache_setenv');
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromValue(apache_getenv($name))->filter(static function ($value) {
return \is_string($value) && $value !== '';
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
return apache_setenv($name, $value);
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
return apache_setenv($name, '');
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\Option;
use PhpOption\Some;
final class ArrayAdapter implements AdapterInterface
{
/**
* The variables and their values.
*
* @var array<string,string>
*/
private $variables;
/**
* Create a new array adapter instance.
*
* @return void
*/
private function __construct()
{
$this->variables = [];
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
return Option::fromArraysValue($this->variables, $name);
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
$this->variables[$name] = $value;
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
unset($this->variables[$name]);
return true;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\Option;
use PhpOption\Some;
final class EnvConstAdapter implements AdapterInterface
{
/**
* Create a new env const adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromArraysValue($_ENV, $name)
->filter(static function ($value) {
return \is_scalar($value);
})
->map(static function ($value) {
if ($value === false) {
return 'false';
}
if ($value === true) {
return 'true';
}
/** @psalm-suppress PossiblyInvalidCast */
return (string) $value;
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
$_ENV[$name] = $value;
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
unset($_ENV[$name]);
return true;
}
}

View File

@ -0,0 +1,85 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class GuardedWriter implements WriterInterface
{
/**
* The inner writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* The variable name allow list.
*
* @var string[]
*/
private $allowList;
/**
* Create a new guarded writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
* @param string[] $allowList
*
* @return void
*/
public function __construct(WriterInterface $writer, array $allowList)
{
$this->writer = $writer;
$this->allowList = $allowList;
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
// Don't set non-allowed variables
if (!$this->isAllowed($name)) {
return false;
}
// Set the value on the inner writer
return $this->writer->write($name, $value);
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
// Don't clear non-allowed variables
if (!$this->isAllowed($name)) {
return false;
}
// Set the value on the inner writer
return $this->writer->delete($name);
}
/**
* Determine if the given variable is allowed.
*
* @param non-empty-string $name
*
* @return bool
*/
private function isAllowed(string $name)
{
return \in_array($name, $this->allowList, true);
}
}

View File

@ -0,0 +1,110 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class ImmutableWriter implements WriterInterface
{
/**
* The inner writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* The inner reader to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface
*/
private $reader;
/**
* The record of loaded variables.
*
* @var array<string,string>
*/
private $loaded;
/**
* Create a new immutable writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
* @param \Dotenv\Repository\Adapter\ReaderInterface $reader
*
* @return void
*/
public function __construct(WriterInterface $writer, ReaderInterface $reader)
{
$this->writer = $writer;
$this->reader = $reader;
$this->loaded = [];
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
// Don't overwrite existing environment variables
// Ruby's dotenv does this with `ENV[key] ||= value`
if ($this->isExternallyDefined($name)) {
return false;
}
// Set the value on the inner writer
if (!$this->writer->write($name, $value)) {
return false;
}
// Record that we have loaded the variable
$this->loaded[$name] = '';
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
// Don't clear existing environment variables
if ($this->isExternallyDefined($name)) {
return false;
}
// Clear the value on the inner writer
if (!$this->writer->delete($name)) {
return false;
}
// Leave the variable as fair game
unset($this->loaded[$name]);
return true;
}
/**
* Determine if the given variable is externally defined.
*
* That is, is it an "existing" variable.
*
* @param non-empty-string $name
*
* @return bool
*/
private function isExternallyDefined(string $name)
{
return $this->reader->read($name)->isDefined() && !isset($this->loaded[$name]);
}
}

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\None;
final class MultiReader implements ReaderInterface
{
/**
* The set of readers to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface[]
*/
private $readers;
/**
* Create a new multi-reader instance.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface[] $readers
*
* @return void
*/
public function __construct(array $readers)
{
$this->readers = $readers;
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
foreach ($this->readers as $reader) {
$result = $reader->read($name);
if ($result->isDefined()) {
return $result;
}
}
return None::create();
}
}

View File

@ -0,0 +1,64 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class MultiWriter implements WriterInterface
{
/**
* The set of writers to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface[]
*/
private $writers;
/**
* Create a new multi-writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface[] $writers
*
* @return void
*/
public function __construct(array $writers)
{
$this->writers = $writers;
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
foreach ($this->writers as $writers) {
if (!$writers->write($name, $value)) {
return false;
}
}
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
foreach ($this->writers as $writers) {
if (!$writers->delete($name)) {
return false;
}
}
return true;
}
}

View File

@ -0,0 +1,91 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\None;
use PhpOption\Option;
use PhpOption\Some;
final class PutenvAdapter implements AdapterInterface
{
/**
* Create a new putenv adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
if (self::isSupported()) {
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
return None::create();
}
/**
* Determines if the adapter is supported.
*
* @return bool
*/
private static function isSupported()
{
return \function_exists('getenv') && \function_exists('putenv');
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromValue(\getenv($name), false)->filter(static function ($value) {
return \is_string($value);
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
\putenv("$name=$value");
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
\putenv($name);
return true;
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
interface ReaderInterface
{
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name);
}

View File

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
final class ReplacingWriter implements WriterInterface
{
/**
* The inner writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* The inner reader to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface
*/
private $reader;
/**
* The record of seen variables.
*
* @var array<string,string>
*/
private $seen;
/**
* Create a new replacement writer instance.
*
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
* @param \Dotenv\Repository\Adapter\ReaderInterface $reader
*
* @return void
*/
public function __construct(WriterInterface $writer, ReaderInterface $reader)
{
$this->writer = $writer;
$this->reader = $reader;
$this->seen = [];
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
if ($this->exists($name)) {
return $this->writer->write($name, $value);
}
// succeed if nothing to do
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
if ($this->exists($name)) {
return $this->writer->delete($name);
}
// succeed if nothing to do
return true;
}
/**
* Does the given environment variable exist.
*
* Returns true if it currently exists, or existed at any point in the past
* that we are aware of.
*
* @param non-empty-string $name
*
* @return bool
*/
private function exists(string $name)
{
if (isset($this->seen[$name])) {
return true;
}
if ($this->reader->read($name)->isDefined()) {
$this->seen[$name] = '';
return true;
}
return false;
}
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
use PhpOption\Option;
use PhpOption\Some;
final class ServerConstAdapter implements AdapterInterface
{
/**
* Create a new server const adapter instance.
*
* @return void
*/
private function __construct()
{
//
}
/**
* Create a new instance of the adapter, if it is available.
*
* @return \PhpOption\Option<\Dotenv\Repository\Adapter\AdapterInterface>
*/
public static function create()
{
/** @var \PhpOption\Option<AdapterInterface> */
return Some::create(new self());
}
/**
* Read an environment variable, if it exists.
*
* @param non-empty-string $name
*
* @return \PhpOption\Option<string>
*/
public function read(string $name)
{
/** @var \PhpOption\Option<string> */
return Option::fromArraysValue($_SERVER, $name)
->filter(static function ($value) {
return \is_scalar($value);
})
->map(static function ($value) {
if ($value === false) {
return 'false';
}
if ($value === true) {
return 'true';
}
/** @psalm-suppress PossiblyInvalidCast */
return (string) $value;
});
}
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value)
{
$_SERVER[$name] = $value;
return true;
}
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name)
{
unset($_SERVER[$name]);
return true;
}
}

View File

@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository\Adapter;
interface WriterInterface
{
/**
* Write to an environment variable, if possible.
*
* @param non-empty-string $name
* @param string $value
*
* @return bool
*/
public function write(string $name, string $value);
/**
* Delete an environment variable, if possible.
*
* @param non-empty-string $name
*
* @return bool
*/
public function delete(string $name);
}

View File

@ -0,0 +1,107 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository;
use Dotenv\Repository\Adapter\ReaderInterface;
use Dotenv\Repository\Adapter\WriterInterface;
use InvalidArgumentException;
final class AdapterRepository implements RepositoryInterface
{
/**
* The reader to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface
*/
private $reader;
/**
* The writer to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface
*/
private $writer;
/**
* Create a new adapter repository instance.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface $reader
* @param \Dotenv\Repository\Adapter\WriterInterface $writer
*
* @return void
*/
public function __construct(ReaderInterface $reader, WriterInterface $writer)
{
$this->reader = $reader;
$this->writer = $writer;
}
/**
* Determine if the given environment variable is defined.
*
* @param string $name
*
* @return bool
*/
public function has(string $name)
{
return '' !== $name && $this->reader->read($name)->isDefined();
}
/**
* Get an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return string|null
*/
public function get(string $name)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->reader->read($name)->getOrElse(null);
}
/**
* Set an environment variable.
*
* @param string $name
* @param string $value
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function set(string $name, string $value)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->writer->write($name, $value);
}
/**
* Clear an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function clear(string $name)
{
if ('' === $name) {
throw new InvalidArgumentException('Expected name to be a non-empty string.');
}
return $this->writer->delete($name);
}
}

View File

@ -0,0 +1,272 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository;
use Dotenv\Repository\Adapter\AdapterInterface;
use Dotenv\Repository\Adapter\EnvConstAdapter;
use Dotenv\Repository\Adapter\GuardedWriter;
use Dotenv\Repository\Adapter\ImmutableWriter;
use Dotenv\Repository\Adapter\MultiReader;
use Dotenv\Repository\Adapter\MultiWriter;
use Dotenv\Repository\Adapter\ReaderInterface;
use Dotenv\Repository\Adapter\ServerConstAdapter;
use Dotenv\Repository\Adapter\WriterInterface;
use InvalidArgumentException;
use PhpOption\Some;
use ReflectionClass;
final class RepositoryBuilder
{
/**
* The set of default adapters.
*/
private const DEFAULT_ADAPTERS = [
ServerConstAdapter::class,
EnvConstAdapter::class,
];
/**
* The set of readers to use.
*
* @var \Dotenv\Repository\Adapter\ReaderInterface[]
*/
private $readers;
/**
* The set of writers to use.
*
* @var \Dotenv\Repository\Adapter\WriterInterface[]
*/
private $writers;
/**
* Are we immutable?
*
* @var bool
*/
private $immutable;
/**
* The variable name allow list.
*
* @var string[]|null
*/
private $allowList;
/**
* Create a new repository builder instance.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface[] $readers
* @param \Dotenv\Repository\Adapter\WriterInterface[] $writers
* @param bool $immutable
* @param string[]|null $allowList
*
* @return void
*/
private function __construct(array $readers = [], array $writers = [], bool $immutable = false, array $allowList = null)
{
$this->readers = $readers;
$this->writers = $writers;
$this->immutable = $immutable;
$this->allowList = $allowList;
}
/**
* Create a new repository builder instance with no adapters added.
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public static function createWithNoAdapters()
{
return new self();
}
/**
* Create a new repository builder instance with the default adapters added.
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public static function createWithDefaultAdapters()
{
$adapters = \iterator_to_array(self::defaultAdapters());
return new self($adapters, $adapters);
}
/**
* Return the array of default adapters.
*
* @return \Generator<\Dotenv\Repository\Adapter\AdapterInterface>
*/
private static function defaultAdapters()
{
foreach (self::DEFAULT_ADAPTERS as $adapter) {
$instance = $adapter::create();
if ($instance->isDefined()) {
yield $instance->get();
}
}
}
/**
* Determine if the given name if of an adapterclass.
*
* @param string $name
*
* @return bool
*/
private static function isAnAdapterClass(string $name)
{
if (!\class_exists($name)) {
return false;
}
return (new ReflectionClass($name))->implementsInterface(AdapterInterface::class);
}
/**
* Creates a repository builder with the given reader added.
*
* Accepts either a reader instance, or a class-string for an adapter. If
* the adapter is not supported, then we silently skip adding it.
*
* @param \Dotenv\Repository\Adapter\ReaderInterface|string $reader
*
* @throws \InvalidArgumentException
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function addReader($reader)
{
if (!(\is_string($reader) && self::isAnAdapterClass($reader)) && !($reader instanceof ReaderInterface)) {
throw new InvalidArgumentException(
\sprintf(
'Expected either an instance of %s or a class-string implementing %s',
ReaderInterface::class,
AdapterInterface::class
)
);
}
$optional = Some::create($reader)->flatMap(static function ($reader) {
return \is_string($reader) ? $reader::create() : Some::create($reader);
});
$readers = \array_merge($this->readers, \iterator_to_array($optional));
return new self($readers, $this->writers, $this->immutable, $this->allowList);
}
/**
* Creates a repository builder with the given writer added.
*
* Accepts either a writer instance, or a class-string for an adapter. If
* the adapter is not supported, then we silently skip adding it.
*
* @param \Dotenv\Repository\Adapter\WriterInterface|string $writer
*
* @throws \InvalidArgumentException
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function addWriter($writer)
{
if (!(\is_string($writer) && self::isAnAdapterClass($writer)) && !($writer instanceof WriterInterface)) {
throw new InvalidArgumentException(
\sprintf(
'Expected either an instance of %s or a class-string implementing %s',
WriterInterface::class,
AdapterInterface::class
)
);
}
$optional = Some::create($writer)->flatMap(static function ($writer) {
return \is_string($writer) ? $writer::create() : Some::create($writer);
});
$writers = \array_merge($this->writers, \iterator_to_array($optional));
return new self($this->readers, $writers, $this->immutable, $this->allowList);
}
/**
* Creates a repository builder with the given adapter added.
*
* Accepts either an adapter instance, or a class-string for an adapter. If
* the adapter is not supported, then we silently skip adding it. We will
* add the adapter as both a reader and a writer.
*
* @param \Dotenv\Repository\Adapter\WriterInterface|string $adapter
*
* @throws \InvalidArgumentException
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function addAdapter($adapter)
{
if (!(\is_string($adapter) && self::isAnAdapterClass($adapter)) && !($adapter instanceof AdapterInterface)) {
throw new InvalidArgumentException(
\sprintf(
'Expected either an instance of %s or a class-string implementing %s',
WriterInterface::class,
AdapterInterface::class
)
);
}
$optional = Some::create($adapter)->flatMap(static function ($adapter) {
return \is_string($adapter) ? $adapter::create() : Some::create($adapter);
});
$readers = \array_merge($this->readers, \iterator_to_array($optional));
$writers = \array_merge($this->writers, \iterator_to_array($optional));
return new self($readers, $writers, $this->immutable, $this->allowList);
}
/**
* Creates a repository builder with mutability enabled.
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function immutable()
{
return new self($this->readers, $this->writers, true, $this->allowList);
}
/**
* Creates a repository builder with the given allow list.
*
* @param string[]|null $allowList
*
* @return \Dotenv\Repository\RepositoryBuilder
*/
public function allowList(array $allowList = null)
{
return new self($this->readers, $this->writers, $this->immutable, $allowList);
}
/**
* Creates a new repository instance.
*
* @return \Dotenv\Repository\RepositoryInterface
*/
public function make()
{
$reader = new MultiReader($this->readers);
$writer = new MultiWriter($this->writers);
if ($this->immutable) {
$writer = new ImmutableWriter($writer, $reader);
}
if ($this->allowList !== null) {
$writer = new GuardedWriter($writer, $this->allowList);
}
return new AdapterRepository($reader, $writer);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Dotenv\Repository;
interface RepositoryInterface
{
/**
* Determine if the given environment variable is defined.
*
* @param string $name
*
* @return bool
*/
public function has(string $name);
/**
* Get an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return string|null
*/
public function get(string $name);
/**
* Set an environment variable.
*
* @param string $name
* @param string $value
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function set(string $name, string $value);
/**
* Clear an environment variable.
*
* @param string $name
*
* @throws \InvalidArgumentException
*
* @return bool
*/
public function clear(string $name);
}

View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store\File;
/**
* @internal
*/
final class Paths
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Returns the full paths to the files.
*
* @param string[] $paths
* @param string[] $names
*
* @return string[]
*/
public static function filePaths(array $paths, array $names)
{
$files = [];
foreach ($paths as $path) {
foreach ($names as $name) {
$files[] = \rtrim($path, \DIRECTORY_SEPARATOR).\DIRECTORY_SEPARATOR.$name;
}
}
return $files;
}
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store\File;
use Dotenv\Exception\InvalidEncodingException;
use Dotenv\Util\Str;
use PhpOption\Option;
/**
* @internal
*/
final class Reader
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Read the file(s), and return their raw content.
*
* We provide the file path as the key, and its content as the value. If
* short circuit mode is enabled, then the returned array with have length
* at most one. File paths that couldn't be read are omitted entirely.
*
* @param string[] $filePaths
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @throws \Dotenv\Exception\InvalidEncodingException
*
* @return array<string,string>
*/
public static function read(array $filePaths, bool $shortCircuit = true, string $fileEncoding = null)
{
$output = [];
foreach ($filePaths as $filePath) {
$content = self::readFromFile($filePath, $fileEncoding);
if ($content->isDefined()) {
$output[$filePath] = $content->get();
if ($shortCircuit) {
break;
}
}
}
return $output;
}
/**
* Read the given file.
*
* @param string $path
* @param string|null $encoding
*
* @throws \Dotenv\Exception\InvalidEncodingException
*
* @return \PhpOption\Option<string>
*/
private static function readFromFile(string $path, string $encoding = null)
{
/** @var Option<string> */
$content = Option::fromValue(@\file_get_contents($path), false);
return $content->flatMap(static function (string $content) use ($encoding) {
return Str::utf8($content, $encoding)->mapError(static function (string $error) {
throw new InvalidEncodingException($error);
})->success();
});
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
use Dotenv\Exception\InvalidPathException;
use Dotenv\Store\File\Reader;
final class FileStore implements StoreInterface
{
/**
* The file paths.
*
* @var string[]
*/
private $filePaths;
/**
* Should file loading short circuit?
*
* @var bool
*/
private $shortCircuit;
/**
* The file encoding.
*
* @var string|null
*/
private $fileEncoding;
/**
* Create a new file store instance.
*
* @param string[] $filePaths
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return void
*/
public function __construct(array $filePaths, bool $shortCircuit, string $fileEncoding = null)
{
$this->filePaths = $filePaths;
$this->shortCircuit = $shortCircuit;
$this->fileEncoding = $fileEncoding;
}
/**
* Read the content of the environment file(s).
*
* @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException
*
* @return string
*/
public function read()
{
if ($this->filePaths === []) {
throw new InvalidPathException('At least one environment file path must be provided.');
}
$contents = Reader::read($this->filePaths, $this->shortCircuit, $this->fileEncoding);
if (\count($contents) > 0) {
return \implode("\n", $contents);
}
throw new InvalidPathException(
\sprintf('Unable to read any of the environment file(s) at [%s].', \implode(', ', $this->filePaths))
);
}
}

View File

@ -0,0 +1,141 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
use Dotenv\Store\File\Paths;
final class StoreBuilder
{
/**
* The of default name.
*/
private const DEFAULT_NAME = '.env';
/**
* The paths to search within.
*
* @var string[]
*/
private $paths;
/**
* The file names to search for.
*
* @var string[]
*/
private $names;
/**
* Should file loading short circuit?
*
* @var bool
*/
private $shortCircuit;
/**
* The file encoding.
*
* @var string|null
*/
private $fileEncoding;
/**
* Create a new store builder instance.
*
* @param string[] $paths
* @param string[] $names
* @param bool $shortCircuit
* @param string|null $fileEncoding
*
* @return void
*/
private function __construct(array $paths = [], array $names = [], bool $shortCircuit = false, string $fileEncoding = null)
{
$this->paths = $paths;
$this->names = $names;
$this->shortCircuit = $shortCircuit;
$this->fileEncoding = $fileEncoding;
}
/**
* Create a new store builder instance with no names.
*
* @return \Dotenv\Store\StoreBuilder
*/
public static function createWithNoNames()
{
return new self();
}
/**
* Create a new store builder instance with the default name.
*
* @return \Dotenv\Store\StoreBuilder
*/
public static function createWithDefaultName()
{
return new self([], [self::DEFAULT_NAME]);
}
/**
* Creates a store builder with the given path added.
*
* @param string $path
*
* @return \Dotenv\Store\StoreBuilder
*/
public function addPath(string $path)
{
return new self(\array_merge($this->paths, [$path]), $this->names, $this->shortCircuit, $this->fileEncoding);
}
/**
* Creates a store builder with the given name added.
*
* @param string $name
*
* @return \Dotenv\Store\StoreBuilder
*/
public function addName(string $name)
{
return new self($this->paths, \array_merge($this->names, [$name]), $this->shortCircuit, $this->fileEncoding);
}
/**
* Creates a store builder with short circuit mode enabled.
*
* @return \Dotenv\Store\StoreBuilder
*/
public function shortCircuit()
{
return new self($this->paths, $this->names, true, $this->fileEncoding);
}
/**
* Creates a store builder with the specified file encoding.
*
* @param string|null $fileEncoding
*
* @return \Dotenv\Store\StoreBuilder
*/
public function fileEncoding(string $fileEncoding = null)
{
return new self($this->paths, $this->names, $this->shortCircuit, $fileEncoding);
}
/**
* Creates a new store instance.
*
* @return \Dotenv\Store\StoreInterface
*/
public function make()
{
return new FileStore(
Paths::filePaths($this->paths, $this->names),
$this->shortCircuit,
$this->fileEncoding
);
}
}

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
interface StoreInterface
{
/**
* Read the content of the environment file(s).
*
* @throws \Dotenv\Exception\InvalidEncodingException|\Dotenv\Exception\InvalidPathException
*
* @return string
*/
public function read();
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace Dotenv\Store;
final class StringStore implements StoreInterface
{
/**
* The file content.
*
* @var string
*/
private $content;
/**
* Create a new string store instance.
*
* @param string $content
*
* @return void
*/
public function __construct(string $content)
{
$this->content = $content;
}
/**
* Read the content of the environment file(s).
*
* @return string
*/
public function read()
{
return $this->content;
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Dotenv\Util;
use GrahamCampbell\ResultType\Error;
use GrahamCampbell\ResultType\Success;
/**
* @internal
*/
final class Regex
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Perform a preg match, wrapping up the result.
*
* @param string $pattern
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<bool,string>
*/
public static function matches(string $pattern, string $subject)
{
return self::pregAndWrap(static function (string $subject) use ($pattern) {
return @\preg_match($pattern, $subject) === 1;
}, $subject);
}
/**
* Perform a preg match all, wrapping up the result.
*
* @param string $pattern
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<int,string>
*/
public static function occurrences(string $pattern, string $subject)
{
return self::pregAndWrap(static function (string $subject) use ($pattern) {
return (int) @\preg_match_all($pattern, $subject);
}, $subject);
}
/**
* Perform a preg replace callback, wrapping up the result.
*
* @param string $pattern
* @param callable $callback
* @param string $subject
* @param int|null $limit
*
* @return \GrahamCampbell\ResultType\Result<string,string>
*/
public static function replaceCallback(string $pattern, callable $callback, string $subject, int $limit = null)
{
return self::pregAndWrap(static function (string $subject) use ($pattern, $callback, $limit) {
return (string) @\preg_replace_callback($pattern, $callback, $subject, $limit ?? -1);
}, $subject);
}
/**
* Perform a preg split, wrapping up the result.
*
* @param string $pattern
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<string[],string>
*/
public static function split(string $pattern, string $subject)
{
return self::pregAndWrap(static function (string $subject) use ($pattern) {
/** @var string[] */
return (array) @\preg_split($pattern, $subject);
}, $subject);
}
/**
* Perform a preg operation, wrapping up the result.
*
* @template V
*
* @param callable(string):V $operation
* @param string $subject
*
* @return \GrahamCampbell\ResultType\Result<V,string>
*/
private static function pregAndWrap(callable $operation, string $subject)
{
$result = $operation($subject);
if (\preg_last_error() !== \PREG_NO_ERROR) {
/** @var \GrahamCampbell\ResultType\Result<V,string> */
return Error::create(\preg_last_error_msg());
}
/** @var \GrahamCampbell\ResultType\Result<V,string> */
return Success::create($result);
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Dotenv\Util;
use GrahamCampbell\ResultType\Error;
use GrahamCampbell\ResultType\Success;
use PhpOption\Option;
/**
* @internal
*/
final class Str
{
/**
* This class is a singleton.
*
* @codeCoverageIgnore
*
* @return void
*/
private function __construct()
{
//
}
/**
* Convert a string to UTF-8 from the given encoding.
*
* @param string $input
* @param string|null $encoding
*
* @return \GrahamCampbell\ResultType\Result<string,string>
*/
public static function utf8(string $input, string $encoding = null)
{
if ($encoding !== null && !\in_array($encoding, \mb_list_encodings(), true)) {
/** @var \GrahamCampbell\ResultType\Result<string,string> */
return Error::create(
\sprintf('Illegal character encoding [%s] specified.', $encoding)
);
}
$converted = $encoding === null ?
@\mb_convert_encoding($input, 'UTF-8') :
@\mb_convert_encoding($input, 'UTF-8', $encoding);
/**
* this is for support UTF-8 with BOM encoding
* @see https://en.wikipedia.org/wiki/Byte_order_mark
* @see https://github.com/vlucas/phpdotenv/issues/500
*/
if (\substr($converted, 0, 3) == "\xEF\xBB\xBF") {
$converted = \substr($converted, 3);
}
/** @var \GrahamCampbell\ResultType\Result<string,string> */
return Success::create($converted);
}
/**
* Search for a given substring of the input.
*
* @param string $haystack
* @param string $needle
*
* @return \PhpOption\Option<int>
*/
public static function pos(string $haystack, string $needle)
{
/** @var \PhpOption\Option<int> */
return Option::fromValue(\mb_strpos($haystack, $needle, 0, 'UTF-8'), false);
}
/**
* Grab the specified substring of the input.
*
* @param string $input
* @param int $start
* @param int|null $length
*
* @return string
*/
public static function substr(string $input, int $start, int $length = null)
{
return \mb_substr($input, $start, $length, 'UTF-8');
}
/**
* Compute the length of the given string.
*
* @param string $input
*
* @return int
*/
public static function len(string $input)
{
return \mb_strlen($input, 'UTF-8');
}
}

View File

@ -0,0 +1,209 @@
<?php
declare(strict_types=1);
namespace Dotenv;
use Dotenv\Exception\ValidationException;
use Dotenv\Repository\RepositoryInterface;
use Dotenv\Util\Regex;
use Dotenv\Util\Str;
class Validator
{
/**
* The environment repository instance.
*
* @var \Dotenv\Repository\RepositoryInterface
*/
private $repository;
/**
* The variables to validate.
*
* @var string[]
*/
private $variables;
/**
* Create a new validator instance.
*
* @param \Dotenv\Repository\RepositoryInterface $repository
* @param string[] $variables
*
* @throws \Dotenv\Exception\ValidationException
*
* @return void
*/
public function __construct(RepositoryInterface $repository, array $variables)
{
$this->repository = $repository;
$this->variables = $variables;
}
/**
* Assert that each variable is present.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function required()
{
return $this->assert(
static function (?string $value) {
return $value !== null;
},
'is missing'
);
}
/**
* Assert that each variable is not empty.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function notEmpty()
{
return $this->assertNullable(
static function (string $value) {
return Str::len(\trim($value)) > 0;
},
'is empty'
);
}
/**
* Assert that each specified variable is an integer.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function isInteger()
{
return $this->assertNullable(
static function (string $value) {
return \ctype_digit($value);
},
'is not an integer'
);
}
/**
* Assert that each specified variable is a boolean.
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function isBoolean()
{
return $this->assertNullable(
static function (string $value) {
if ($value === '') {
return false;
}
return \filter_var($value, \FILTER_VALIDATE_BOOLEAN, \FILTER_NULL_ON_FAILURE) !== null;
},
'is not a boolean'
);
}
/**
* Assert that each variable is amongst the given choices.
*
* @param string[] $choices
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function allowedValues(array $choices)
{
return $this->assertNullable(
static function (string $value) use ($choices) {
return \in_array($value, $choices, true);
},
\sprintf('is not one of [%s]', \implode(', ', $choices))
);
}
/**
* Assert that each variable matches the given regular expression.
*
* @param string $regex
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function allowedRegexValues(string $regex)
{
return $this->assertNullable(
static function (string $value) use ($regex) {
return Regex::matches($regex, $value)->success()->getOrElse(false);
},
\sprintf('does not match "%s"', $regex)
);
}
/**
* Assert that the callback returns true for each variable.
*
* @param callable(?string):bool $callback
* @param string $message
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function assert(callable $callback, string $message)
{
$failing = [];
foreach ($this->variables as $variable) {
if ($callback($this->repository->get($variable)) === false) {
$failing[] = \sprintf('%s %s', $variable, $message);
}
}
if (\count($failing) > 0) {
throw new ValidationException(\sprintf(
'One or more environment variables failed assertions: %s.',
\implode(', ', $failing)
));
}
return $this;
}
/**
* Assert that the callback returns true for each variable.
*
* Skip checking null variable values.
*
* @param callable(string):bool $callback
* @param string $message
*
* @throws \Dotenv\Exception\ValidationException
*
* @return \Dotenv\Validator
*/
public function assertNullable(callable $callback, string $message)
{
return $this->assert(
static function (?string $value) use ($callback) {
if ($value === null) {
return true;
}
return $callback($value);
},
$message
);
}
}