Use class constants to define config schema, rather than config-schema.yaml

Instead of maintaining the config schema as a yaml file, we
maintain it as a set of constants in a class. From the information in
these constants, we can generate a JSON schema (yaml) file, and an
php file containing optimized arrays for fast loading.

Advantages:
- PHP doc available to IDEs. The generated markdown file is no longer
  needed.
- Can use PHP constants when defining default values.

NOTE: needs backport to 1.38

Change-Id: I663c08b8a200644cbe7e5f65c20f1592a4f3974d
This commit is contained in:
daniel 2022-03-10 21:38:13 +01:00
parent 5ec380a8f6
commit 2fe23d6860
13 changed files with 13229 additions and 8509 deletions

View file

@ -286,6 +286,7 @@ $wgAutoloadLocalClasses = [
'Config' => __DIR__ . '/includes/config/Config.php',
'ConfigException' => __DIR__ . '/includes/config/ConfigException.php',
'ConfigFactory' => __DIR__ . '/includes/config/ConfigFactory.php',
'ConfigSchemaDerivativeTrait' => __DIR__ . '/maintenance/includes/ConfigSchemaDerivativeTrait.php',
'ConfiguredReadOnlyMode' => __DIR__ . '/includes/ConfiguredReadOnlyMode.php',
'ConstantDependency' => __DIR__ . '/includes/cache/dependency/ConstantDependency.php',
'Content' => __DIR__ . '/includes/content/Content.php',
@ -541,8 +542,8 @@ $wgAutoloadLocalClasses = [
'GanConverter' => __DIR__ . '/includes/language/converters/GanConverter.php',
'GenderCache' => __DIR__ . '/includes/cache/GenderCache.php',
'GenerateCollationData' => __DIR__ . '/maintenance/language/generateCollationData.php',
'GenerateConfigDoc' => __DIR__ . '/maintenance/generateConfigDoc.php',
'GenerateConfigSchema' => __DIR__ . '/maintenance/generateConfigSchema.php',
'GenerateConfigSchemaArray' => __DIR__ . '/maintenance/generateConfigSchemaArray.php',
'GenerateConfigSchemaYaml' => __DIR__ . '/maintenance/generateConfigSchemaYaml.php',
'GenerateJsonI18n' => __DIR__ . '/maintenance/generateJsonI18n.php',
'GenerateNormalizerDataAr' => __DIR__ . '/maintenance/language/generateNormalizerDataAr.php',
'GenerateNormalizerDataMl' => __DIR__ . '/maintenance/language/generateNormalizerDataMl.php',

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

11903
includes/MainConfigSchema.php Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,97 @@
<?php
namespace MediaWiki\Settings\Source;
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.
*
* @since 1.38
*/
class ReflectionSchemaSource implements SettingsSource {
/**
* 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();
$value = $const->getValue();
if ( !is_array( $value ) ) {
continue;
}
if ( $this->includeDoc ) {
$doc = $const->getDocComment();
if ( $doc ) {
$value['description'] = $this->normalizeComment( $doc );
}
}
$schemas[ $name ] = $value;
}
} 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;
}
}

View file

@ -289,8 +289,8 @@ return [
'CopyUploadTimeout' => [
'default' => false,
'type' => [
0 => 'integer',
1 => 'boolean',
0 => 'boolean',
1 => 'integer',
],
],
'MaxUploadSize' => [
@ -3394,9 +3394,6 @@ return [
'UseAutomaticEditSummaries' => [
'default' => true,
],
'CommandLineMode' => [
'default' => false,
],
'CommandLineDarkBg' => [
'default' => false,
],
@ -4234,7 +4231,7 @@ return [
'type' => [
0 => 'string',
1 => 'boolean',
2 => null,
2 => 'null',
],
],
],

View file

@ -1,102 +0,0 @@
<?php
/**
* Convert a JSON abstract schema to a schema file in the given DBMS type
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Maintenance
*/
use MediaWiki\Settings\Source\FileSource;
require_once __DIR__ . '/Maintenance.php';
/**
* Maintenance script to generate doc/configuration.md from
* includes/config-schema.yaml
*
* @ingroup Maintenance
*/
class GenerateConfigDoc extends Maintenance {
/** @var string */
private const DEFAULT_INPUT_PATH = 'includes/config-schema.yaml';
/** @var string */
private const DEFAULT_OUTPUT_PATH = 'docs/Configuration.md';
public function __construct() {
parent::__construct();
$this->addDescription( 'Build Configuration.md file from config-schema.yaml' );
$this->addOption(
'schema',
'Path to the config schema file relative to $IP. Default: ' . self::DEFAULT_INPUT_PATH,
false,
true
);
$this->addOption(
'output',
'Path to output relative to $IP. Default: ' . self::DEFAULT_OUTPUT_PATH,
false,
true
);
}
public function execute() {
$input = ( new FileSource( $this->getInputPath() ) )->load();
$result = "<!-- This file is automatically generated using maintenance/generateConfigDoc.php. -->\n";
$result .= "<!-- Do not edit this file directly. -->\n";
$result .= "<!-- See maintenance/generateConfigDoc.php for instructions. -->\n";
$result .= "This is a list of configuration variables that can be set in LocalSettings.php.\n\n";
// Table of contents
$result .= "[TOC]\n\n";
// Details about each config variable
foreach ( $input['config-schema'] as $configKey => $configSchema ) {
$result .= "# $configKey {#$configKey}\n";
if ( array_key_exists( 'description', $configSchema ) ) {
$result .= $configSchema['description'];
}
$result .= "\n\n";
}
file_put_contents( $this->getOutputPath(), $result );
}
private function getInputPath(): string {
global $IP;
$inputPath = $this->getOption( 'schema', self::DEFAULT_INPUT_PATH );
return $IP . DIRECTORY_SEPARATOR . $inputPath;
}
private function getOutputPath(): string {
global $IP;
$outputPath = $this->getOption( 'output', self::DEFAULT_OUTPUT_PATH );
if ( $outputPath === '-' || $outputPath === 'php://stdout' ) {
return 'php://stdout';
}
return $IP . DIRECTORY_SEPARATOR . $outputPath;
}
}
$maintClass = GenerateConfigDoc::class;
require_once RUN_MAINTENANCE_IF_MAIN;

View file

@ -1,73 +0,0 @@
<?php
use MediaWiki\Settings\Source\FileSource;
use Wikimedia\StaticArrayWriter;
require_once __DIR__ . '/Maintenance.php';
/**
* Maintenance script that generates the PHP representation of the config-schema.yaml file.
*
* @ingroup Maintenance
*/
class GenerateConfigSchema extends Maintenance {
/** @var string */
private const DEFAULT_INPUT_PATH = 'includes/config-schema.yaml';
/** @var string */
private const DEFAULT_OUTPUT_PATH = 'includes/config-schema.php';
public function __construct() {
parent::__construct();
$this->addDescription( 'Build config-schema.php file from config-schema.yaml' );
$this->addOption(
'schema',
'Path to the config schema file relative to $IP. Default: ' . self::DEFAULT_INPUT_PATH,
false,
true
);
$this->addOption(
'output',
'Path to output relative to $IP. Default: ' . self::DEFAULT_OUTPUT_PATH,
false,
true
);
}
public function execute() {
$schema = ( new FileSource( $this->getInputPath() ) )->load();
foreach ( $schema['config-schema'] as $key => &$value ) {
unset( $value['description'] );
}
$content = ( new StaticArrayWriter() )->write(
$schema,
"This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
"phpcs:disable Generic.Files.LineLength"
);
file_put_contents( $this->getOutputPath(), $content );
}
private function getInputPath(): string {
global $IP;
$inputPath = $this->getOption( 'schema', self::DEFAULT_INPUT_PATH );
return $IP . DIRECTORY_SEPARATOR . $inputPath;
}
private function getOutputPath(): string {
global $IP;
$outputPath = $this->getOption( 'output', self::DEFAULT_OUTPUT_PATH );
if ( $outputPath === '-' || $outputPath === 'php://stdout' ) {
return 'php://stdout';
}
return $IP . DIRECTORY_SEPARATOR . $outputPath;
}
}
$maintClass = GenerateConfigSchema::class;
require_once RUN_MAINTENANCE_IF_MAIN;

View file

@ -0,0 +1,50 @@
<?php
use Wikimedia\StaticArrayWriter;
require_once __DIR__ . '/Maintenance.php';
require_once __DIR__ . '/includes/ConfigSchemaDerivativeTrait.php';
/**
* Maintenance script that generates the PHP representation of the config-schema.yaml file.
*
* @ingroup Maintenance
*/
class GenerateConfigSchemaArray extends Maintenance {
use ConfigSchemaDerivativeTrait;
/** @var string */
private const DEFAULT_OUTPUT_PATH = __DIR__ . '/../includes/config-schema.php';
public function __construct() {
parent::__construct();
$this->addDescription( 'Generate an optimized config-schema.php file.' );
$this->addOption(
'output',
'Path to output relative to $IP. Default: ' . self::DEFAULT_OUTPUT_PATH,
false,
true
);
}
public function execute() {
$schema = $this->loadSettingsSource();
foreach ( $schema['config-schema'] as $key => &$value ) {
unset( $value['description'] );
}
$content = ( new StaticArrayWriter() )->write(
$schema,
"This file is automatically generated using maintenance/generateConfigSchema.php.\n" .
"phpcs:disable Generic.Files.LineLength"
);
$this->writeOutput( self::DEFAULT_OUTPUT_PATH, $content );
}
}
$maintClass = GenerateConfigSchemaArray::class;
require_once RUN_MAINTENANCE_IF_MAIN;

View file

@ -0,0 +1,59 @@
<?php
use Symfony\Component\Yaml\Yaml;
require_once __DIR__ . '/Maintenance.php';
require_once __DIR__ . '/includes/ConfigSchemaDerivativeTrait.php';
/**
* Maintenance script that generates a YAML file containing a JSON Schema representation
* of the config schema.
*
* @ingroup Maintenance
*/
class GenerateConfigSchemaYaml extends Maintenance {
use ConfigSchemaDerivativeTrait;
/** @var string */
private const DEFAULT_OUTPUT_PATH = __DIR__ . '/../docs/config-schema.yaml';
public function __construct() {
parent::__construct();
$this->addDescription( 'Generate config-schema.yaml' );
$this->addOption(
'output',
'Output file. Default: ' . self::DEFAULT_OUTPUT_PATH,
false,
true
);
}
public function execute() {
$schemas = $this->loadSchema();
// Cast empty arrays to objects if they are declared to be of type object.
// This ensures they get represented in yaml as {} rather than [].
foreach ( $schemas as &$sch ) {
if ( isset( $sch['default'] ) && isset( $sch['type'] ) ) {
$types = (array)$sch['type'];
if ( $sch['default'] === [] && in_array( 'object', $types ) ) {
$sch['default'] = new stdClass();
}
}
}
$yamlFlags = Yaml::DUMP_OBJECT_AS_MAP
| Yaml::DUMP_MULTI_LINE_LITERAL_BLOCK
| Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE;
$array = [ 'config-schema' => $schemas ];
$yaml = Yaml::dump( $array, 4, 4, $yamlFlags );
$this->writeOutput( self::DEFAULT_OUTPUT_PATH, $yaml );
}
}
$maintClass = GenerateConfigSchemaYaml::class;
require_once RUN_MAINTENANCE_IF_MAIN;

View file

@ -0,0 +1,96 @@
<?php
/**
* @defgroup Benchmark Benchmark
* @ingroup Maintenance
*/
/**
* Trait for scripts generating files based on the config schema.
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
* http://www.gnu.org/copyleft/gpl.html
*
* @file
* @ingroup Config
*/
use MediaWiki\MainConfigSchema;
use MediaWiki\Settings\Source\ReflectionSchemaSource;
/**
* Trait for scripts generating files based on the config schema.
* @ingroup Config
* @since 1.38
*/
trait ConfigSchemaDerivativeTrait {
/**
* Loads the config schema from the MainConfigSchema class.
*
* @return array An associative array with a single key, 'config-schema',
* containing the config schema definition.
*/
private function loadSettingsSource(): array {
$source = new ReflectionSchemaSource( MainConfigSchema::class, true );
$settings = $source->load();
return $settings;
}
/**
* Loads the config schema from the MainConfigSchema class.
*
* @return array the config schema definition.
*/
private function loadSchema(): array {
return $this->loadSettingsSource()['config-schema'];
}
/**
* @param string $defaultPath
* @param string $content
*/
private function writeOutput( $defaultPath, $content ) {
$path = $this->getOutputPath( $defaultPath );
// ensure a single line break at the end of the file
$content = trim( $content ) . "\n";
file_put_contents( $path, $content );
}
/**
* @param string $default The default output path
*
* @return string
*/
private function getOutputPath( $default ): string {
$outputPath = $this->getOption( 'output', $default );
if ( $outputPath === '-' ) {
$outputPath = 'php://stdout';
}
return $outputPath;
}
/**
* Stub for method supplied by the Maintenance base class.
*
* @param string $name The name of the param
* @param mixed|null $default Anything you want, default null
* @return mixed
* @return-taint none
*/
abstract protected function getOption( $name, $default = null );
}

View file

@ -3,12 +3,13 @@
namespace MediaWiki\Tests\Structure;
use ExtensionRegistry;
use MediaWiki\MainConfigSchema;
use MediaWiki\Settings\Config\ArrayConfigBuilder;
use MediaWiki\Settings\Config\PhpIniSink;
use MediaWiki\Settings\SettingsBuilder;
use MediaWiki\Settings\Source\FileSource;
use MediaWiki\Settings\Source\Format\YamlFormat;
use MediaWiki\Settings\Source\PhpSettingsSource;
use MediaWiki\Settings\Source\ReflectionSchemaSource;
use MediaWiki\Settings\Source\SettingsSource;
use MediaWiki\Shell\Shell;
use MediaWikiIntegrationTestCase;
@ -19,21 +20,14 @@ use MediaWikiIntegrationTestCase;
class SettingsTest extends MediaWikiIntegrationTestCase {
/**
* Returns the contents of config-schema.yaml as an array.
* Returns the main configuration schema as a settings array.
*
* @return array
*/
private static function getSchemaData(): array {
static $data = null;
if ( !$data ) {
$file = __DIR__ . '/../../../includes/config-schema.yaml';
$data = file_get_contents( $file );
$yaml = new YamlFormat();
$data = $yaml->decode( $data );
}
return $data;
private function getSchemaData(): array {
$source = new ReflectionSchemaSource( MainConfigSchema::class, true );
$settings = $source->load();
return $settings;
}
/**
@ -90,13 +84,13 @@ class SettingsTest extends MediaWikiIntegrationTestCase {
}
public function provideConfigGeneration() {
yield 'docs/Configuration.md' => [
'script' => __DIR__ . '/../../../maintenance/generateConfigDoc.php',
'expectedFile' => __DIR__ . '/../../../docs/Configuration.md',
yield 'includes/config-schema.php' => [
'script' => MW_INSTALL_PATH . '/maintenance/generateConfigSchemaArray.php',
'expectedFile' => MW_INSTALL_PATH . '/includes/config-schema.php',
];
yield 'incl/Configuration.md' => [
'script' => __DIR__ . '/../../../maintenance/generateConfigSchema.php',
'expectedFile' => __DIR__ . '/../../../includes/config-schema.php',
yield 'docs/config-schema.yaml' => [
'script' => MW_INSTALL_PATH . '/maintenance/generateConfigSchemaYaml.php',
'expectedFile' => MW_INSTALL_PATH . '/docs/config-schema.yaml',
];
}
@ -108,10 +102,13 @@ class SettingsTest extends MediaWikiIntegrationTestCase {
$result = $schemaGenerator->execute();
$this->assertSame( 0, $result->getExitCode(), 'Config generation must finish successfully' );
$this->assertSame( '', $result->getStderr(), 'Config generation must not have errors' );
$errors = $result->getStderr();
$errors = preg_replace( '/^Xdebug:.*\n/m', '', $errors );
$this->assertSame( '', $errors, 'Config generation must not have errors' );
$oldGeneratedSchema = file_get_contents( $expectedFile );
$relativePath = wfRelativePath( $script, __DIR__ . '/../../..' );
$relativePath = wfRelativePath( $script, MW_INSTALL_PATH );
$this->assertEquals(
$oldGeneratedSchema,
@ -121,8 +118,8 @@ class SettingsTest extends MediaWikiIntegrationTestCase {
}
public function provideDefaultSettingsConsistency() {
yield 'YAML' => [ new FileSource( 'includes/config-schema.yaml' ) ];
yield 'PHP' => [ new PhpSettingsSource( 'includes/config-schema.php' ) ];
yield 'YAML' => [ new FileSource( MW_INSTALL_PATH . '/docs/config-schema.yaml' ) ];
yield 'PHP' => [ new PhpSettingsSource( MW_INSTALL_PATH . '/includes/config-schema.php' ) ];
}
/**
@ -132,7 +129,7 @@ class SettingsTest extends MediaWikiIntegrationTestCase {
*/
public function testDefaultSettingsConsistency( SettingsSource $source ) {
$defaultSettingsProps = ( static function () {
require __DIR__ . '/../../../includes/DefaultSettings.php';
require MW_INSTALL_PATH . '/includes/DefaultSettings.php';
$vars = get_defined_vars();
unset( $vars['input'] );
$result = [];
@ -150,7 +147,7 @@ class SettingsTest extends MediaWikiIntegrationTestCase {
$this->createNoOpMock( PhpIniSink::class )
);
$settingsBuilder->load( $source );
$settingsBuilder->apply();
$defaults = iterator_to_array( $settingsBuilder->getDefaultConfig() );
foreach ( $defaultSettingsProps as $key => $value ) {
if ( in_array( $key, [
@ -160,9 +157,12 @@ class SettingsTest extends MediaWikiIntegrationTestCase {
] ) ) {
continue;
}
$this->assertTrue( $configBuilder->build()->has( $key ), "Missing $key" );
$this->assertEquals( $value, $configBuilder->build()->get( $key ), "Wrong value for $key\n" );
$this->assertArrayHasKey( $key, $defaults, "Missing $key from $source" );
$this->assertEquals( $value, $defaults[ $key ], "Wrong value for $key\n" );
}
$missingKeys = array_diff_key( $defaults, $defaultSettingsProps );
$this->assertSame( [], $missingKeys, 'Keys missing from DefaultSettings.php' );
}
public function provideArraysHaveMergeStrategy() {

View file

@ -0,0 +1,44 @@
<?php
namespace MediaWiki\Tests\Unit\Settings\Source;
use MediaWiki\Settings\SettingsBuilderException;
use MediaWiki\Settings\Source\ReflectionSchemaSource;
use PHPUnit\Framework\TestCase;
/**
* @covers \MediaWiki\Settings\Source\ReflectionSchemaSource
*/
class ReflectionSchemaSourceTest extends TestCase {
public const TEST_SCHEMA = [
'type' => 'object'
];
private const TEST_PRIVATE = [
'type' => 'object'
];
public const TEST_STRING = 'test';
public function testLoad() {
$source = new ReflectionSchemaSource( self::class );
$settings = $source->load();
$this->assertArrayHasKey( 'config-schema', $settings );
$this->assertArrayHasKey( 'TEST_SCHEMA', $settings['config-schema'] );
$this->assertArrayHasKey( 'type', $settings['config-schema']['TEST_SCHEMA'] );
$this->assertSame( 'object', $settings['config-schema']['TEST_SCHEMA']['type'] );
$this->assertArrayNotHasKey( 'TEST_PRIVATE', $settings['config-schema'] );
$this->assertArrayNotHasKey( 'TEST_STRING', $settings['config-schema'] );
}
public function testLoadInvalidClass() {
$this->expectException( SettingsBuilderException::class );
$source = new ReflectionSchemaSource( 'ThisClassDoesNotExist' );
$source->load();
}
}