wiki.techinc.nl/includes/Settings/Source/ReflectionSchemaSource.php
Aryeh Gregor b72b9a8c43 Move dynamic defaults into MainConfigSchema
The goal is to keep the actual default values for settings in the same
place as the setting is declared, and applied using the regular means
for loading the settings -- not in a separate piece of code that needs
to be loaded through some entirely different mechanism.

SetupDynamicConfig.php now contains a few categories of things:

* Post-processing of configuration settings, where already-set settings
  are altered. This could be moved to MainConfigSchema too as a separate
  set of methods.
* Processing of old aliases of settings (blacklist, slave) that are not
  registered as settings anymore and therefore are not available to
  MainConfigSchema. This could perhaps be moved to LocalSettings
  processing somehow?
* Setting $wgUseEnotif, which is also not registered as a setting.
  Easiest would be just to declare it as a setting and have it set
  unconditionally.
* Setting the actual timezone to $wgLocaltimezone. This is not related
  to configuration and should just be in Setup.php.

Bug: T305093
Change-Id: Ia5c23b52dbbfcb3d07ffcf5d3b7f2d7befba2a26
2022-07-07 09:55:48 +10:00

165 lines
4.3 KiB
PHP

<?php
namespace MediaWiki\Settings\Source;
use Closure;
use MediaWiki\Settings\SettingsBuilderException;
use ReflectionClass;
use ReflectionException;
/**
* Constructs a settings array based on a PHP class by inspecting class
* members to construct a schema.
*
* The value of each constant must be an array structured like a JSON Schema.
* For convenience, type declarations support PHPDoc style types in addition to
* JSON types. To avoid confusion, use 'list' for sequential arrays and 'map'
* for associative arrays.
*
* Dynamic default values can be declared using the 'dynamicDefault' key.
* The structure of the dynamic default declaration is an array with two keys:
* - 'callback': this is a PHP callable string or array, closures are not supported.
* - 'use': A list of other config variables that the dynamic default depends on.
* The values of these variables will be passed to the callback as parameters.
*
* The following shorthands can be used with dynamic default declarations:
* - if the value for 'use' is empty, it can be omitted.
* - if 'callback' is omitted, it is assumed to be a static method "getDefault$name" on
* the same class where $name is the name of the variable.
* - if the dynamic default declaration is not an array but a string, that
* string is taken to be the callback, with no parameters.
* - if the dynamic default declaration is the boolean value true,
* the callback is assumed to be a static method "getDefault$name" on
* the same class where $name is the name of the variable.
*
* @since 1.39
*/
class ReflectionSchemaSource implements SettingsSource {
use JsonSchemaTrait;
/**
* Name of a PHP class
* @var string
*/
private $class;
/**
* @var bool
*/
private $includeDoc;
/**
* @param string $class
* @param bool $includeDoc
*/
public function __construct( string $class, bool $includeDoc = false ) {
$this->class = $class;
$this->includeDoc = $includeDoc;
}
/**
* @throws SettingsBuilderException
* @return array
*/
public function load(): array {
$schemas = [];
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 ( $this->includeDoc ) {
$doc = $const->getDocComment();
if ( $doc ) {
$schema['description'] = $this->normalizeComment( $doc );
}
}
if ( isset( $schema['dynamicDefault'] ) ) {
$schema['dynamicDefault'] =
$this->normalizeDynamicDefault( $name, $schema['dynamicDefault'] );
}
if ( !array_key_exists( 'default', $schema ) ) {
$schema['default'] = null;
}
$schema = self::normalizeJsonSchema( $schema );
$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
];
}
/**
* 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*$/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;
}
}