Completed reference implementation, added tests

This commit is contained in:
Александр Кирюхин 2018-01-19 03:13:16 +03:00
parent 6b07f5f4eb
commit 6f452ea995
20 changed files with 550 additions and 8 deletions

25
phpunit.xml.dist Normal file
View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false"
syntaxCheck="false"
bootstrap="./tests/bootstrap.php"
>
<testsuites>
<testsuite name="Dotenv tests">
<directory>./tests/</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
</phpunit>

0
readme.md Normal file
View file

80
src/Compiler/Compiler.php Normal file
View file

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
namespace NeonXP\Dotenv\Compiler;
use NeonXP\Dotenv\Exception\RuntimeException;
use NeonXP\Dotenv\Types\KeyValue;
/**
* Class Compiler
* @package NeonXP\Dotenv\Compiler
*/
class Compiler implements CompilerInterface
{
const REGEX_VARIABLE = '/\$\{(.+?)\}/';
/**
* @var KeyValue[]
*/
protected $collection = [];
/**
* @var KeyValue[]
*/
protected $cache = [];
/**
* @inheritdoc
* @param KeyValue[] $collection
*/
public function setRawCollection(array $collection): void
{
$this->collection = [];
$this->cache = [];
foreach ($collection as $keyValue) {
$this->collection[$keyValue->getKey()] = $keyValue;
}
}
/**
* @inheritdoc
* @param KeyValue $keyValue
* @return KeyValue
*/
public function compileKeyValue(KeyValue $keyValue): KeyValue
{
$newValue = preg_replace_callback(self::REGEX_VARIABLE, function ($variable) use ($keyValue) {
$variable = $variable[1];
if ($variable === $keyValue->getKey()) {
throw new RuntimeException('Self referencing');
}
if (isset($this->cache[$variable])) {
return $this->cache[$variable]->getValue();
} elseif (isset($this->collection[$variable]) && !$this->needToCompile($this->collection[$variable])) {
return $this->collection[$variable]->getValue();
} elseif (isset($this->collection[$variable]) && $this->needToCompile($this->collection[$variable])) {
return $this->compileKeyValue($this->collection[$variable])->getValue();
}
return "UNKNOWN VARIABLE {$variable}";
}, $keyValue->getValue());
$result = new KeyValue($keyValue->getKey(), $newValue);
$this->cache[$result->getKey()] = $result;
return $result;
}
/**
* @param KeyValue $keyValue
* @return bool
*/
protected function needToCompile(KeyValue $keyValue): bool
{
return !!preg_match(self::REGEX_VARIABLE, $keyValue->getValue());
}
}

View file

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
/** /**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su> * @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT * @license: MIT
@ -8,16 +10,20 @@ namespace NeonXP\Dotenv\Compiler;
use NeonXP\Dotenv\Types\KeyValue; use NeonXP\Dotenv\Types\KeyValue;
/**
* Interface CompilerInterface
* @package NeonXP\Dotenv\Compiler
*/
interface CompilerInterface interface CompilerInterface
{ {
/** /**
* @param KeyValue[] $collection * @param KeyValue[] $collection
*/ */
function setRawCollection(array $collection): void; public function setRawCollection(array $collection): void;
/** /**
* @param KeyValue $keyValue * @param KeyValue $keyValue
* @return KeyValue * @return KeyValue
*/ */
function compileKeyValue(KeyValue $keyValue): KeyValue; public function compileKeyValue(KeyValue $keyValue): KeyValue;
} }

