* * For the full copyright and license information, please view the LICENSE * file that was distributed with this source code. */ namespace League\Config; use Dflydev\DotAccessData\Data; use Dflydev\DotAccessData\DataInterface; use Dflydev\DotAccessData\Exception\DataException; use Dflydev\DotAccessData\Exception\InvalidPathException; use Dflydev\DotAccessData\Exception\MissingPathException; use League\Config\Exception\UnknownOptionException; use League\Config\Exception\ValidationException; use Nette\Schema\Expect; use Nette\Schema\Processor; use Nette\Schema\Schema; use Nette\Schema\ValidationException as NetteValidationException; final class Configuration implements ConfigurationBuilderInterface, ConfigurationInterface { /** @psalm-readonly */ private Data $userConfig; /** * @var array * * @psalm-allow-private-mutation */ private array $configSchemas = []; /** @psalm-allow-private-mutation */ private Data $finalConfig; /** * @var array * * @psalm-allow-private-mutation */ private array $cache = []; /** @psalm-readonly */ private ConfigurationInterface $reader; /** * @param array $baseSchemas */ public function __construct(array $baseSchemas = []) { $this->configSchemas = $baseSchemas; $this->userConfig = new Data(); $this->finalConfig = new Data(); $this->reader = new ReadOnlyConfiguration($this); } /** * Registers a new configuration schema at the given top-level key * * @psalm-allow-private-mutation */ public function addSchema(string $key, Schema $schema): void { $this->invalidate(); $this->configSchemas[$key] = $schema; } /** * {@inheritDoc} * * @psalm-allow-private-mutation */ public function merge(array $config = []): void { $this->invalidate(); $this->userConfig->import($config, DataInterface::REPLACE); } /** * {@inheritDoc} * * @psalm-allow-private-mutation */ public function set(string $key, $value): void { $this->invalidate(); try { $this->userConfig->set($key, $value); } catch (DataException $ex) { throw new UnknownOptionException($ex->getMessage(), $key, (int) $ex->getCode(), $ex); } } /** * {@inheritDoc} * * @psalm-external-mutation-free */ public function get(string $key) { if (\array_key_exists($key, $this->cache)) { return $this->cache[$key]; } try { $this->build(self::getTopLevelKey($key)); return $this->cache[$key] = $this->finalConfig->get($key); } catch (InvalidPathException | MissingPathException $ex) { throw new UnknownOptionException($ex->getMessage(), $key, (int) $ex->getCode(), $ex); } } /** * {@inheritDoc} * * @psalm-external-mutation-free */ public function exists(string $key): bool { if (\array_key_exists($key, $this->cache)) { return true; } try { $this->build(self::getTopLevelKey($key)); return $this->finalConfig->has($key); } catch (InvalidPathException | UnknownOptionException $ex) { return false; } } /** * @psalm-mutation-free */ public function reader(): ConfigurationInterface { return $this->reader; } /** * @psalm-external-mutation-free */ private function invalidate(): void { $this->cache = []; $this->finalConfig = new Data(); } /** * Applies the schema against the configuration to return the final configuration * * @throws ValidationException|UnknownOptionException|InvalidPathException * * @psalm-allow-private-mutation */ private function build(string $topLevelKey): void { if ($this->finalConfig->has($topLevelKey)) { return; } if (! isset($this->configSchemas[$topLevelKey])) { throw new UnknownOptionException(\sprintf('Missing config schema for "%s"', $topLevelKey), $topLevelKey); } try { $userData = [$topLevelKey => $this->userConfig->get($topLevelKey)]; } catch (DataException $ex) { $userData = []; } try { $schema = $this->configSchemas[$topLevelKey]; $processor = new Processor(); $processed = $processor->process(Expect::structure([$topLevelKey => $schema]), $userData); $this->raiseAnyDeprecationNotices($processor->getWarnings()); $this->finalConfig->import((array) self::convertStdClassesToArrays($processed)); } catch (NetteValidationException $ex) { throw new ValidationException($ex); } } /** * Recursively converts stdClass instances to arrays * * @phpstan-template T * * @param T $data * * @return mixed * * @phpstan-return ($data is \stdClass ? array : T) * * @psalm-pure */ private static function convertStdClassesToArrays($data) { if ($data instanceof \stdClass) { $data = (array) $data; } if (\is_array($data)) { foreach ($data as $k => $v) { $data[$k] = self::convertStdClassesToArrays($v); } } return $data; } /** * @param string[] $warnings */ private function raiseAnyDeprecationNotices(array $warnings): void { foreach ($warnings as $warning) { @\trigger_error($warning, \E_USER_DEPRECATED); } } /** * @throws InvalidPathException */ private static function getTopLevelKey(string $path): string { if (\strlen($path) === 0) { throw new InvalidPathException('Path cannot be an empty string'); } $path = \str_replace(['.', '/'], '.', $path); $firstDelimiter = \strpos($path, '.'); if ($firstDelimiter === false) { return $path; } return \substr($path, 0, $firstDelimiter); } }