sender = $sender; $this->rootData = $data; $this->loader = $loader; $this->parent = $parent; $this->globals = $globals; $this->slots = null; $this->maxErrors = $max_errors; $this->currentData = [ [$data, false], ]; if ($slots) { $this->setSlots($slots); } } /** * @param $data * @param Schema|null $sender * @param array|null $globals * @param array|null $slots * @param int|null $max_errors * @return self */ public function newInstance( $data, ?Schema $sender, ?array $globals = null, ?array $slots = null, ?int $max_errors = null ): self { return new self($data, $this->loader, $this, $sender, $globals ?? $this->globals, $slots ?? $this->slots, $max_errors ?? $this->maxErrors); } public function create( Schema $sender, ?Variables $mapper = null, ?Variables $globals = null, ?array $slots = null, ?int $maxErrors = null ): self { if ($globals) { $globals = $globals->resolve($this->rootData(), $this->currentDataPath()); if (!is_array($globals)) { $globals = (array)$globals; } $globals += $this->globals; } else { $globals = $this->globals; } if ($mapper) { $data = $mapper->resolve($this->rootData(), $this->currentDataPath()); } else { $data = $this->currentData(); } return new self($data, $this->loader, $this, $sender, $globals, $slots ?? $this->slots, $maxErrors ?? $this->maxErrors); } public function sender(): ?Schema { return $this->sender; } /** * @return self|null */ public function parent(): ?self { return $this->parent; } /** * @return SchemaLoader */ public function loader(): SchemaLoader { return $this->loader; } /** * @return mixed */ public function rootData() { return $this->rootData; } /** * @return mixed */ public function currentData() { return $this->currentData[$this->pathIndex][0]; } /** * @param $value */ public function setCurrentData($value): void { $this->currentData[$this->pathIndex][0] = $value; $this->currentData[$this->pathIndex][1] = false; } /** * @return string|null */ public function currentDataType(): ?string { $type = $this->currentData[$this->pathIndex][1]; if ($type === false) { $type = Helper::getJsonType($this->currentData[$this->pathIndex][0]); $this->currentData[$this->pathIndex][1] = $type; } return $type; } public function fullDataPath(): array { if ($this->fullPath === null) { if ($this->parent === null) { return $this->currentDataPath; } $this->fullPath = array_merge($this->parent->fullDataPath(), $this->currentDataPath); } return $this->fullPath; } /** * @return int[]|string[] */ public function currentDataPath(): array { return $this->currentDataPath; } /** * @param string|int $key * @return $this */ public function pushDataPath($key): self { $this->currentDataPath[] = $key; if ($this->fullPath !== null) { $this->fullPath[] = $key; } $data = $this->currentData[$this->pathIndex][0]; if (is_array($data)) { $data = $data[$key] ?? null; } elseif (is_object($data)) { $data = $data->{$key} ?? null; } else { $data = null; } $this->currentData[] = [$data, false]; $this->pathIndex++; return $this; } /** * @return $this */ public function popDataPath(): self { if ($this->pathIndex < 1) { return $this; } if ($this->fullPath !== null) { array_pop($this->fullPath); } array_pop($this->currentDataPath); array_pop($this->currentData); $this->pathIndex--; return $this; } /** * @return array */ public function globals(): array { return $this->globals; } /** * @param array $globals * @param bool $overwrite * @return $this */ public function setGlobals(array $globals, bool $overwrite = false): self { if ($overwrite) { $this->globals = $globals; } elseif ($globals) { $this->globals = $globals + $this->globals; } return $this; } /** * @return object[]|Schema[]|string[]|null */ public function slots(): ?array { return $this->slots; } /** * @param array|null $slots * @return $this */ public function setSlots(?array $slots): self { if ($slots) { $list = []; foreach ($slots as $name => $value) { if (is_bool($value)) { $value = $this->loader->loadBooleanSchema($value); } elseif (is_object($value)) { if ($value instanceof Schema) { $list[$name] = $value; continue; } $value = $this->loader->loadObjectSchema($value); } elseif (is_string($value)) { if (isset($this->slots[$value])) { $value = $this->slots[$value]; } elseif ($this->parent) { $value = $this->parent->slot($value); } } if ($value instanceof Schema) { $list[$name] = $value; } } $this->slots = $list; } else { $this->slots = null; } return $this; } /** * @param string $name * @return Schema|null */ public function slot(string $name): ?Schema { return $this->slots[$name] ?? null; } /** * @return int */ public function maxErrors(): int { return $this->maxErrors; } /** * @param int $max * @return $this */ public function setMaxErrors(int $max): self { $this->maxErrors = $max; return $this; } /* --------------------- */ /** * @param Schema $schema * @return $this */ public function pushSharedObject(Schema $schema): self { $unevaluated = !in_array($schema->info()->draft(), ['06', '07']); if ($unevaluated && ($parser = $this->loader->parser()) && !$parser->option('allowUnevaluated', true)) { $unevaluated = false; } $this->shared[] = [ 'schema' => $schema, 'unevaluated' => $unevaluated, 'object' => null, ]; $this->sharedIndex++; return $this; } /** * @return $this */ public function popSharedObject(): self { if ($this->sharedIndex < 0) { return $this; } $data = array_pop($this->shared); $this->sharedIndex--; if ($data['unevaluated'] && $data['object']) { if ($this->sharedIndex >= 0) { $this->mergeUnevaluated($data['object']); } elseif ($this->parent && $this->parent->sharedIndex >= 0) { $this->parent->mergeUnevaluated($data['object']); } } return $this; } /** * @return object|null */ public function sharedObject(): ?object { if ($this->sharedIndex < 0) { return null; } return $this->shared[$this->sharedIndex]['object'] ??= (object)[]; } public function schema(): ?Schema { return $this->shared[$this->sharedIndex]['schema'] ?? null; } public function trackUnevaluated(): bool { return $this->shared[$this->sharedIndex]['unevaluated'] ?? false; } protected function mergeUnevaluated(object $obj): void { switch ($this->currentDataType()) { case 'object': if (isset($obj->evaluatedProperties)) { $this->addEvaluatedProperties($obj->evaluatedProperties); } break; case 'array': if (isset($obj->evaluatedItems)) { $this->addEvaluatedItems($obj->evaluatedItems); } break; } } /* ----------------*/ public function getStringLength(): ?int { if ($this->currentDataType() !== 'string') { return null; } $shared = $this->sharedObject(); if (!isset($shared->stringLength)) { $shared->stringLength = UnicodeString::from($this->currentData())->length(); } return $shared->stringLength; } public function setDecodedContent(string $content): bool { if ($this->currentDataType() !== 'string') { return false; } $this->sharedObject()->decodedContent = $content; return true; } public function getDecodedContent(): ?string { if ($this->currentDataType() !== 'string') { return null; } return $this->sharedObject()->decodedContent ?? $this->currentData(); } public function getObjectProperties(): ?array { if ($this->currentDataType() !== 'object') { return null; } return $this->sharedObject()->objectProperties ??= array_keys(get_object_vars($this->currentData())); } public function addCheckedProperties(?array $properties): bool { if (!$properties) { return false; } $shared = $this->sharedObject(); if (!isset($shared->checkedProperties)) { $shared->checkedProperties = $properties; } else { $shared->checkedProperties = array_values(array_unique(array_merge($shared->checkedProperties, $properties))); } return true; } public function getCheckedProperties(): ?array { return $this->sharedObject()->checkedProperties ?? null; } public function getUncheckedProperties(): ?array { $properties = $this->getObjectProperties(); if (!$properties) { return $properties; } $checked = $this->sharedObject()->checkedProperties ?? null; if (!$checked) { return $properties; } return array_values(array_diff($properties, $checked)); } public function markAllAsEvaluatedProperties(): bool { return $this->addEvaluatedProperties($this->getObjectProperties()); } public function addEvaluatedProperties(?array $properties): bool { if (!$properties || !($this->currentDataType() === 'object') || !$this->trackUnevaluated()) { return false; } $shared = $this->sharedObject(); if (!isset($shared->evaluatedProperties)) { $shared->evaluatedProperties = $properties; } else { $shared->evaluatedProperties = array_values(array_unique(array_merge($shared->evaluatedProperties, $properties))); } return true; } public function getEvaluatedProperties(): ?array { return $this->sharedObject()->evaluatedProperties ?? null; } public function getUnevaluatedProperties(): ?array { $properties = $this->getObjectProperties(); if (!$properties) { return $properties; } $evaluated = $this->sharedObject()->evaluatedProperties ?? null; if (!$evaluated) { return $properties; } return array_values(array_diff($properties, $evaluated)); } public function markAllAsEvaluatedItems(): bool { return $this->addEvaluatedItems(range(0, count($this->currentData()))); } public function markCountAsEvaluatedItems(int $count): bool { if (!$count) { return false; } return $this->addEvaluatedItems(range(0, $count)); } public function addEvaluatedItems(?array $items): bool { if (!$items || !($this->currentDataType() === 'array') || !$this->trackUnevaluated()) { return false; } $shared = $this->sharedObject(); if (!isset($shared->evaluatedItems)) { $shared->evaluatedItems = $items; } else { $shared->evaluatedItems = array_values(array_unique(array_merge($shared->evaluatedItems, $items), SORT_NUMERIC)); } return true; } public function getEvaluatedItems(): ?array { return $this->sharedObject()->evaluatedItems ?? null; } public function getUnevaluatedItems(): ?array { if ($this->currentDataType() !== 'array') { return null; } $items = array_keys($this->currentData()); if (!$items) { return $items; } $evaluated = $this->sharedObject()->evaluatedItems ?? null; if (!$evaluated) { return $items; } return array_values(array_diff($items, $evaluated)); } public function validateSchemaWithoutEvaluated( Schema $schema, ?int $maxErrors = null, bool $reset_on_error_only = false, ?ArrayObject $array = null ): ?ValidationError { $currentMaxErrors = $this->maxErrors; $this->maxErrors = $maxErrors ?? $currentMaxErrors; if ($this->trackUnevaluated()) { $shared = $this->sharedObject(); $props = $shared->evaluatedProperties ?? null; $items = $shared->evaluatedItems ?? null; $error = $schema->validate($this); if ($array) { $value = null; if ($shared->evaluatedProperties ?? null) { if ($props) { if ($diff = array_diff($shared->evaluatedProperties, $props)) { $value['properties'] = $diff; } } else { $value['properties'] = $shared->evaluatedProperties; } } if ($shared->evaluatedItems ?? null) { if ($items) { if ($diff = array_diff($shared->evaluatedItems, $items)) { $value['items'] = $diff; } } else { $value['items'] = $shared->evaluatedItems; } } if ($value) { $array[] = $value; } } if ($reset_on_error_only) { if ($error) { $shared->evaluatedProperties = $props; $shared->evaluatedItems = $items; } } else { $shared->evaluatedProperties = $props; $shared->evaluatedItems = $items; } } else { $error = $schema->validate($this); } $this->maxErrors = $currentMaxErrors; return $error; } }