diff options
Diffstat (limited to 'includes/Settings/Source/FileSource.php')
-rw-r--r-- | includes/Settings/Source/FileSource.php | 161 |
1 files changed, 161 insertions, 0 deletions
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() + ] + ); + } + } +} |