class JsonFile
Reads/writes json files.
@author Konstantin Kudryashiv <ever.zet@gmail.com> @author Jordi Boggiano <j.boggiano@seld.be>
Hierarchy
- class \Composer\Json\JsonFile
Expanded class hierarchy of JsonFile
35 files declare their use of JsonFile
- ArchiveManager.php in vendor/
composer/ composer/ src/ Composer/ Package/ Archiver/ ArchiveManager.php - ArtifactRepository.php in vendor/
composer/ composer/ src/ Composer/ Repository/ ArtifactRepository.php - Auditor.php in vendor/
composer/ composer/ src/ Composer/ Advisory/ Auditor.php - AutoloadGenerator.php in vendor/
composer/ composer/ src/ Composer/ Autoload/ AutoloadGenerator.php - BumpCommand.php in vendor/
composer/ composer/ src/ Composer/ Command/ BumpCommand.php
File
-
vendor/
composer/ composer/ src/ Composer/ Json/ JsonFile.php, line 30
Namespace
Composer\JsonView source
class JsonFile {
public const LAX_SCHEMA = 1;
public const STRICT_SCHEMA = 2;
public const AUTH_SCHEMA = 3;
public const LOCK_SCHEMA = 4;
/** @deprecated Use \JSON_UNESCAPED_SLASHES */
public const JSON_UNESCAPED_SLASHES = 64;
/** @deprecated Use \JSON_PRETTY_PRINT */
public const JSON_PRETTY_PRINT = 128;
/** @deprecated Use \JSON_UNESCAPED_UNICODE */
public const JSON_UNESCAPED_UNICODE = 256;
public const COMPOSER_SCHEMA_PATH = __DIR__ . '/../../../res/composer-schema.json';
public const LOCK_SCHEMA_PATH = __DIR__ . '/../../../res/composer-lock-schema.json';
public const INDENT_DEFAULT = ' ';
/** @var string */
private $path;
/** @var ?HttpDownloader */
private $httpDownloader;
/** @var ?IOInterface */
private $io;
/** @var string */
private $indent = self::INDENT_DEFAULT;
/**
* Initializes json file reader/parser.
*
* @param string $path path to a lockfile
* @param ?HttpDownloader $httpDownloader required for loading http/https json files
* @param ?IOInterface $io
* @throws \InvalidArgumentException
*/
public function __construct(string $path, ?HttpDownloader $httpDownloader = null, ?IOInterface $io = null) {
$this->path = $path;
if (null === $httpDownloader && Preg::isMatch('{^https?://}i', $path)) {
throw new \InvalidArgumentException('http urls require a HttpDownloader instance to be passed');
}
$this->httpDownloader = $httpDownloader;
$this->io = $io;
}
public function getPath() : string {
return $this->path;
}
/**
* Checks whether json file exists.
*/
public function exists() : bool {
return is_file($this->path);
}
/**
* Reads json file.
*
* @throws ParsingException
* @throws \RuntimeException
* @return mixed
*/
public function read() {
try {
if ($this->httpDownloader) {
$json = $this->httpDownloader
->get($this->path)
->getBody();
}
else {
if (!Filesystem::isReadable($this->path)) {
throw new \RuntimeException('The file "' . $this->path . '" is not readable.');
}
if ($this->io && $this->io
->isDebug()) {
$realpathInfo = '';
$realpath = realpath($this->path);
if (false !== $realpath && $realpath !== $this->path) {
$realpathInfo = ' (' . $realpath . ')';
}
$this->io
->writeError('Reading ' . $this->path . $realpathInfo);
}
$json = file_get_contents($this->path);
}
} catch (TransportException $e) {
throw new \RuntimeException($e->getMessage(), 0, $e);
} catch (\Exception $e) {
throw new \RuntimeException('Could not read ' . $this->path . "\n\n" . $e->getMessage());
}
if ($json === false) {
throw new \RuntimeException('Could not read ' . $this->path);
}
$this->indent = self::detectIndenting($json);
return static::parseJson($json, $this->path);
}
/**
* Writes json file.
*
* @param mixed[] $hash writes hash into json file
* @param int $options json_encode options
* @throws \UnexpectedValueException|\Exception
* @return void
*/
public function write(array $hash, int $options = JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) {
if ($this->path === 'php://memory') {
file_put_contents($this->path, static::encode($hash, $options, $this->indent));
return;
}
$dir = dirname($this->path);
if (!is_dir($dir)) {
if (file_exists($dir)) {
throw new \UnexpectedValueException(realpath($dir) . ' exists and is not a directory.');
}
if (!@mkdir($dir, 0777, true)) {
throw new \UnexpectedValueException($dir . ' does not exist and could not be created.');
}
}
$retries = 3;
while ($retries--) {
try {
$this->filePutContentsIfModified($this->path, static::encode($hash, $options, $this->indent) . ($options & JSON_PRETTY_PRINT ? "\n" : ''));
break;
} catch (\Exception $e) {
if ($retries > 0) {
usleep(500000);
continue;
}
throw $e;
}
}
}
/**
* Modify file properties only if content modified
*
* @return int|false
*/
private function filePutContentsIfModified(string $path, string $content) {
$currentContent = @file_get_contents($path);
if (false === $currentContent || $currentContent !== $content) {
return file_put_contents($path, $content);
}
return 0;
}
/**
* Validates the schema of the current json file according to composer-schema.json rules
*
* @param int $schema a JsonFile::*_SCHEMA constant
* @param string|null $schemaFile a path to the schema file
* @throws JsonValidationException
* @throws ParsingException
* @return true true on success
*
* @phpstan-param self::*_SCHEMA $schema
*/
public function validateSchema(int $schema = self::STRICT_SCHEMA, ?string $schemaFile = null) : bool {
if (!Filesystem::isReadable($this->path)) {
throw new \RuntimeException('The file "' . $this->path . '" is not readable.');
}
$content = file_get_contents($this->path);
$data = json_decode($content);
if (null === $data && 'null' !== $content) {
self::validateSyntax($content, $this->path);
}
return self::validateJsonSchema($this->path, $data, $schema, $schemaFile);
}
/**
* Validates the schema of the current json file according to composer-schema.json rules
*
* @param mixed $data Decoded JSON data to validate
* @param int $schema a JsonFile::*_SCHEMA constant
* @param string|null $schemaFile a path to the schema file
* @throws JsonValidationException
* @return true true on success
*
* @phpstan-param self::*_SCHEMA $schema
*/
public static function validateJsonSchema(string $source, $data, int $schema, ?string $schemaFile = null) : bool {
$isComposerSchemaFile = false;
if (null === $schemaFile) {
if ($schema === self::LOCK_SCHEMA) {
$schemaFile = self::LOCK_SCHEMA_PATH;
}
else {
$isComposerSchemaFile = true;
$schemaFile = self::COMPOSER_SCHEMA_PATH;
}
}
// Prepend with file:// only when not using a special schema already (e.g. in the phar)
if (false === strpos($schemaFile, '://')) {
$schemaFile = 'file://' . $schemaFile;
}
$schemaData = (object) [
'$ref' => $schemaFile,
];
if ($schema === self::LAX_SCHEMA) {
$schemaData->additionalProperties = true;
$schemaData->required = [];
}
elseif ($schema === self::STRICT_SCHEMA && $isComposerSchemaFile) {
$schemaData->additionalProperties = false;
$schemaData->required = [
'name',
'description',
];
}
elseif ($schema === self::AUTH_SCHEMA && $isComposerSchemaFile) {
$schemaData = (object) [
'$ref' => $schemaFile . '#/properties/config',
'$schema' => "https://json-schema.org/draft-04/schema#",
];
}
$validator = new Validator();
$validator->check($data, $schemaData);
if (!$validator->isValid()) {
$errors = [];
foreach ((array) $validator->getErrors() as $error) {
$errors[] = ($error['property'] ? $error['property'] . ' : ' : '') . $error['message'];
}
throw new JsonValidationException('"' . $source . '" does not match the expected JSON schema', $errors);
}
return true;
}
/**
* Encodes an array into (optionally pretty-printed) JSON
*
* @param mixed $data Data to encode into a formatted JSON string
* @param int $options json_encode options (defaults to JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)
* @param string $indent Indentation string
* @return string Encoded json
*/
public static function encode($data, int $options = 448, string $indent = self::INDENT_DEFAULT) : string {
$json = json_encode($data, $options);
if (false === $json) {
self::throwEncodeError(json_last_error());
}
if (($options & JSON_PRETTY_PRINT) > 0 && $indent !== self::INDENT_DEFAULT) {
// Pretty printing and not using default indentation
return Preg::replaceCallback('#^ {4,}#m', static function ($match) use ($indent) : string {
return str_repeat($indent, (int) (strlen($match[0]) / 4));
}, $json);
}
return $json;
}
/**
* Throws an exception according to a given code with a customized message
*
* @param int $code return code of json_last_error function
* @throws \RuntimeException
* @return never
*/
private static function throwEncodeError(int $code) : void {
switch ($code) {
case JSON_ERROR_DEPTH:
$msg = 'Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = 'Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = 'Unexpected control character found';
break;
case JSON_ERROR_UTF8:
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$msg = 'Unknown error';
}
throw new \RuntimeException('JSON encoding failed: ' . $msg);
}
/**
* Parses json string and returns hash.
*
* @param null|string $json json string
* @param string $file the json file
*
* @throws ParsingException
* @return mixed
*/
public static function parseJson(?string $json, ?string $file = null) {
if (null === $json) {
return null;
}
$data = json_decode($json, true);
if (null === $data && JSON_ERROR_NONE !== json_last_error()) {
self::validateSyntax($json, $file);
}
return $data;
}
/**
* Validates the syntax of a JSON string
*
* @param string $file
* @throws \UnexpectedValueException
* @throws ParsingException
* @return bool true on success
*/
protected static function validateSyntax(string $json, ?string $file = null) : bool {
$parser = new JsonParser();
$result = $parser->lint($json);
if (null === $result) {
if (defined('JSON_ERROR_UTF8') && JSON_ERROR_UTF8 === json_last_error()) {
if ($file === null) {
throw new \UnexpectedValueException('The input is not UTF-8, could not parse as JSON');
}
else {
throw new \UnexpectedValueException('"' . $file . '" is not UTF-8, could not parse as JSON');
}
}
return true;
}
if ($file === null) {
throw new ParsingException('The input does not contain valid JSON' . "\n" . $result->getMessage(), $result->getDetails());
}
else {
throw new ParsingException('"' . $file . '" does not contain valid JSON' . "\n" . $result->getMessage(), $result->getDetails());
}
}
public static function detectIndenting(?string $json) : string {
if (Preg::isMatchStrictGroups('#^([ \\t]+)"#m', $json ?? '', $match)) {
return $match[1];
}
return self::INDENT_DEFAULT;
}
}
Members
Title Sort descending | Deprecated | Modifiers | Object type | Summary |
---|---|---|---|---|
JsonFile::$httpDownloader | private | property | @var ?HttpDownloader | |
JsonFile::$indent | private | property | @var string | |
JsonFile::$io | private | property | @var ?IOInterface | |
JsonFile::$path | private | property | @var string | |
JsonFile::AUTH_SCHEMA | public | constant | ||
JsonFile::COMPOSER_SCHEMA_PATH | public | constant | ||
JsonFile::detectIndenting | public static | function | ||
JsonFile::encode | public static | function | Encodes an array into (optionally pretty-printed) JSON | |
JsonFile::exists | public | function | Checks whether json file exists. | |
JsonFile::filePutContentsIfModified | private | function | Modify file properties only if content modified | |
JsonFile::getPath | public | function | ||
JsonFile::INDENT_DEFAULT | public | constant | ||
JsonFile::JSON_PRETTY_PRINT | Deprecated | public | constant | |
JsonFile::JSON_UNESCAPED_SLASHES | Deprecated | public | constant | |
JsonFile::JSON_UNESCAPED_UNICODE | Deprecated | public | constant | |
JsonFile::LAX_SCHEMA | public | constant | ||
JsonFile::LOCK_SCHEMA | public | constant | ||
JsonFile::LOCK_SCHEMA_PATH | public | constant | ||
JsonFile::parseJson | public static | function | Parses json string and returns hash. | |
JsonFile::read | public | function | Reads json file. | |
JsonFile::STRICT_SCHEMA | public | constant | ||
JsonFile::throwEncodeError | private static | function | Throws an exception according to a given code with a customized message | |
JsonFile::validateJsonSchema | public static | function | Validates the schema of the current json file according to composer-schema.json rules | |
JsonFile::validateSchema | public | function | Validates the schema of the current json file according to composer-schema.json rules | |
JsonFile::validateSyntax | protected static | function | Validates the syntax of a JSON string | |
JsonFile::write | public | function | Writes json file. | |
JsonFile::__construct | public | function | Initializes json file reader/parser. |