$extensions */ public function __construct( public bool $zts, public array $extensions, ) { } public static function fromYaml(mixed $value): self { if (!is_array($value) || ($value !== [] && array_is_list($value))) { throw new \InvalidArgumentException('`php-builder` must be a mapping'); } return self::fromArray($value); } public static function fromCommandLine(string $value): self { $value = trim($value); if ($value === '') { throw new \InvalidArgumentException('Option --php-builder requires a non-empty configuration'); } try { $parsed = Yaml::parse(self::semicolonSeparatedYaml($value)); } catch (\Throwable $exception) { throw new \InvalidArgumentException( 'Invalid --php-builder configuration: ' . $exception->getMessage(), previous: $exception, ); } return self::fromYaml($parsed); } /** @param array $config */ private static function fromArray(array $config): self { $unknown = array_diff(array_keys($config), ['zts', 'extensions']); if ($unknown !== []) { throw new \InvalidArgumentException( 'Unknown `php-builder` option' . (count($unknown) > 1 ? 's' : '') . ': ' . implode(', ', array_map(static fn (string $key): string => "`{$key}`", $unknown)), ); } $extensions = self::stringList($config['extensions'] ?? [], 'php-builder.extensions', true); $extensions = SapiExtensionRequirements::merge($extensions); return new self( self::boolean($config['zts'] ?? false), $extensions, ); } /** @return list */ private static function stringList(mixed $value, string $name, bool $allowEmpty = false): array { $values = is_string($value) ? preg_split('/\s*,\s*/', trim($value)) : $value; if (!is_array($values)) { throw new \InvalidArgumentException("`{$name}` must be a string or list"); } $result = []; foreach ($values as $entry) { if (!is_string($entry) || trim($entry) === '') { throw new \InvalidArgumentException("Each `{$name}` entry must be a non-empty string"); } $result[] = strtolower(trim($entry)); } if (!$allowEmpty && $result === []) { throw new \InvalidArgumentException("`{$name}` must not be empty"); } return $result; } private static function boolean(mixed $value): bool { if (is_bool($value)) { return $value; } if ($value === 1 || $value === 0) { return (bool) $value; } if (is_string($value)) { return match (strtolower(trim($value))) { 'on', 'true', 'yes', '1' => true, 'off', 'false', 'no', '0' => false, default => throw new \InvalidArgumentException('`php-builder.zts` must be on or off'), }; } throw new \InvalidArgumentException('`php-builder.zts` must be on or off'); } private static function semicolonSeparatedYaml(string $value): string { $result = ''; $quote = null; $depth = 0; $escaped = false; foreach (str_split($value) as $character) { if ($escaped) { $result .= $character; $escaped = false; continue; } if ($character === '\\' && $quote === '"') { $result .= $character; $escaped = true; continue; } if (($character === '"' || $character === "'") && ($quote === null || $quote === $character)) { $quote = $quote === null ? $character : null; $result .= $character; continue; } if ($quote === null) { if ($character === '[' || $character === '{') { $depth++; } elseif ($character === ']' || $character === '}') { $depth--; } elseif ($character === ';' && $depth === 0) { $result .= "\n"; continue; } } $result .= $character; } return preg_replace('/\n[ \t]+/', "\n", trim($result)) ?? trim($result); } }