* @see README.md */ class RefResolver { /** * HACK to prevent too many recursive expansions. * Happens e.g. when you want to validate a schema against the schema * definition. * * @var integer */ protected static $depth = 0; /** * maximum references depth * @var integer */ public static $maxDepth = 7; /** * @var UriRetrieverInterface */ protected $uriRetriever = null; /** * @var object */ protected $rootSchema = null; /** * @param UriRetriever $retriever */ public function __construct($retriever = null) { $this->uriRetriever = $retriever; } /** * Retrieves a given schema given a ref and a source URI * * @param string $ref Reference from schema * @param string $sourceUri URI where original schema was located * @return object Schema */ public function fetchRef($ref, $sourceUri) { $retriever = $this->getUriRetriever(); $jsonSchema = $retriever->retrieve($ref, $sourceUri); $this->resolve($jsonSchema); return $jsonSchema; } /** * Return the URI Retriever, defaulting to making a new one if one * was not yet set. * * @return UriRetriever */ public function getUriRetriever() { if (is_null($this->uriRetriever)) { $this->setUriRetriever(new UriRetriever); } return $this->uriRetriever; } /** * Resolves all $ref references for a given schema. Recurses through * the object to resolve references of any child schemas. * * The 'format' property is omitted because it isn't required for * validation. Theoretically, this class could be extended to look * for URIs in formats: "These custom formats MAY be expressed as * an URI, and this URI MAY reference a schema of that format." * * The 'id' property is not filled in, but that could be made to happen. * * @param object $schema JSON Schema to flesh out * @param string $sourceUri URI where this schema was located */ public function resolve($schema, $sourceUri = null) { if (self::$depth > self::$maxDepth) { self::$depth = 0; throw new JsonDecodingException(JSON_ERROR_DEPTH); } ++self::$depth; if (! is_object($schema)) { --self::$depth; return; } if (null === $sourceUri && ! empty($schema->id)) { $sourceUri = $schema->id; } if (null === $this->rootSchema) { $this->rootSchema = $schema; } // Resolve $ref first $this->resolveRef($schema, $sourceUri); // These properties are just schemas // eg. items can be a schema or an array of schemas foreach (array('additionalItems', 'additionalProperties', 'extends', 'items') as $propertyName) { $this->resolveProperty($schema, $propertyName, $sourceUri); } // These are all potentially arrays that contain schema objects // eg. type can be a value or an array of values/schemas // eg. items can be a schema or an array of schemas foreach (array('disallow', 'extends', 'items', 'type', 'allOf', 'anyOf', 'oneOf') as $propertyName) { $this->resolveArrayOfSchemas($schema, $propertyName, $sourceUri); } // These are all objects containing properties whose values are schemas foreach (array('dependencies', 'patternProperties', 'properties') as $propertyName) { $this->resolveObjectOfSchemas($schema, $propertyName, $sourceUri); } --self::$depth; } /** * Given an object and a property name, that property should be an * array whose values can be schemas. * * @param object $schema JSON Schema to flesh out * @param string $propertyName Property to work on * @param string $sourceUri URI where this schema was located */ public function resolveArrayOfSchemas($schema, $propertyName, $sourceUri) { if (! isset($schema->$propertyName) || ! is_array($schema->$propertyName)) { return; } foreach ($schema->$propertyName as $possiblySchema) { $this->resolve($possiblySchema, $sourceUri); } } /** * Given an object and a property name, that property should be an * object whose properties are schema objects. * * @param object $schema JSON Schema to flesh out * @param string $propertyName Property to work on * @param string $sourceUri URI where this schema was located */ public function resolveObjectOfSchemas($schema, $propertyName, $sourceUri) { if (! isset($schema->$propertyName) || ! is_object($schema->$propertyName)) { return; } foreach (get_object_vars($schema->$propertyName) as $possiblySchema) { $this->resolve($possiblySchema, $sourceUri); } } /** * Given an object and a property name, that property should be a * schema object. * * @param object $schema JSON Schema to flesh out * @param string $propertyName Property to work on * @param string $sourceUri URI where this schema was located */ public function resolveProperty($schema, $propertyName, $sourceUri) { if (! isset($schema->$propertyName)) { return; } $this->resolve($schema->$propertyName, $sourceUri); } /** * Look for the $ref property in the object. If found, remove the * reference and augment this object with the contents of another * schema. * * @param object $schema JSON Schema to flesh out * @param string $sourceUri URI where this schema was located */ public function resolveRef($schema, $sourceUri) { $ref = '$ref'; if (empty($schema->$ref)) { return; } $splitRef = explode('#', $schema->$ref, 2); $refDoc = $splitRef[0]; $refPath = null; if (count($splitRef) === 2) { $refPath = explode('/', $splitRef[1]); array_shift($refPath); } if (empty($refDoc) && empty($refPath)) { // TODO: Not yet implemented - root pointer ref, causes recursion issues return; } if (!empty($refDoc)) { $refSchema = $this->fetchRef($refDoc, $sourceUri); } else { $refSchema = $this->rootSchema; } if (null !== $refPath) { $refSchema = $this->resolveRefSegment($refSchema, $refPath); } unset($schema->$ref); // Augment the current $schema object with properties fetched foreach (get_object_vars($refSchema) as $prop => $value) { $schema->$prop = $value; } } /** * Set URI Retriever for use with the Ref Resolver * * @param UriRetriever $retriever * @return $this for chaining */ public function setUriRetriever(UriRetriever $retriever) { $this->uriRetriever = $retriever; return $this; } protected function resolveRefSegment($data, $pathParts) { foreach ($pathParts as $path) { $path = strtr($path, array('~1' => '/', '~0' => '~', '%25' => '%')); if (is_array($data)) { $data = $data[$path]; } else { $data = $data->{$path}; } } return $data; } }