aboutsummaryrefslogtreecommitdiffstats
path: root/includes/Settings
diff options
context:
space:
mode:
authorDan Duvall <dduvall@wikimedia.org>2021-11-11 10:46:32 -0800
committerDan Duvall <dduvall@wikimedia.org>2021-11-15 14:07:59 -0800
commit9a4af2566438e1b2350a02e37046ee2b67a54d88 (patch)
tree55b7098a1c52cff72f29001df3021e450dddc808 /includes/Settings
parent18fddad2ea4dd134738078e88dde8907b12af052 (diff)
downloadmediawikicore-9a4af2566438e1b2350a02e37046ee2b67a54d88.tar.gz
mediawikicore-9a4af2566438e1b2350a02e37046ee2b67a54d88.zip
Introduced settings sources and formats
A `SettingsSource` is meant to represent any kind of local or remote store from which settings can be read, be this a local file, remote URL, database, etc. It is concerned with reading in (and possibly decoding) settings data, and computing a consistent hash key that may be used in caching. A `SettingsFormat` is meant to detect supported file types and/or decode source contents into settings arrays. As of now, JSON is the only supported format but others may be implemented. `FileSource` is the first source implementation, with its default format being JSON, meant to read settings from local JSON files. `ArraySource` is mostly useful for testing using array literals. Refactored `SettingsBuilder` methods to use the new source abstractions. Bug: T295499 Change-Id: If7869609c4ad1ccd0894d5ba358f885007168972
Diffstat (limited to 'includes/Settings')
-rw-r--r--includes/Settings/SettingsBuilder.php81
-rw-r--r--includes/Settings/Source/ArraySource.php25
-rw-r--r--includes/Settings/Source/FileSource.php161
-rw-r--r--includes/Settings/Source/Format/JsonFormat.php57
-rw-r--r--includes/Settings/Source/Format/SettingsFormat.php35
-rw-r--r--includes/Settings/Source/SettingsSource.php25
6 files changed, 341 insertions, 43 deletions
diff --git a/includes/Settings/SettingsBuilder.php b/includes/Settings/SettingsBuilder.php
index 5426220020a1..35cda91a4f1a 100644
--- a/includes/Settings/SettingsBuilder.php
+++ b/includes/Settings/SettingsBuilder.php
@@ -3,6 +3,9 @@
namespace MediaWiki\Settings;
use MediaWiki\Settings\Config\ConfigSink;
+use MediaWiki\Settings\Source\ArraySource;
+use MediaWiki\Settings\Source\FileSource;
+use MediaWiki\Settings\Source\SettingsSource;
/**
* Utility for loading settings files.
@@ -33,30 +36,16 @@ class SettingsBuilder {
}
/**
- * Load settings from a file.
+ * Load settings from a {@link SettingsSource}.
*
* @unstable
*
- * @param string $source
+ * @param SettingsSource $source
* @return $this
*/
- public function loadFile( string $source ): self {
- $newSettings = $this->readSettingsFile( $source );
-
- return $this->loadArray( $newSettings );
- }
+ public function load( SettingsSource $source ): self {
+ $newSettings = $source->load();
- /**
- * Load settings from aa array.
- *
- * @unstable
- *
- * @param array $newSettings
- * @param string $sourceName
- *
- * @return $this
- */
- public function loadArray( array $newSettings, $sourceName = '<array>' ): self {
$this->settings['config'] =
array_merge( $this->settings['config'], $newSettings['config'] ?? [] );
@@ -66,7 +55,7 @@ class SettingsBuilder {
);
if ( !empty( $schemaOverrides ) ) {
throw new SettingsBuilderException( 'Overriding config schema in {source}', [
- 'source' => $sourceName,
+ 'source' => $source,
'override_keys' => implode( ',', array_keys( $schemaOverrides ) ),
] );
}
@@ -77,6 +66,36 @@ class SettingsBuilder {
}
/**
+ * Load settings from an array.
+ *
+ * @unstable
+ *
+ * @param array $newSettings
+ *
+ * @return $this
+ */
+ public function loadArray( array $newSettings ): self {
+ return $this->load( new ArraySource( $newSettings ) );
+ }
+
+ /**
+ * Load settings from a file.
+ *
+ * @unstable
+ *
+ * @param string $path
+ * @return $this
+ */
+ public function loadFile( string $path ): self {
+ // Qualify the path if it isn't already absolute
+ if ( !preg_match( '!^[a-zA-Z]:\\\\!', $path ) && $path[0] != DIRECTORY_SEPARATOR ) {
+ $path = $this->baseDir . DIRECTORY_SEPARATOR . $path;
+ }
+
+ return $this->load( new FileSource( $path ) );
+ }
+
+ /**
* Apply any settings loaded so far to the runtime environment.
*
* @unstable
@@ -104,28 +123,4 @@ class SettingsBuilder {
'config-schema' => [],
];
}
-
- /**
- * @param string $path
- *
- * @return array
- */
- private function readSettingsFile( string $path ): array {
- $fullPath = $this->baseDir . '/' . $path;
-
- if ( !file_exists( $fullPath ) ) {
- throw new SettingsBuilderException( "settings file '{path}' does not exist", [
- 'path' => $fullPath
- ] );
- }
-
- $json = file_get_contents( $fullPath );
- $newSettings = json_decode( $json, true );
-
- if ( !is_array( $newSettings ) ) {
- throw new SettingsBuilderException( "failed to decode JSON from '$fullPath'" );
- }
-
- return $newSettings;
- }
}
diff --git a/includes/Settings/Source/ArraySource.php b/includes/Settings/Source/ArraySource.php
new file mode 100644
index 000000000000..a7a1cdfbf66f
--- /dev/null
+++ b/includes/Settings/Source/ArraySource.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace MediaWiki\Settings\Source;
+
+/**
+ * Settings loaded from an array.
+ *
+ * @since 1.38
+ */
+class ArraySource implements SettingsSource {
+
+ private $settings;
+
+ public function __construct( array $settings ) {
+ $this->settings = $settings;
+ }
+
+ public function load(): array {
+ return $this->settings;
+ }
+
+ public function __toString(): string {
+ return '<array>';
+ }
+}
diff --git a/includes/Settings/Source/FileSource.php b/includes/Settings/Source/FileSource.php
new file mode 100644
index 000000000000..2320f377049b
--- /dev/null
+++ b/includes/Settings/Source/FileSource.php
@@ -0,0 +1,161 @@
+<?php
+
+namespace MediaWiki\Settings\Source;
+
+use MediaWiki\Settings\SettingsBuilderException;
+use MediaWiki\Settings\Source\Format\JsonFormat;
+use MediaWiki\Settings\Source\Format\SettingsFormat;
+use UnexpectedValueException;
+use Wikimedia\AtEase\AtEase;
+
+/**
+ * Settings loaded from a local file path.
+ *
+ * @since 1.38
+ */
+class FileSource implements SettingsSource {
+ /**
+ * Default format with which to attempt decoding if none are given to the
+ * constructor.
+ */
+ private const DEFAULT_FORMAT = JsonFormat::class;
+
+ /**
+ * Possible formats.
+ * @var array
+ */
+ private $formats;
+
+ /**
+ * Path to local file.
+ * @var string
+ */
+ private $path;
+
+ /**
+ * Constructs a new FileSource for the given path and possible matching
+ * formats. The first format to match the path's file extension will be
+ * used to decode the content.
+ *
+ * An end-user caller may be explicit about the given path's format by
+ * providing only one format.
+ *
+ * <code>
+ * <?php
+ * $source = new FileSource( 'my/settings.json', new JsonFormat() );
+ * $source->load();
+ * </code>
+ *
+ * While a generalized caller may want to pass a number of supported
+ * formats.
+ *
+ * <code>
+ * <?php
+ * function loadAllPossibleFormats( string $path ) {
+ * $source = new FileSource(
+ * $path,
+ * new JsonFormat(),
+ * new YamlFormat(),
+ * new TomlFormat()
+ * )
+ * }
+ * </code>
+ *
+ * @param string $path
+ * @param SettingsFormat ...$formats
+ */
+ public function __construct( string $path, SettingsFormat ...$formats ) {
+ $this->path = $path;
+ $this->formats = $formats;
+
+ if ( empty( $this->formats ) ) {
+ $class = self::DEFAULT_FORMAT;
+ $this->formats = [ new $class() ];
+ }
+ }
+
+ /**
+ * Loads contents from the file and decodes them using the first format
+ * to claim support for the file's extension.
+ *
+ * @throws SettingsBuilderException
+ * @return array
+ */
+ public function load(): array {
+ $ext = pathinfo( $this->path, PATHINFO_EXTENSION );
+
+ // If there's only one format, don't bother to match the file
+ // extension.
+ if ( count( $this->formats ) == 1 ) {
+ return $this->readAndDecode( $this->formats[0] );
+ }
+
+ foreach ( $this->formats as $format ) {
+ if ( $format->supportsFileExtension( $ext ) ) {
+ return $this->readAndDecode( $format );
+ }
+ }
+
+ throw new SettingsBuilderException(
+ "None of the given formats ({formats}) are suitable for '{path}'",
+ [
+ 'formats' => implode( ', ', $this->formats ),
+ 'path' => $this->path,
+ ]
+ );
+ }
+
+ /**
+ * Returns this file source as a string.
+ *
+ * @return string
+ */
+ public function __toString(): string {
+ return $this->path;
+ }
+
+ /**
+ * Reads and decodes the file contents using the given format.
+ *
+ * @param SettingsFormat $format
+ *
+ * @return array
+ * @throws SettingsBuilderException
+ */
+ private function readAndDecode( SettingsFormat $format ): array {
+ $contents = AtEase::quietCall( 'file_get_contents', $this->path );
+
+ if ( $contents === false ) {
+ if ( !is_readable( $this->path ) ) {
+ throw new SettingsBuilderException(
+ "File '{path}' is not readable",
+ [ 'path' => $this->path ]
+ );
+ }
+
+ if ( is_dir( $this->path ) ) {
+ throw new SettingsBuilderException(
+ "'{path}' is a directory, not a file",
+ [ 'path' => $this->path ]
+ );
+ }
+
+ throw new SettingsBuilderException(
+ "Failed to read file '{path}'",
+ [ 'path' => $this->path ]
+ );
+ }
+
+ try {
+ return $format->decode( $contents );
+ } catch ( UnexpectedValueException $e ) {
+ throw new SettingsBuilderException(
+ "Failed to decode file '{path}': {message}",
+ [
+ 'path' => $this->path,
+ 'message' => $e->getMessage()
+ ]
+ );
+ }
+ }
+}
diff --git a/includes/Settings/Source/Format/JsonFormat.php b/includes/Settings/Source/Format/JsonFormat.php
new file mode 100644
index 000000000000..1151ab93a2cb
--- /dev/null
+++ b/includes/Settings/Source/Format/JsonFormat.php
@@ -0,0 +1,57 @@
+<?php
+
+namespace MediaWiki\Settings\Source\Format;
+
+use UnexpectedValueException;
+
+/**
+ * Decodes settings data from JSON.
+ */
+class JsonFormat implements SettingsFormat {
+
+ /**
+ * Decodes JSON.
+ *
+ * @param string $data JSON string to decode.
+ *
+ * @return array
+ * @throws UnexpectedValueException
+ */
+ public function decode( string $data ): array {
+ $settings = json_decode( $data, true );
+
+ if ( $settings === null ) {
+ throw new UnexpectedValueException(
+ 'Failed to decode JSON: ' . json_last_error_msg()
+ );
+ }
+
+ if ( !is_array( $settings ) ) {
+ throw new UnexpectedValueException(
+ 'Decoded settings must be an array'
+ );
+ }
+
+ return $settings;
+ }
+
+ /**
+ * Returns true for the file extension 'json'. Case insensitive.
+ *
+ * @param string $ext File extension.
+ *
+ * @return bool
+ */
+ public function supportsFileExtension( string $ext ): bool {
+ return strtolower( $ext ) == 'json';
+ }
+
+ /**
+ * Returns the name/type of this format (JSON).
+ *
+ * @return string
+ */
+ public function __toString(): string {
+ return 'JSON';
+ }
+}
diff --git a/includes/Settings/Source/Format/SettingsFormat.php b/includes/Settings/Source/Format/SettingsFormat.php
new file mode 100644
index 000000000000..943faa88d544
--- /dev/null
+++ b/includes/Settings/Source/Format/SettingsFormat.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace MediaWiki\Settings\Source\Format;
+
+use Stringable;
+use UnexpectedValueException;
+
+/**
+ * A SettingsFormat is meant to detect supported file types and/or decode
+ * source contents into settings arrays.
+ *
+ * @since 1.38
+ * @todo mark as stable before the 1.38 release
+ */
+interface SettingsFormat extends Stringable {
+ /**
+ * Decodes the given settings data and returns an associative array.
+ *
+ * @param string $data Settings data.
+ *
+ * @return array
+ * @throws UnexpectedValueException
+ */
+ public function decode( string $data ): array;
+
+ /**
+ * Whether or not the format claims to support a file with the given
+ * extension.
+ *
+ * @param string $ext File extension.
+ *
+ * @return bool
+ */
+ public function supportsFileExtension( string $ext ): bool;
+}
diff --git a/includes/Settings/Source/SettingsSource.php b/includes/Settings/Source/SettingsSource.php
new file mode 100644
index 000000000000..31df14fefde1
--- /dev/null
+++ b/includes/Settings/Source/SettingsSource.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace MediaWiki\Settings\Source;
+
+use MediaWiki\Settings\SettingsBuilderException;
+use Stringable;
+
+/**
+ * A SettingsSource is meant to represent any kind of local or remote store
+ * from which settings can be read, be it a local file, remote URL, database,
+ * etc. It is concerned with reading (and possibly decoding) settings data.
+ *
+ * @since 1.38
+ * @todo mark as stable before the 1.38 release
+ */
+interface SettingsSource extends Stringable {
+ /**
+ * Loads and returns all settings from this source as an associative
+ * array.
+ *
+ * @return array
+ * @throws SettingsBuilderException
+ */
+ public function load(): array;
+}