Commit bd4ba0a2 authored by Avris's avatar Avris

init

parents
tests/_output/*
vendor/*
.idea/*
**/.DS_Store
## Avris Dotenv ##
### Instalation
composer require avris/dotenv
### .env
Environmental variables are a great way to adjust your application to a specific environment,
especially when it comes to sensitive data, like database password.
Those variables should be set by your HTTP server,
but for local development having a `.env` file is way simpler:
APP_ENV=prod
APP_DEBUG=0
DB_HOST=127.0.0.1
DB_DATABASE=prod
DB_USER=user
DB_PASS=pass
All you need to do in your front controller is load this file using Dotenv:
(new \Avris\Dotenv\Dotenv)->load(__DIR__ . '/../.env');
Now all the variables from the file will be available via `getenv`, `$_ENV` and `$_SERVER`.
### Advanced
If your variable value contains a space, remember to quote it:
FOO="Lorem ipsum"
You can add a comment if you start a line with a `#`:
# comment
FOO=bar
You can use vars inside other vars (escape `$` with `\$`):
VAR1=osiem
VAR2="${VAR1}naście ${CZEGO}"
VAR3=\$ESC
You can also use output of a command (`symfony/process` is required, escape `$` with `\$`):
COMM1=$(whoami)
COMM2=\$(whoami)
Remember .env is a valid bash script.
### Other features
You can just read the variables from a file without populating it as environment variables:
$vars = (new \Avris\Dotenv\Dotenv)->read(__DIR__ . '/../.env');
Or just populate vars into environment without parsing a file:
(new \Avris\Dotenv\Dotenv)->populate([
'FOO' => 'foo',
'BAR' => 'bar',
]);
You can also rever the process and dump vars into a .env file:
(new \Avris\Dotenv\Dotenv)->save(__DIR__ . '/.env', [
'FOO' => 'foo',
'BAR' => 'bar',
]);
Or without saving to a file:
$string = (new \Avris\Dotenv\Dotenv)->dump([
'FOO' => 'foo',
'BAR' => 'bar',
]);
### Fill command
Dotenv provides a simple way to fill your `.env` file with values based on user input and defaults.
Let's say you have an application with two modules that require some environment variables: `Foo\Database` and `Foo\Mailer`.
Install `symfony/console` and create a command that extends `Avris\Dotenv\Command\FillCommand`, providing all the defaults:
final class FillCommandImplementation extends \Avris\Dotenv\Command\FillCommand
{
protected function getDefaults(): iterable
{
yield `Foo\Database` => [
'DB_HOST' => '127.0.0.1',
'DB_DATABASE' => 'foo',
'DB_USER' => 'root',
'DB_PASS' => '',
];
yield `Foo\Mailer` => [
'MAILER_URL' => 'null://localhost',
];
}
}
Let's say your `.env` file looks like this:
###> Foo\Database ###
DB_HOST=127.0.0.1
DB_DATABASE=prod
DB_USER=user
###< Foo\Database ###
FOO=bar
Both `DB_PASS` and `MAILER_URL` are missing and an additional variable `FOO` is defined.
If you now [run the command](https://symfony.com/doc/current/components/console.html),
you will be asked what should `DB_PASS` and `MAILER_URL` be (defaults `` and `null://localhost` are offered),
and the values you submit will be put in the `.env` file without removing `FOO`.
### Copyright ###
* **Author:** Andre Prusinowski [(Avris.it)](https://avris.it)
* **Licence:** [MIT](https://mit.avris.it)
{
"name": "avris/dotenv",
"type": "library",
"description": ".env file handler",
"keywords": ["env","dotenv","enviromental variable"],
"license": "MIT",
"homepage": "https://micrus.avris.it",
"authors": [{
"name": "Avris",
"email": "andre@avris.it",
"homepage": "https://avris.it"
}],
"require": {
"php": "^7.1"
},
"require-dev": {
"phpunit/phpunit": "^6.5",
"symfony/var-dumper": "^4.0",
"symfony/process": "^4.0",
"symfony/console": "^4.0"
},
"suggests": {
"symfony/process": "In order to use FOO=$(bar)",
"symfony/console": "In order to use the FillCommand"
},
"autoload": {
"psr-4": { "Avris\\Dotenv\\": "src" }
},
"autoload-dev": {
"psr-4": { "Avris\\Dotenv\\": "tests" }
}
}
This diff is collapsed.
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://schema.phpunit.de/4.8/phpunit.xsd"
backupGlobals="false"
colors="true"
bootstrap="tests/_bootstrap.php">
<testsuites>
<testsuite name="main">
<directory>tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist addUncoveredFilesFromWhitelist="true">
<directory suffix=".php">src</directory>
</whitelist>
</filter>
<logging>
<log type="coverage-html" target="tests/_output/coverage"/>
<log type="coverage-clover" target="tests/_output/coverage.xml"/>
<log type="coverage-php" target="tests/_output/coverage.serialized"/>
<log type="coverage-text" target="php://stdout" showOnlySummary="true"/>
</logging>
</phpunit>
<?php
namespace Avris\Dotenv\Command;
use Avris\Dotenv\Dotenv;
use Avris\Dotenv\Line\QuestionLine;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\Question;
abstract class FillCommand extends Command
{
protected function configure()
{
$this
->setName('dotenv:fill')
->setDescription('Creates a .env file based on user input and provided defaults')
->addArgument('file', null, '.env file to fill with data', '.env')
;
}
protected function execute(InputInterface $input, OutputInterface $output)
{
$dotenv = new Dotenv();
$filename = $input->getArgument('file');
$newLines = [];
foreach ($dotenv->fill($filename, $this->getDefaults()) as $line) {
if ($line instanceof QuestionLine) {
$line->answer($this->answerQuestion($line, $input, $output));
}
$newLines[] = $line;
}
$dotenv->save(
$filename, // @codeCoverageIgnore
$this->separateSections(implode('', $newLines))
);
}
abstract protected function getDefaults(): iterable;
private function answerQuestion(QuestionLine $line, InputInterface $input, OutputInterface $output)
{
switch ($default = $line->getDefault()) {
case '%generate(secret)%':
return bin2hex(random_bytes(32));
default:
return $this->getHelper('question')->ask(
$input, // @codeCoverageIgnore
$output, // @codeCoverageIgnore
new Question(sprintf('%s (<info>%s</info>): ', $line->getName(), $default), $default)
);
}
}
private function separateSections(string $string)
{
return preg_replace('/ ###\s*###>/ ', " ###\n\n###>", $string);
}
}
<?php
namespace Avris\Dotenv;
use Avris\Dotenv\Service as Service;
final class Dotenv
{
/** @var Service\Parser */
private $parser;
/** @var Service\Populator */
private $populator;
/** @var Service\Filler */
private $filler;
/** @var Service\Dumper */
private $dumper;
/** @var Service\Filesystem */
private $filesystem;
public function __construct()
{
$this->parser = new Service\Parser;
$this->populator = new Service\Populator;
$this->filler = new Service\Filler;
$this->dumper = new Service\Dumper;
$this->filesystem = new Service\Filesystem;
}
public function load(string $filename): array
{
return $this->populator->populate($this->read($filename));
}
public function read(string $filename): array
{
return $this->parse($this->filesystem->readFile($filename));
}
public function parse(string $string): array
{
return $this->parser->parse($string);
}
public function populate(iterable $vars)
{
return $this->populator->populate($vars);
}
public function fill(string $filename, iterable $expected): iterable
{
return $this->filler->fill(
$this->parser->getLines($this->filesystem->readFile($filename)),
$expected
);
}
public function dump(iterable $vars): string
{
return $this->dumper->dump($vars);
}
public function save(string $filename, $vars): string
{
return $this->filesystem->saveFile(
$filename, // @codeCoverageIgnore
is_iterable($vars) ? $this->dump($vars) : $vars
);
}
}
<?php
namespace Avris\Dotenv\Exception;
class FilesystemException extends \Exception
{
}
<?php
namespace Avris\Dotenv\Exception;
use Avris\Dotenv\Line\Line;
class ParseException extends \Exception
{
public function __construct(
Line $line,
string $message = 'Parsing error',
int $code = 0,
\Throwable $previous = null
)
{
parent::__construct(sprintf('%s in line %s', $message, $line->getNumber()), $code, $previous);
}
}
<?php
namespace Avris\Dotenv\Line;
final class IgnoredLine extends Line
{
/** @var string */
private $content;
public function __construct(string $content, int $number = -1)
{
$this->content = $content;
parent::__construct($number);
}
public function getContent(): string
{
return $this->content;
}
}
<?php
namespace Avris\Dotenv\Line;
final class InvalidLine extends Line
{
/** @var string */
private $content;
public function __construct(string $content, int $number = -1)
{
$this->content = $content;
parent::__construct($number);
}
public function getContent(): string
{
return $this->content;
}
}
<?php
namespace Avris\Dotenv\Line;
abstract class Line
{
/** @var int */
private $number;
public function __construct(int $number = -1)
{
$this->number = $number;
}
public function getNumber(): int
{
return $this->number;
}
abstract public function getContent(): string;
public function __toString(): string
{
return $this->getContent() . "\n";
}
}
<?php
namespace Avris\Dotenv\Line;
final class QuestionLine extends Line
{
/** @var string */
private $name;
/** @var string */
private $default;
/** @var ?string */
private $value;
public function __construct(
string $name,
string $default
) {
parent::__construct(-1);
$this->name = $name;
$this->default = $default;
}
public function getContent(): string
{
return sprintf(
'%s=%s',
$this->getName(),
strpos($this->getValue(), ' ') === false
? $this->getValue()
: '"' . str_replace('"', '\"', $this->getValue()) . '"'
);
}
public function getName(): string
{
return $this->name;
}
public function getDefault(): string
{
return $this->default;
}
public function getValue(): ?string
{
return $this->value;
}
public function answer(string $value): self
{
$this->value = $value;
return $this;
}
}
<?php
namespace Avris\Dotenv\Line;
final class SectionEndLine extends Line
{
/** @var string */
private $section;
public function __construct(string $section, int $number = -1)
{
$this->section = $section;
parent::__construct($number);
}
public function getContent(): string
{
return sprintf('###< %s ###', $this->getSection());
}
public function getSection(): string
{
return $this->section;
}
}
<?php
namespace Avris\Dotenv\Line;
final class SectionStartLine extends Line
{
/** @var string */
private $section;
public function __construct(string $section, int $number = -1)
{
$this->section = $section;
parent::__construct($number);
}
public function getContent(): string
{
return sprintf('###> %s ###', $this->getSection());
}
public function getSection(): string
{
return $this->section;
}
}
<?php
namespace Avris\Dotenv\Line;
final class VarLine extends Line
{
/** @var string */
private $name;
/** @var string */
private $value;
public function __construct(string $name, string $value, int $number = -1)
{
$this->name = $name;
$this->value = $value;
parent::__construct($number);
}
public function getContent(): string
{
return sprintf(
'%s=%s',
$this->getName(),
strpos($this->getValue(), ' ') === false
? $this->getValue()
: '"' . str_replace('"', '\"', $this->getValue()) . '"'
);
}
public function getName(): string
{
return $this->name;
}
public function getValue(): string
{
return $this->value;
}
}
<?php
namespace Avris\Dotenv\Service;
use Avris\Dotenv\Line\IgnoredLine;
use Avris\Dotenv\Line\SectionEndLine;
use Avris\Dotenv\Line\SectionStartLine;
use Avris\Dotenv\Line\VarLine;
final class Dumper
{
public function dump(iterable $vars): string
{
return implode('', iterator_to_array($this->buildLines($vars)));
}
public function buildLines(iterable $vars): iterable
{
foreach ($vars as $name => $value) {
if (is_iterable($value)) {
yield new SectionStartLine($name);
foreach ($value as $varName => $varValue) {
yield new VarLine($varName, $varValue);
}
yield new SectionEndLine($name);
yield new IgnoredLine('');
} else {
yield new VarLine($name, $value);
}
}
}
}
<?php
namespace Avris\Dotenv\Service;
use Avris\Dotenv\Exception\FilesystemException;
final class Filesystem
{
public function readFile(string $filename): string
{
if (!file_exists($filename)) {
return '';
}
$content = file_get_contents($filename);
if ($content === false) {
throw new FilesystemException(sprintf('Cannot read from file %s', $filename));
}
return $content;
}
public function saveFile(string $filename, string $content): string
{
$dir = dirname($filename);
if (!file_exists($dir)) {
mkdir($dir, 0777, true);
}
if (!is_dir($dir)) {
throw new FilesystemException(sprintf('Cannot write to file %s: %s is not a directory', $filename, $dir));
}
if (file_put_contents($filename, $content) === false) {
throw new FilesystemException(sprintf('Cannot write to file %s', $filename));
}
return $content;
}
}
<?php
namespace Avris\Dotenv\Service;
use Avris\Dotenv\Line\IgnoredLine;
use Avris\Dotenv\Line\QuestionLine;
use Avris\Dotenv\Line\SectionEndLine;
use Avris\Dotenv\Line\SectionStartLine;
use Avris\Dotenv\Line\VarLine;
final class Filler
{
public function fill(iterable $lines, iterable $expected): iterable
{
$section = '';
$expected = iterator_to_array($expected);
foreach ($lines as $line) {
if ($line instanceof IgnoredLine) {
yield $line;
} elseif ($line instanceof VarLine) {
yield $line;
unset($expected[$section][$line->getName()]);
} elseif ($line instanceof SectionStartLine) {
$section = $line->getSection();
yield $line;
} elseif ($line instanceof SectionEndLine) {
foreach ($expected[$section] ?? [] as $name => $default) {
yield new QuestionLine($name, $default);
unset($expected[$section][$name]);
}
yield $line;
unset($expected[$section]);
$section = '';
}
}
foreach ($expected as $section => $vars) {
if (!is_iterable($vars)) {
throw new \InvalidArgumentException('Invalid format of the $expected array');
}
if ($section !== '') {
yield new SectionStartLine($section);
}
foreach ($vars as $name => $default) {
yield new QuestionLine($name, $default);
unset($expected[$section][$name]);
}
if ($section !== '') {
yield new SectionEndLine($section);
}
unset($expected[$section]);
}
}
}
<?php
namespace Avris\Dotenv\Service;
use Avris\Dotenv\Exception\ParseException;
use Avris\Dotenv\Line\IgnoredLine;
use Avris\Dotenv\Line\InvalidLine;
use Avris\Dotenv\Line\Line;
use Avris\Dotenv\Line\SectionEndLine;
use Avris\Dotenv\Line\SectionStartLine;
use Avris\Dotenv\Line\VarLine;
use Symfony\Component\Process\Process;
final class Parser
{
const SECTION_NAME = '[A-Za-z0-9\\\\]+';
const VAR_NAME = '[A-Z][A-Z0-9_]*';
/** @var string[] */
private $parsedValues = [];
public function parse(string $data): array
{
$vars = [];
$section = '';
foreach ($this->getLines($data) as $line) {
if ($line instanceof SectionStartLine) {
if ($section !== '') {
throw new ParseException($line);