Completed
Pull Request — 1.x (#196)
by Akihito
02:15
created

JsonSchemaInterceptor::getTarget()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 11
rs 9.9
c 0
b 0
f 0
cc 3
nc 3
nop 2
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\JsonSchemaKeytFoundException;
11
use BEAR\Resource\Exception\JsonSchemaNotFoundException;
12
use BEAR\Resource\JsonSchemaExceptionHandlerInterface;
13
use BEAR\Resource\ResourceObject;
14
use http\Exception\RuntimeException;
15
use function is_array;
16
use function is_object;
17
use function is_string;
18
use JsonSchema\Constraints\Constraint;
19
use JsonSchema\Validator;
20
use Ray\Aop\MethodInterceptor;
21
use Ray\Aop\MethodInvocation;
22
use Ray\Aop\ReflectionMethod;
23
use Ray\Di\Di\Named;
24
25
final class JsonSchemaInterceptor implements MethodInterceptor
26
{
27
    /**
28
     * @var string
29
     */
30
    private $schemaDir;
31
32
    /**
33
     * @var string
34
     */
35
    private $validateDir;
36
37
    /**
38
     * @var null|string
39
     */
40
    private $schemaHost;
41
42
    /**
43
     * @var JsonSchemaExceptionHandlerInterface
44
     */
45
    private $handler;
46
47
    /**
48
     * @Named("schemaDir=json_schema_dir,validateDir=json_validate_dir,schemaHost=json_schema_host")
49
     */
50
    public function __construct(string $schemaDir, string $validateDir, JsonSchemaExceptionHandlerInterface $handler, string $schemaHost = null)
51
    {
52
        $this->schemaDir = $schemaDir;
53
        $this->validateDir = $validateDir;
54
        $this->schemaHost = $schemaHost;
55
        $this->handler = $handler;
56
    }
57
58
    /**
59
     * {@inheritdoc}
60
     */
61
    public function invoke(MethodInvocation $invocation)
62
    {
63
        /** @var ReflectionMethod $method */
64
        $method = $invocation->getMethod();
65
        /** @var JsonSchema $jsonSchema */
66
        $jsonSchema = $method->getAnnotation(JsonSchema::class);
67
        if ($jsonSchema->params) {
68
            $arguments = $this->getNamedArguments($invocation);
69
            $this->validateRequest($jsonSchema, $arguments);
70
        }
71
        /** @var ResourceObject $ro */
72
        $ro = $invocation->proceed();
73
        if ($ro->code === 200 || $ro->code == 201) {
74
            $this->validateResponse($ro, $jsonSchema);
75
        }
76
77
        return $ro;
78
    }
79
80
    private function validateRequest(JsonSchema $jsonSchema, array $arguments)
81
    {
82
        $schemaFile = $this->validateDir . '/' . $jsonSchema->params;
83
        $this->validateFileExists($schemaFile);
84
        $this->validate($arguments, $schemaFile);
85
    }
86
87
    private function validateResponse(ResourceObject $ro, JsonSchema $jsonSchema)
88
    {
89
        $schemaFile = $this->getSchemaFile($jsonSchema, $ro);
90
        try {
91
            $this->validateRo($ro, $schemaFile, $jsonSchema);
92
            if (is_string($this->schemaHost)) {
93
                $ro->headers['Link'] = sprintf('<%s%s>; rel="describedby"', $this->schemaHost, $jsonSchema->schema);
94
            }
95
        } catch (JsonSchemaException $e) {
96
            $this->handler->handle($ro, $e, $schemaFile);
97
        }
98
    }
99
100
    private function validateRo(ResourceObject $ro, string $schemaFile, JsonSchema $jsonSchema)
101
    {
102
        $json = json_decode((string) $ro);
103
        if (! $json) {
104
            return;
105
        }
106
        $target = is_object($json) ? $this->getTarget($json, $jsonSchema) : $json;
107
        $this->validate($target, $schemaFile);
108
    }
109
110
    private function getTarget(\stdClass $json, JsonSchema $jsonSchema)
111
    {
112
        if ($jsonSchema->key === null) {
113
            return $json;
114
        }
115
        if (! $json->{$jsonSchema->key}) {
116
            throw new JsonSchemaKeytFoundException($jsonSchema->key);
117
        }
118
119
        return $json->{$jsonSchema->key};
120
    }
121
122
    private function validate($scanObject, $schemaFile)
123
    {
124
        $validator = new Validator;
125
        $schema = (object) ['$ref' => 'file://' . $schemaFile];
126
        $scanArray = $this->deepArray($scanObject);
127
        $validator->validate($scanArray, $schema, Constraint::CHECK_MODE_TYPE_CAST);
128
        $isValid = $validator->isValid();
129
        if ($isValid) {
130
            return;
131
        }
132
133
        throw $this->throwJsonSchemaException($validator, $schemaFile);
134
    }
135
136
    private function deepArray($values)
137
    {
138
        if (! is_array($values)) {
139
            return $values;
140
        }
141
        $result = [];
142
        foreach ($values as $key => $value) {
143
            $result[$key] = is_object($value) ? $this->deepArray((array) $value) : $result[$key] = $value;
144
        }
145
146
        return $result;
147
    }
148
149
    private function throwJsonSchemaException(Validator $validator, string $schemaFile) : JsonSchemaException
150
    {
151
        $errors = $validator->getErrors();
152
        $msg = '';
153
        foreach ($errors as $error) {
154
            $msg .= sprintf('[%s] %s; ', $error['property'], $error['message']);
155
        }
156
        $msg .= "by {$schemaFile}";
157
158
        return new JsonSchemaException($msg, Code::ERROR);
159
    }
160
161
    private function getSchemaFile(JsonSchema $jsonSchema, ResourceObject $ro) : string
162
    {
163
        if (! $jsonSchema->schema) {
164
            // for BC only
165
            $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...
166
            $roFileName = $this->getParentClassName($ro);
167
            $bcFile = str_replace('.php', '.json', (string) $roFileName);
168
            if (file_exists($bcFile)) {
169
                return $bcFile;
170
            }
171
        }
172
        $schemaFile = $this->schemaDir . '/' . $jsonSchema->schema;
173
        $this->validateFileExists($schemaFile);
174
175
        return $schemaFile;
176
    }
177
178
    private function getParentClassName(ResourceObject $ro) : string
179
    {
180
        $parent = (new \ReflectionClass($ro))->getParentClass();
181
182
        return  $parent instanceof \ReflectionClass ? (string) $parent->getFileName() : '';
183
    }
184
185
    private function validateFileExists(string $schemaFile)
186
    {
187
        if (! file_exists($schemaFile) || is_dir($schemaFile)) {
188
            throw new JsonSchemaNotFoundException($schemaFile);
189
        }
190
    }
191
192
    private function getNamedArguments(MethodInvocation $invocation)
193
    {
194
        $parameters = $invocation->getMethod()->getParameters();
195
        $values = $invocation->getArguments();
196
        $arguments = [];
197
        foreach ($parameters as $index => $parameter) {
198
            $arguments[$parameter->name] = $values[$index] ?? $parameter->getDefaultValue();
199
        }
200
201
        return $arguments;
202
    }
203
}
204