View file

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
/** /**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su> * @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT * @license: MIT
@ -8,10 +10,16 @@ namespace NeonXP\Dotenv;
use NeonXP\Dotenv\Compiler\CompilerInterface; use NeonXP\Dotenv\Compiler\CompilerInterface;
use NeonXP\Dotenv\Exception\RuntimeException; use NeonXP\Dotenv\Exception\RuntimeException;
use NeonXP\Dotenv\Loader\FileLoader;
use NeonXP\Dotenv\Loader\LoaderInterface; use NeonXP\Dotenv\Loader\LoaderInterface;
use NeonXP\Dotenv\Parser\Parser;
use NeonXP\Dotenv\Parser\ParserInterface; use NeonXP\Dotenv\Parser\ParserInterface;
use NeonXP\Dotenv\Types\KeyValue; use NeonXP\Dotenv\Types\KeyValue;
/**
* Class Dotenv
* @package NeonXP\Dotenv
*/
class Dotenv implements \ArrayAccess, \IteratorAggregate class Dotenv implements \ArrayAccess, \IteratorAggregate
{ {
/** /**
@ -40,6 +48,13 @@ class Dotenv implements \ArrayAccess, \IteratorAggregate
*/ */
public function __construct(LoaderInterface $loader = null, ParserInterface $parser = null, CompilerInterface $compiler = null) public function __construct(LoaderInterface $loader = null, ParserInterface $parser = null, CompilerInterface $compiler = null)
{ {
if (!$loader) {
$loader = new FileLoader(); // Default loader
}
if (!$parser) {
$parser = new Parser(); // Default parser
}
$this->loader = $loader; $this->loader = $loader;
$this->parser = $parser; $this->parser = $parser;
$this->compiler = $compiler; $this->compiler = $compiler;
@ -63,6 +78,10 @@ class Dotenv implements \ArrayAccess, \IteratorAggregate
}, },
[] []
); );
foreach ($this->loadedValues as $key => $value) {
$_ENV[$key] = $value;
putenv($key . "=" . $value);
}
return $this; return $this;
} }
@ -85,7 +104,7 @@ class Dotenv implements \ArrayAccess, \IteratorAggregate
* @return mixed * @return mixed
* @throws RuntimeException * @throws RuntimeException
*/ */
public function get(string $key, mixed $default = null): mixed public function get(string $key, $default = null)
{ {
if (!$this->has($key)) { if (!$this->has($key)) {
return $default; return $default;
@ -122,7 +141,7 @@ class Dotenv implements \ArrayAccess, \IteratorAggregate
* @return bool * @return bool
* @throws RuntimeException * @throws RuntimeException
*/ */
public function offsetExists($offset) public function offsetExists($offset): bool
{ {
return $this->has($offset); return $this->has($offset);
} }

View file

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
/** /**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su> * @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT * @license: MIT
@ -6,7 +8,10 @@
namespace NeonXP\Dotenv\Exception; namespace NeonXP\Dotenv\Exception;
/**
* Class RuntimeException
* @package NeonXP\Dotenv\Exception
*/
class RuntimeException extends \Exception class RuntimeException extends \Exception
{ {

41
src/Loader/FileLoader.php Normal file
View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
namespace NeonXP\Dotenv\Loader;
use NeonXP\Dotenv\Exception\RuntimeException;
/**
* Class FileLoader
* @package NeonXP\Dotenv\Loader
*/
class FileLoader implements LoaderInterface
{
const COMMENT_LINE_REGEX = '/^\s*#/';
/**
* @inheritdoc
* @param string $filePath
* @return array
* @throws RuntimeException
*/
public function load(string $filePath = '.env'): array
{
if (!file_exists($filePath)) {
throw new RuntimeException("There is no {$filePath} file!");
}
$lines = file($filePath);
$lines = array_map('trim', $lines);
$lines = array_filter($lines, function (string $line) {
return trim($line) && !preg_match(self::COMMENT_LINE_REGEX, $line);
});
$lines = array_values($lines);
return $lines;
}
}

View file

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
/** /**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su> * @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT * @license: MIT
@ -6,9 +8,14 @@
namespace NeonXP\Dotenv\Loader; namespace NeonXP\Dotenv\Loader;
/**
* Interface LoaderInterface
* @package NeonXP\Dotenv\Loader
*/
interface LoaderInterface interface LoaderInterface
{ {
/** /**
* Load not empty lines from file or other source
* @param string $filePath * @param string $filePath
* @return string[] * @return string[]
*/ */

55
src/Parser/Parser.php Normal file
View file

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
namespace NeonXP\Dotenv\Parser;
use NeonXP\Dotenv\Types\KeyValue;
/**
* Class Parser
* @package NeonXP\Dotenv\Parser
*/
class Parser implements ParserInterface
{
const REGEX_EXPORT_PREFIX = '/^\s*export\s/i';
// Quotes and comments
const SINGLE_QUOTED_LINE_WITH_COMMENT = '/^\'(.*?)\'\s+#.*?$/i';
const DOUBLE_QUOTED_LINE_WITH_COMMENT = '/^\"(.+?)\"\s+#.*?$/i';
const SINGLE_QUOTED_LINE = '/^\'(.+?)\'$/i';
const DOUBLE_QUOTED_LINE = '/^\"(.*?)\"$/i';
// Types
const BOOLEAN = '/^(true|false)$/i';
const NUMBER = '/^(\d+)$/';
public function parseLine(string $line): KeyValue
{
$line = preg_replace(self::REGEX_EXPORT_PREFIX, '', $line);
list($key, $value) = explode('=', $line, 2) + ['', ''];
$key = trim($key);
$value = trim($value);
$matches = [];
if (
preg_match(self::SINGLE_QUOTED_LINE_WITH_COMMENT, $value, $matches) ||
preg_match(self::DOUBLE_QUOTED_LINE_WITH_COMMENT, $value, $matches) ||
preg_match(self::SINGLE_QUOTED_LINE, $value, $matches) ||
preg_match(self::DOUBLE_QUOTED_LINE, $value, $matches)
) {
$value = $matches[1];
}
if (preg_match(self::BOOLEAN, $value)) {
$value = (strtolower($value) === 'true');
} elseif (preg_match(self::NUMBER, $value)) {
$value = intval($value);
}
return new KeyValue($key, $value);
}
}

View file

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
/** /**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su> * @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT * @license: MIT
@ -8,6 +10,10 @@ namespace NeonXP\Dotenv\Parser;
use NeonXP\Dotenv\Types\KeyValue; use NeonXP\Dotenv\Types\KeyValue;
/**
* Interface ParserInterface
* @package NeonXP\Dotenv\Parser
*/
interface ParserInterface interface ParserInterface
{ {
/** /**

View file

@ -1,4 +1,6 @@
<?php <?php
declare(strict_types=1);
/** /**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su> * @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT * @license: MIT
@ -6,7 +8,10 @@
namespace NeonXP\Dotenv\Types; namespace NeonXP\Dotenv\Types;
/**
* Class KeyValue
* @package NeonXP\Dotenv\Types
*/
class KeyValue class KeyValue
{ {
/** /**
@ -24,7 +29,7 @@ class KeyValue
* @param string $key * @param string $key
* @param mixed $value * @param mixed $value
*/ */
public function __construct(string $key, mixed $value) public function __construct(string $key, $value)
{ {
$this->key = $key; $this->key = $key;
$this->value = $value; $this->value = $value;
@ -41,7 +46,7 @@ class KeyValue
/** /**
* @return mixed * @return mixed
*/ */
public function getValue(): mixed public function getValue()
{ {
return $this->value; return $this->value;
} }

44
tests/CompilerTest.php Normal file
View file

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use NeonXP\Dotenv\Types\KeyValue;
use PHPUnit\Framework\TestCase;
/**
* Class CompilerTest
*/
class CompilerTest extends TestCase
{
public function testParseLines()
{
$collection = [
'KEY1' => 'VALUE1',
'KEY2' => '${KEY1} ${KEY3}',
'KEY3' => 'VALUE3',
'KEY4' => 'Test ${KEY2} => ${KEY3}'
];
$tests = [
'KEY1' => 'VALUE1',
'KEY2' => 'VALUE1 VALUE3',
'KEY3' => 'VALUE3',
'KEY4' => 'Test VALUE1 VALUE3 => VALUE3',
];
$compiler = new \NeonXP\Dotenv\Compiler\Compiler();
$collectionOfKeyValues = [];
foreach ($collection as $key => $value) {
$collectionOfKeyValues[] = new KeyValue($key, $value);
}
$compiler->setRawCollection($collectionOfKeyValues);
foreach ($tests as $key => $expected) {
$result = $compiler->compileKeyValue(new KeyValue($key, $collection[$key]));
$this->assertEquals($key, $result->getKey());
$this->assertEquals($expected, $result->getValue());
}
}
}

68
tests/DotenvTest.php Normal file
View file

@ -0,0 +1,68 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use NeonXP\Dotenv\Compiler\CompilerInterface;
use NeonXP\Dotenv\Dotenv;
use NeonXP\Dotenv\Exception\RuntimeException;
use NeonXP\Dotenv\Loader\LoaderInterface;
use NeonXP\Dotenv\Parser\ParserInterface;
use PHPUnit\Framework\TestCase;
/**
* Class TestDotenv
*/
class DotenvTest extends TestCase
{
/**
* @var LoaderInterface
*/
private $mockLoader;
/**
* @var ParserInterface
*/
private $mockParser;
/**
* @var CompilerInterface
*/
private $mockCompiler;
public function setUp()
{
$this->mockLoader = new MockLoader();
$this->mockParser = new MockParser();
$this->mockCompiler = new MockCompiler();
}
public function testLoad()
{
$dotenv = new Dotenv($this->mockLoader, $this->mockParser, $this->mockCompiler);
try {
$dotenv->get('TEST1');
$this->assertTrue(false, 'Dotenv must throws exception if it not loaded');
} catch (RuntimeException $exception) {
$this->assertTrue(true, 'Dotenv must throws exception if it not loaded');
}
$dotenv->load();
$this->assertNull( $dotenv->get('NOT_EXISTS'));
$this->assertEquals('default value', $dotenv->get('NOT_EXISTS', 'default value'));
$this->assertEquals('VALUE3', $dotenv->get('KEY3', 'default value'));
$this->assertEquals('VALUE3', $dotenv['KEY3']);
$idx = 1;
foreach ($dotenv as $key => $value) {
$this->assertEquals("KEY{$idx}", $key);
$this->assertEquals("VALUE{$idx}", $value);
$idx++;
}
}
}

23
tests/FileLoaderTest.php Normal file
View file

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use PHPUnit\Framework\TestCase;
/**
* Class FileLoaderTest
*/
class FileLoaderTest extends TestCase
{
public function testLoadfile()
{
$loader = new NeonXP\Dotenv\Loader\FileLoader();
$result = $loader->load(__DIR__ . '/misc/.env.test');
$this->assertEquals(['KEY1=VALUE1', 'KEY2=VALUE2'], $result);
}
}

37
tests/ParserTest.php Normal file
View file

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use NeonXP\Dotenv\Parser\Parser;
use PHPUnit\Framework\TestCase;
/**
* Class ParserTest
*/
class ParserTest extends TestCase
{
public function testParseLines()
{
$tests = [
"key1='value1' # comment" => ['key1', 'value1'],
"key2 = 'value2'" => ['key2', 'value2'],
"key3 = \"value3\" # comment" => ['key3', 'value3'],
"key4 =\"value4\"" => ['key4', 'value4'],
"key5 ='value5 # not comment'" => ['key5', 'value5 # not comment'],
"key6 = \"value6 # not comment\"" => ['key6', 'value6 # not comment'],
"boolean=true" => ['boolean', true],
"numeric = 123" => ['numeric', 123]
];
$parser = new Parser();
foreach ($tests as $test => $expected) {
$result = $parser->parseLine($test);
$this->assertEquals($expected[0], $result->getKey());
$this->assertEquals($expected[1], $result->getValue());
}
}
}

20
tests/bootstrap.php Normal file
View file

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
require_once (__DIR__ . "/mocks/MockLoader.php");
require_once (__DIR__ . "/mocks/MockParser.php");
require_once (__DIR__ . "/mocks/MockCompiler.php");
$vendorDir = __DIR__ . '/../../..';
if (file_exists($file = $vendorDir . '/autoload.php')) {
require_once $file;
} else if (file_exists($file = './vendor/autoload.php')) {
require_once $file;
} else {
throw new \RuntimeException("Not found composer autoload");
}

7
tests/misc/.env.test Normal file
View file

@ -0,0 +1,7 @@
# This is comment
# Before was empty line
KEY1=VALUE1
KEY2=VALUE2
#KEY3=VALUE3

View file

@ -0,0 +1,34 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use NeonXP\Dotenv\Compiler\CompilerInterface;
use NeonXP\Dotenv\Types\KeyValue;
/**
* Class MockCompiler
*/
class MockCompiler implements CompilerInterface
{
/**
* @param KeyValue[] $collection
*/
function setRawCollection(array $collection): void
{
// Do nothing
}
/**
* @param KeyValue $keyValue
* @return KeyValue
*/
function compileKeyValue(KeyValue $keyValue): KeyValue
{
return $keyValue;
}
}

View file

@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use NeonXP\Dotenv\Loader\LoaderInterface;
/**
* Class MockLoader
*/
class MockLoader implements LoaderInterface
{
/**
* Load not empty lines from file or other source
* @param string $filePath
* @return string[]
*/
public function load(string $filePath = '.env'): array
{
return [
'KEY1=VALUE1',
'KEY2=VALUE2',
'KEY3=VALUE3',
'KEY4=VALUE4',
'KEY5=VALUE5',
];
}
}

View file

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
/**
* @author: Alexander Kiryukhin <alexander@kiryukhin.su>
* @license: MIT
*/
use NeonXP\Dotenv\Parser\ParserInterface;
use NeonXP\Dotenv\Types\KeyValue;
/**
* Class MockParser
*/
class MockParser implements ParserInterface
{
/**
* @param string $line
* @return KeyValue
*/
public function parseLine(string $line): KeyValue
{
list($key, $value) = explode("=", $line);
return new KeyValue($key, $value);
}
}