$schema */ public static function check(array $schema, mixed $value, string $path = 'args'): ?string { $type = $schema['type'] ?? null; if ($type !== null && !self::isType($type, $value)) { return sprintf('%s must be %s', $path, is_array($type) ? implode(' or ', $type) : (string) $type); } if (isset($schema['enum']) && !in_array($value, $schema['enum'], true)) { return sprintf('%s must be one of %s', $path, implode(', ', array_map('strval', $schema['enum']))); } if (is_string($value)) { if (isset($schema['maxLength']) && mb_strlen($value) > (int) $schema['maxLength']) { return sprintf('%s is longer than %d characters', $path, (int) $schema['maxLength']); } if (isset($schema['minLength']) && mb_strlen($value) < (int) $schema['minLength']) { return sprintf('%s is empty', $path); } if (isset($schema['pattern']) && !preg_match('~' . str_replace('~', '\~', (string) $schema['pattern']) . '~u', $value)) { return sprintf('%s is not in the expected form', $path); } } if (is_int($value) || is_float($value)) { if (isset($schema['minimum']) && $value < $schema['minimum']) { return sprintf('%s must be at least %s', $path, (string) $schema['minimum']); } if (isset($schema['maximum']) && $value > $schema['maximum']) { return sprintf('%s must be at most %s', $path, (string) $schema['maximum']); } } if (is_array($value) && self::isList($value) && ($type === 'array' || (is_array($type) && in_array('array', $type, true)))) { if (isset($schema['maxItems']) && count($value) > (int) $schema['maxItems']) { return sprintf('%s has more than %d items', $path, (int) $schema['maxItems']); } if (isset($schema['items']) && is_array($schema['items'])) { foreach ($value as $index => $item) { $error = self::check($schema['items'], $item, $path . '[' . $index . ']'); if ($error !== null) { return $error; } } } return null; } if (is_array($value) && ($type === 'object' || (is_array($type) && in_array('object', $type, true)))) { foreach ((array) ($schema['required'] ?? []) as $key) { if (!array_key_exists((string) $key, $value)) { return sprintf('%s.%s is required', $path, (string) $key); } } $properties = is_array($schema['properties'] ?? null) ? $schema['properties'] : []; foreach ($value as $key => $item) { if (!isset($properties[$key])) { if (($schema['additionalProperties'] ?? false) === true) { continue; } return sprintf('%s.%s is not a known field', $path, (string) $key); } $error = self::check($properties[$key], $item, $path . '.' . $key); if ($error !== null) { return $error; } } } return null; } private static function isType(mixed $type, mixed $value): bool { if (is_array($type)) { foreach ($type as $one) { if (self::isType($one, $value)) { return true; } } return false; } return match ((string) $type) { 'string' => is_string($value), 'integer' => is_int($value) || (is_float($value) && floor($value) === $value), 'number' => is_int($value) || is_float($value), 'boolean' => is_bool($value), 'array' => is_array($value) && self::isList($value), 'object' => is_array($value) && ($value === [] || !self::isList($value)), 'null' => $value === null, default => false, }; } /** @param array $value */ private static function isList(array $value): bool { return $value === [] || array_keys($value) === range(0, count($value) - 1); } }