packages/deriver/src/Configuration/Configuration.php

1<?php
2
3declare(strict_types=1);
4
5namespace Deriver\Configuration;
6
7use Deriver\Exception\ConfigurationException;
8
9/**
10 * Validated command settings, independent of the preparation and evaluation layers.
11 * @visibility public
12 * @example Reading command defaults
13 *     \Deriver\Configuration\Configuration::fromArray([])->paths // => ['.']
14 */
15final class Configuration
16{
17    /**
18     * @var array<string, scalar|array<array-key, mixed>|null> Explicit native variable and environment inputs
19     */
20    public readonly array $environment;
21
22    /**
23     * @param list<string> $paths Search roots
24     * @param list<string> $include Root-relative wildcard patterns
25     * @param list<string> $exclude Root-relative wildcard patterns
26     * @throws ConfigurationException If source selection is empty or a language setting is outside its supported range
27     */
28    private function __construct(
29        public readonly array $paths,
30        public readonly array $include,
31        public readonly array $exclude,
32        public readonly ?string $phpVersion,
33        public readonly int $integerBits,
34        public readonly ?int $floatPrecision,
35        Environment $environment,
36        public readonly bool $standardEvaluators,
37        public readonly ReportFormat $format,
38    ) {
39        if ($paths === [] || $include === []) {
40            throw new ConfigurationException('paths and include must each contain at least one entry.');
41        }
42        if ($phpVersion !== null && preg_match('/\A[1-9][0-9]*\.[0-9]+\z/', $phpVersion) !== 1) {
43            throw new ConfigurationException('phpVersion must use major.minor notation.');
44        }
45        if ($integerBits !== 64 || $floatPrecision !== null && $floatPrecision < -1) {
46            throw new ConfigurationException('integerBits must be 64 and floatPrecision must be null or an integer >= -1.');
47        }
48        $this->environment = $environment->values;
49    }
50
51    /**
52     * Validates a settings mapping without reading files or constructing analysis objects.
53     * @param array<array-key, mixed> $settings Settings decoded from YAML or supplied by a caller
54     * @throws ConfigurationException If keys or values do not match the command schema
55     */
56    public static function fromArray(array $settings): self
57    {
58        $defaults = ['paths' => ['.'], 'include' => ['**/*.php'], 'exclude' => [], 'phpVersion' => null, 'integerBits' => 64, 'floatPrecision' => null, 'environment' => [], 'standardEvaluators' => true, 'format' => 'text'];
59        foreach ($settings as $key => $value) {
60            if (!array_key_exists($key, $defaults)) {
61                throw new ConfigurationException('Unknown configuration key: ' . $key);
62            }
63        }
64        $settings = array_replace($defaults, $settings);
65        $patterns = [];
66        foreach (['paths', 'include', 'exclude'] as $key) {
67            $items = $settings[$key];
68            if (!is_array($items) || !array_is_list($items)) {
69                throw new ConfigurationException($key . ' must be a list of strings.');
70            }
71            $patterns[$key] = [];
72            foreach ($items as $item) {
73                if (!is_string($item) || trim($item) === '' || str_contains($item, "\0")) {
74                    throw new ConfigurationException($key . ' must contain nonempty strings without null bytes.');
75                }
76                $patterns[$key][] = $item;
77            }
78        }
79        $version = $settings['phpVersion'];
80        $precision = $settings['floatPrecision'];
81        if ($version !== null && !is_string($version)) {
82            throw new ConfigurationException('phpVersion must be a quoted major.minor string.');
83        }
84        if (!is_int($settings['integerBits']) || $precision !== null && !is_int($precision) || !is_bool($settings['standardEvaluators'])) {
85            throw new ConfigurationException('integerBits must be 64, floatPrecision null or an integer >= -1, and standardEvaluators a boolean.');
86        }
87        if (!is_array($settings['environment']) || !is_string($settings['format']) || ($format = ReportFormat::tryFrom($settings['format'])) === null) {
88            throw new ConfigurationException('environment must be a mapping and format must be text or json.');
89        }
90        $environment = new Environment($settings['environment']);
91        return new self($patterns['paths'], $patterns['include'], $patterns['exclude'], $version, $settings['integerBits'], $precision, $environment, $settings['standardEvaluators'], $format);
92    }
93}
94