Completed
Push — fake_json ( 3414f8...1903bd )
by Akihito
09:01 queued 07:08
created

JsonSchemaInterceptor::validate()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

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