You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
PhpEnvReader/src/EnvReader/GenericConfig.php

289 lines
11 KiB
PHP

<?php
namespace jrosset\EnvReader;
use DateTime;
use DateTimeInterface;
use Exception;
use InvalidArgumentException;
use jrosset\Collections\IArrayCast;
use jrosset\Collections\InsensitiveCaseKeyCollection;
use jrosset\Collections\InsensitiveCaseKeyImmutableCollection;
use jrosset\Singleton\ISingleton;
use jrosset\Singleton\TSingleton;
use RangeException;
use ReflectionEnum;
use ReflectionEnumBackedCase;
use ReflectionException;
use UnexpectedValueException;
use UnitEnum;
/**
* A generic configuration class
*
* Overwrite {@see GenericConfig::initialProperties()} to set initial properties
*/
abstract class GenericConfig implements ISingleton {
use TSingleton;
/**
* @var InsensitiveCaseKeyCollection Current properties
*/
protected InsensitiveCaseKeyCollection $properties;
/**
* Initialize initial properties then read ENV file
*
* @throws Exception If ENV can't be read
*/
protected function __construct () {
$this->properties = new InsensitiveCaseKeyCollection($this->initialProperties());
$this->readConfig();
}
/**
* Initial properties
*
* @return string[]|IArrayCast Initial properties
*/
protected function initialProperties (): array|IArrayCast {
return [];
}
/**
* Read the configuration file
*
* @return void
*
* @throws Exception If the configuration file is invalid
*/
protected abstract function readConfig (): void;
/**
* Get all properties
*
* @return InsensitiveCaseKeyImmutableCollection Get all properties (properties' name are in upper case)
*/
public function getProperties (): InsensitiveCaseKeyImmutableCollection {
return new InsensitiveCaseKeyImmutableCollection($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 $this->properties->exists($name);
}
/**
* Get a property value
*
* @param string $name The property name
* @param mixed|null $default The default value if property is not set
* <br>Raise an exception if property is not set AND $default is Null
*
* @return mixed The property value
*
* @throws UnexpectedValueException If property is not set AND $default is Null
*/
public function getProperty (string $name, mixed $default = null): mixed {
if (!$this->hasProperty($name)) {
if ($default === null) {
throw new UnexpectedValueException('The "' . $name . '" property is not set');
}
return $default;
}
return $this->properties->get($name);
}
/**
* Get a property value as a string
*
* @param string $name The property name
* @param string|null $default The default value if property is not set
* <br>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
* @throws RangeException If the property can't be cast to a boolean
*
* @noinspection PhpUnused
*/
public function getPropertyAsString (string $name, ?string $default = null): string {
$value = $this->getProperty($name, $default);
if (!is_string($value) && !is_object($value) && !method_exists($value, '__ToString')) {
throw new RangeException('The "' . $name . '" property is not a valid string : ' . $value);
}
return (string)$value;
}
/**
* 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
* <br>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 {
return match ($value = $this->getProperty($name, $default === null ? null : ($default === true ? '1' : '0'))) {
'1', 'true' => true,
'0', 'false' => 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
* <br>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*(?<value>\d+)\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
* <br>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*(?<value>\d+(?:\.\d+)?)\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 <b>must</b> 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
* <br>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?->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');
}
return $value;
}
/**
* Get a property value as an enum
*
* @param string $name The property name
* @param class-string<UnitEnum> $enumClass The enum class name
* @param UnitEnum|null $default The default value if property is not set
* <br>Raise an exception if not <b>$enumClass</b> enum
* <br>Raise an exception if property is not set AND $default is Null
* <br>Raise an exception if property is set but not valid (not an enum)
*
* @return UnitEnum The enum
*
* @throws ReflectionException If <b>$enumClass</b> is not a valid enum class
* @throws InvalidArgumentException If <b>$default</b> is not a <b>$enumClass</b> enum
*/
public function getPropertyAsEnum (string $name, string $enumClass, ?UnitEnum $default = null): UnitEnum {
$enumReflection = new ReflectionEnum($enumClass);
//region Check default value type
if ($default !== null && !$default instanceof $enumClass) {
throw new InvalidArgumentException('The default property type must be an ' . $enumClass);
}
//endregion
//region Return default value (or raise exception) if property is not set
if (!$this->hasProperty($name)) {
if ($default === null) {
throw new UnexpectedValueException('The "' . $name . '" property is not set');
}
return $default;
}
//endregion
//region Try to find the enum through a case name
$propertyValue = $this->getPropertyAsString($name);
if ($enumReflection->hasCase($propertyValue)) {
return $enumReflection->getCase($propertyValue)->getValue();
}
//endregion
//region Try to find enum through the value (if BackedEnum)
if ($enumReflection->isBacked()) {
if (((string)$enumReflection->getBackingType()) === 'int') {
$propertyValue = $this->getPropertyAsInt($name);
}
/** @var ReflectionEnumBackedCase $enumBackedCase */
foreach ($enumReflection->getCases() as $enumBackedCase) {
if ($enumBackedCase->getBackingValue() === $propertyValue) {
return $enumBackedCase->getValue();
}
}
}
//endregion
//region Unable to find valid enum → raise exception
throw new UnexpectedValueException('The "' . $name . '" property is not a valid enum ' . $enumClass);
//endregion
}
}