* load(); * * * While a specialized caller may want to pass a specialized format * * * load(); * * * @param string $path * @param SettingsFormat|null $format */ public function __construct( string $path, ?SettingsFormat $format = null ) { $this->path = $path; $this->format = $format; } /** * Disallow stale results from file sources in the case of load failure as * failing to read from disk would be quite catastrophic and worthy of * propagation. */ public function allowsStaleLoad(): bool { return false; } /** * 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 ( $this->format ) { return $this->readAndDecode( $this->format ); } foreach ( self::BUILT_IN_FORMATS as $format ) { if ( $format::supportsFileExtension( $ext ) ) { return $this->readAndDecode( new $format() ); } } throw new SettingsBuilderException( "None of the built-in formats are suitable for '{path}'", [ 'path' => $this->path, ] ); } /** * The cache expiry TTL (in seconds) for this file source. */ public function getExpiryTtl(): int { return self::EXPIRY_TTL; } /** * Coefficient used in determining early expiration of cached settings to * avoid stampedes. */ public function getExpiryWeight(): float { return self::EXPIRY_WEIGHT; } /** * Returns a hash key computed from the file's inode, size, and last * modified timestamp. */ public function getHashKey(): string { $stat = stat( $this->path ); if ( $stat === false ) { throw new SettingsBuilderException( "Failed to stat file '{path}'", [ 'path' => $this->path ] ); } return sprintf( '%x-%x-%x', $stat['ino'], $stat['size'], $stat['mtime'] ); } /** * Returns this file source as a 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() ] ); } } public function locateInclude( string $location ): string { return SettingsFileUtils::resolveRelativeLocation( $location, dirname( $this->path ) ); } }