Completed
Pull Request — 1.x (#196)
by Akihito
03:45 queued 02:06
created

JsonSchemaInterceptor::deepArray()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
cc 4
nc 4
nop 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace BEAR\Resource\Interceptor;
6
7
use BEAR\Resource\Annotation\JsonSchema;
8
use BEAR\Resource\Code;
9
use BEAR\Resource\Exception\JsonSchemaException;
10
use BEAR\Resource\Exception\JsonSchemaNotFoundException;
11
use BEAR\Resource\JsonSchemaExceptionHandlerInterface;
12
use BEAR\Resource\ResourceObject;
13
use http\Exception\RuntimeException;
14
use function is_array;
15
use function is_object;
16
use function is_string;
17
use JsonSchema\Constraints\Constraint;
18
use JsonSchema\Validator;
19
use Ray\Aop\MethodInterceptor;
20
use Ray\Aop\MethodInvocation;
21
use Ray\Aop\ReflectionMethod;
22
use Ray\Di\Di\Named;
23
24
final class JsonSchemaInterceptor implements MethodInterceptor
25
{
26
    /**
27
     * @var string
28
     */
29
    private $schemaDir;
30
31
    /**
32
     * @var string
33
     */
34
    private $validateDir;
35
36
    /**
37
     * @var null|string
38
     */
39
    private $schemaHost;
40
41
    /**
42
     * @var JsonSchemaExceptionHandlerInterface
43
     */
44
    private $handler;
45
46
    /**
47
     * @Named("schemaDir=json_schema_dir,validateDir=json_validate_dir,schemaHost=json_schema_host")
48
     */
49
    public function __construct(string $schemaDir, string $validateDir, JsonSchemaExceptionHandlerInterface $handler, string $schemaHost = null)
50
    {
51
        $this->schemaDir = $schemaDir;
52
        $this->validateDir = $validateDir;
53
        $this->schemaHost = $schemaHost;
54
        $this->handler = $handler;
55
    }
56
57
    /**
58
     * {@inheritdoc}
59
     */
60
    public function invoke(MethodInvocation $invocation)
61
    {
62
        /** @var ReflectionMethod $method */
63
        $method = $invocation->getMethod();
64
        /** @var JsonSchema $jsonSchema */
65
        $jsonSchema = $method->getAnnotation(JsonSchema::class);
66
        if ($jsonSchema->params) {
67
            $arguments = $this->getNamedArguments($invocation);
68
            $this->validateRequest($jsonSchema, $arguments);
69
        }
70
        /** @var ResourceObject $ro */
71
        $ro = $invocation->proceed();
72
        if ($ro->code === 200 || $ro->code == 201) {
73
            $this->validateResponse($ro, $jsonSchema);
74
        }
75
76
        return $ro;
77
    }
78
79
    private function validateRequest(JsonSchema $jsonSchema, array $arguments)
80
    {
81
        $schemaFile = $this->validateDir . '/' . $jsonSchema->params;
82
        $this->validateFileExists($schemaFile);
83
        $this->validate($arguments, $schemaFile);
84
    }
85
86
    private function validateResponse(ResourceObject $ro, JsonSchema $jsonSchema)
87
    {
88
        $schemaFile = $this->getSchemaFile($jsonSchema, $ro);
89
        try {
90
            $this->validateRo($ro, $schemaFile, $jsonSchema);
91
            if (is_string($this->schemaHost)) {
92
                $ro->headers['Link'] = sprintf('<%s%s>; rel="describedby"', $this->schemaHost, $jsonSchema->schema);
93
            }
94
        } catch (JsonSchemaException $e) {
95
            $this->handler->handle($ro, $e, $schemaFile);
96
        }
97
    }
98
99
    private function validateRo(ResourceObject $ro, string $schemaFile, JsonSchema $jsonSchema)
100
    {
101
        $json = json_decode((string) $ro);
102
        if (! $json) {
103
            return;
104
        }
105
        $target = $json ? $this->getTarget($json, $jsonSchema) : [];
106
        $this->validate($target, $schemaFile);
107
    }
108
109
    private function getTarget(\stdClass $json, JsonSchema $jsonSchema)
110
    {
111
        if ($jsonSchema->key === null) {
112
            return $json;
113
        }
114
        if (! $json->{$jsonSchema->key}) {
115
            throw new RuntimeException($jsonSchema->key);
116
        }
117
118
        return $json->{$jsonSchema->key};
119
    }
120
121
    private function validate($scanObject, $schemaFile)
122
    {
123
        $validator = new Validator;
124
        $schema = (object) ['$ref' => 'file://' . $schemaFile];
125
        $scanArray = $this->deepArray($scanObject);
126
        $validator->validate($scanArray, $schema, Constraint::CHECK_MODE_TYPE_CAST);
127
        $isValid = $validator->isValid();
128
        if ($isValid) {
129
            return;
130
        }
131
132
        throw $this->throwJsonSchemaException($validator, $schemaFile);
133
    }
134
135
    private function deepArray($values)
136
    {
137
        if (! is_array($values)) {
138
            return $values;
139
        }
140
        $result = [];
141
        foreach ($values as $key => $value) {
142
            $result[$key] = is_object($value) ? $this->deepArray((array) $value) : $result[$key] = $value;
143
        }
144
145
        return $result;
146
    }
147
148
    private function throwJsonSchemaException(Validator $validator, string $schemaFile) : JsonSchemaException
149
    {
150
        $errors = $validator->getErrors();
151
        $msg = '';
152
        foreach ($errors as $error) {
153
            $msg .= sprintf('[%s] %s; ', $error['property'], $error['message']);
154
        }
155
        $msg .= "by {$schemaFile}";
156
157
        return new JsonSchemaException($msg, Code::ERROR);
158
    }
159
160
    private function getSchemaFile(JsonSchema $jsonSchema, ResourceObject $ro) : string
161
    {
162
        if (! $jsonSchema->schema) {
163
            // for BC only
164
            $ref = new \ReflectionClass($ro);
0 ignored issues
show
Unused Code introduced by
$ref is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
165
            $roFileName = $this->getParentClassName($ro);
166
            $bcFile = str_replace('.php', '.json', (string) $roFileName);
167
            if (file_exists($bcFile)) {
168
                return $bcFile;
169
            }
170
        }
171
        $schemaFile = $this->schemaDir . '/' . $jsonSchema->schema;
172
        $this->validateFileExists($schemaFile);
173
174
        return $schemaFile;
175
    }
176
177
    private function getParentClassName(ResourceObject $ro) : string
178
    {
179
        $parent = (new \ReflectionClass($ro))->getParentClass();
180
181
        return  $parent instanceof \ReflectionClass ? (string) $parent->getFileName() : '';
182
    }
183
184
    private function validateFileExists(string $schemaFile)
185
    {
186
        if (! file_exists($schemaFile) || is_dir($schemaFile)) {
187
            throw new JsonSchemaNotFoundException($schemaFile);
188
        }
189
    }
190
191
    private function getNamedArguments(MethodInvocation $invocation)
192
    {
193
        $parameters = $invocation->getMethod()->getParameters();
194
        $values = $invocation->getArguments();
195
        $arguments = [];
196
        foreach ($parameters as $index => $parameter) {
197
            $arguments[$parameter->name] = $values[$index] ?? $parameter->getDefaultValue();
198
        }
199
200
        return $arguments;
201
    }
202
}
203