class = $class; $this->includeDoc = $includeDoc; } /** * @inheritDoc */ public function load(): array { return $this->loadAsComponents(); } /** * @param bool $inlineReferences Whether the references found in the schema `$ref` should * be inlined, meaning resolving its final type and embedding it as a regular schema. No * definitions `$defs` will be returned. * @throws SettingsBuilderException * @return array */ public function loadAsComponents( bool $inlineReferences = false ): array { $schemas = []; $defs = []; $obsolete = []; try { $class = new ReflectionClass( $this->class ); foreach ( $class->getReflectionConstants() as $const ) { if ( !$const->isPublic() ) { continue; } $name = $const->getName(); $schema = $const->getValue(); if ( !is_array( $schema ) ) { continue; } if ( isset( $schema['obsolete'] ) ) { $obsolete[ $name ] = $schema['obsolete']; continue; } if ( $this->includeDoc ) { $doc = $const->getDocComment(); if ( $doc ) { $schema['description'] = $this->normalizeComment( $doc ); } } if ( isset( $schema['dynamicDefault'] ) ) { $schema['dynamicDefault'] = $this->normalizeDynamicDefault( $name, $schema['dynamicDefault'] ); } $schema['default'] ??= null; $schema = self::normalizeJsonSchema( $schema, $defs, $this->class, $name, $inlineReferences ); $schemas[ $name ] = $schema; } } catch ( ReflectionException $e ) { throw new SettingsBuilderException( 'Failed to load schema from class {class}', [ 'class' => $this->class ], 0, $e ); } return [ 'config-schema' => $schemas, 'schema-definitions' => $defs, 'obsolete-config' => $obsolete ]; } /** * Load the data as a single top-level JSON Schema. * * Returned JSON Schema is for an object, which includes the individual config schemas. The * returned schema may contain `$defs`, which then may be referenced internally in the schema * via `$ref`. * * @param bool $inlineReferences Whether the references found in the schema `$ref` should * be inlined, meaning resolving its final type and embedding it as a regular schema. No * definitions `$defs` will be returned. * @return array */ public function loadAsSchema( bool $inlineReferences = false ): array { $info = $this->loadAsComponents( $inlineReferences ); $schema = [ 'type' => 'object', 'properties' => $info['config-schema'], ]; if ( $info['schema-definitions'] ) { $schema['$defs'] = $info['schema-definitions']; } return $schema; } /** * Returns this file source as a string. * * @return string */ public function __toString(): string { return 'class ' . $this->class; } private function normalizeComment( string $doc ) { $doc = preg_replace( '/^\s*\/\*+\s*|\s*\*+\/\s*$/', '', $doc ); $doc = preg_replace( '/^\s*\**$/m', " ", $doc ); $doc = preg_replace( '/^\s*\**[ \t]?/m', '', $doc ); return $doc; } private function normalizeDynamicDefault( string $name, $spec ) { if ( $spec === true ) { $spec = [ 'callback' => [ $this->class, "getDefault{$name}" ] ]; } if ( is_string( $spec ) ) { $spec = [ 'callback' => $spec ]; } if ( !isset( $spec['callback'] ) ) { $spec['callback'] = [ $this->class, "getDefault{$name}" ]; } // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset per fallback above. if ( $spec['callback'] instanceof Closure ) { throw new SettingsBuilderException( "dynamicDefaults callback for $name must be JSON serializable. " . "Closures are not supported." ); } if ( !is_callable( $spec['callback'] ) ) { $pretty = var_export( $spec['callback'], true ); $pretty = preg_replace( '/\s+/', ' ', $pretty ); throw new SettingsBuilderException( "dynamicDefaults callback for $name is not callable: " . $pretty ); } return $spec; } }