diff --git a/composer.json b/composer.json index a3885e0..75a2f4a 100644 --- a/composer.json +++ b/composer.json @@ -4,8 +4,9 @@ "keywords": [ ], "minimum-stability": "stable", - "require": { - "php": "~7.4 || ~8.0" + "require": { + "php": "~7.4 || ~8.0", + "jrosset/singleton": "^1.0" }, "autoload": { "psr-4": { @@ -30,4 +31,4 @@ "docs": "https://git.jrosset.ovh/jrosset/PhpEnvReader/wiki", "source": "https://git.jrosset.ovh/jrosset/PhpEnvReader" } -} \ No newline at end of file +} diff --git a/composer.lock b/composer.lock index 80da9e8..e596821 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,50 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "2ab889d9a2d5d5b28b48164a73b529b2", - "packages": [ ], + "content-hash": "2f92e58b7bd7a162f29ef03fe018cec6", + "packages": [ + { + "name": "jrosset/singleton", + "version": "v1.0.0", + "source": { + "type": "git", + "url": "https://git.jrosset.ovh/jrosset/PhpSingleton", + "reference": "0fccc3b4b1c46e42656e84e264555e57bf2f27cd" + }, + "require": { + "php": "~7.4 || ~8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "jrosset\\": "src/" + }, + "exclude-from-classmap": [ + "tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "CC-BY-4.0" + ], + "authors": [ + { + "name": "Julien Rosset", + "email": "jul.rosset@gmail.com" + } + ], + "description": "PHP Trait to implements the singleton design pattern", + "homepage": "https://git.jrosset.ovh/jrosset/PhpSingleton", + "support": { + "docs": "https://git.jrosset.ovh/jrosset/PhpSingleton/wiki", + "email": "jul.rosset@gmail.com", + "issues": "https://git.jrosset.ovh/jrosset/PhpSingleton/issues", + "source": "https://git.jrosset.ovh/jrosset/PhpSingleton", + "wiki": "https://git.jrosset.ovh/jrosset/PhpSingleton/wiki" + }, + "time": "2021-09-03T10:57:30+00:00" + } + ], "packages-dev": [ ], "aliases": [ ], "minimum-stability": "stable", diff --git a/src/EnvReader/Env.php b/src/EnvReader/Env.php new file mode 100644 index 0000000..8fd1089 --- /dev/null +++ b/src/EnvReader/Env.php @@ -0,0 +1,235 @@ + + * Overwrite {@see Env::initProperties()} to set initial properties + */ +abstract class Env { + use TSingleton; + + /** + * ENV file path + */ + protected const PATH_ENV = '.env'; + + /** + * @var string[] Current properties, read from ENV file (<property name> => <property value>)
+ * NOTE: All properties' name are stored in upper-case + */ + private array $properties; + + /** + * Initialize initial properties then read ENV file + * + * @throws Exception If ENV can't be read + */ + protected function __construct () { + $this->properties = array_change_key_case($this->initProperties(), CASE_UPPER); + $this->readEnv(); + } + /** + * Get all properties + * + * @return string[] Get all properties (properties' name are in upper case) + */ + public function getProperties (): array { + return $this->properties; + } + /** + * Check if a property id defined + * + * @param string $name The property name + * + * @return bool Is the property defined ? + */ + public function hasProperty (string $name): bool { + return array_key_exists(mb_strtoupper($name), $this->getProperties()); + } + /** + * Get a property value + * + * @param string $name The property name + * @param string|null $default The default value if property is not set + * Raise an exception if property is not set AND $default is Null + * + * @return string The property value + * + * @throws UnexpectedValueException If property is not set AND $default is Null + */ + public function getProperty (string $name, ?string $default = null): string { + $name = mb_strtoupper($name); + if (!$this->hasProperty($name)) { + if ($default === null) { + throw new UnexpectedValueException('The "' . $name . '" property is not set'); + } + return $default; + } + + return $this->properties[$name]; + } + /** + * Get a property value as a boolean + * + * @param string $name The property name + * @param bool|null $default The default value if property is not set + * Raise an exception if property is not set AND $default is Null + * + * @return bool The property value + * + * @throws UnexpectedValueException If property is not set AND $default is Null + * @throws RangeException If the property can't be cast to a boolean + * + * @noinspection PhpUnused + */ + public function getPropertyAsBool (string $name, ?bool $default = null): bool { + switch ($value = $this->getProperty($name, $default === null ? null : ($default === true ? '1' : '0'))) { + case '1': + case 'true': + return true; + + case '0': + case 'false': + return false; + + default: + throw new RangeException('The "' . $name . '" property is not a valid boolean : ' . $value); + } + } + /** + * Get a property value as an integer + * + * @param string $name The property name + * @param int|null $default The default value if property is not set + * Raise an exception if property is not set AND $default is Null + * + * @return int The property value + * + * @throws UnexpectedValueException If property is not set AND $default is Null + * @throws RangeException If the property can't be cast to an integer + * + * @noinspection PhpUnused + */ + public function getPropertyAsInt (string $name, ?int $default = null): int { + $value = $this->getProperty($name, $default === null ? null : (string)$default); + if (preg_match('#^\s*(?[0-9]+)\s*$#', $value, $match) !== 1) { + throw new RangeException('The "' . $name . '" property is not a valid integer : ' . $value); + } + return (int)$match['value']; + } + /** + * Get a property value as a float value (the decimal separator is dot) + * + * @param string $name The property name + * @param float|null $default The default value if property is not set + * Raise an exception if property is not set AND $default is Null + * + * @return float The property value + * + * @throws UnexpectedValueException If property is not set AND $default is Null + * @throws RangeException If the property can't be cast to a float value + * + * @noinspection PhpUnused + */ + public function getPropertyAsReal (string $name, ?float $default = null): float { + $value = $this->getProperty($name, $default === null ? null : (string)$default); + if (preg_match('#^\s*(?[0-9]+(?:\.[0-9]+)?)\s*$#', $value, $match) !== 1) { + throw new RangeException('The "' . $name . '" property is not a valid float value : ' . $value); + } + return (float)$match['value']; + } + /** + * Get a property value as a date and time ({@link https://www.php.net/manual/book.datetime.php DateTime}) + * + * The date must respect {@link https://datatracker.ietf.org/doc/html/rfc3339 RFC339} norm + * + * @param string $name The property name + * @param DateTime|null $default The default value if property is not set + * Raise an exception if property is not set AND $default is Null + * + * @return DateTime The property value + * + * @throws UnexpectedValueException If property is not set AND $default is Null + * @throws RangeException If the property can't be cast to a date and time + * + * @noinspection PhpUnused + */ + public function getPropertyAsDateTime (string $name, ?DateTime $default = null): DateTime { + $value = DateTime::createFromFormat( + DateTimeInterface::RFC3339, + $this->getProperty($name, $default === null ? null : $default->format(DateTimeInterface::RFC3339)) + ); + + $errors = DateTime::getLastErrors(); + if ($errors !== false) { + $messages = []; + + if (($nb_error = $errors['error_count']) > 0) { + foreach ($errors['errors'] as $idx => $error) { + $messages[] = 'ERREUR #' . $idx . ' : ' . $error; + } + } + if (($nb_warning = $errors['warning_count']) > 0) { + foreach ($errors['warnings'] as $idx => $warning) { + $messages[] = 'ALERTE #' . $idx . ' : ' . $warning; + } + } + + if ($nb_error + $nb_warning > 0) { + throw new RangeException('The "' . $name . '" property is not a valid date and time : ' . $value . "\n\n" . implode("\n", $messages)); + } + } + if ($value === false) { + throw new RangeException('The "' . $name . '" property is not a valid date and time : ' . $value); + } + + return $value; + } + /** + * Initialize initial properties + * + * @return string[] Properties initial values (<property name> => <property value>) + */ + protected function initProperties (): array { + return [ + 'APP_DEV' => 0, + ]; + } + /** + * Read the ENV file + * + * @throws Exception If ENV can't be read + */ + private function readEnv (): void { + if (!file_exists(static::PATH_ENV)) { + return; + } + + $lines = file(static::PATH_ENV, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES); + if ($lines === false) { + throw new Exception('Unable to read environment file "' . static::PATH_ENV . '"'); + } + + foreach ($lines as $line) { + if (preg_match('/^\s*(?:(?#).+|(?[a-zA-Z0-9_-]+)=(?.+))$/i', $line, $match) !== 1) { + continue; // Ligne invalide + } + + if (isset($match['comment']) && trim($match['comment']) !== '') { + continue; // Commentaire + } + + $this->properties[mb_strtoupper($match['key'])] = $match['value']; + } + } +} \ No newline at end of file