Completed
Push — 1.x ( 365e71...2c7718 )
by Akihito
13s queued 11s
created

JsonSchemaInterceptor::invoke()   B

Complexity

Conditions 6
Paths 7

Size

Total Lines 20
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 11
CRAP Score 6

Importance

Changes 8
Bugs 0 Features 2
Metric Value
dl 0
loc 20
ccs 11
cts 11
cp 1
rs 8.8571
c 8
b 0
f 2
cc 6
eloc 13
nc 7
nop 1
crap 6
1
<?php
2
/**
3
 * This file is part of the BEAR.Resource package.
4
 *
5
 * @license http://opensource.org/licenses/MIT MIT
6
 */
7
namespace BEAR\Resource\Interceptor;
8
9
use BEAR\Resource\Annotation\JsonSchema;
10
use BEAR\Resource\Code;
11
use BEAR\Resource\Exception\JsonSchemaErrorException;
12
use BEAR\Resource\Exception\JsonSchemaException;
13
use BEAR\Resource\Exception\JsonSchemaNotFoundException;
14
use BEAR\Resource\ResourceObject;
15
use JsonSchema\Constraints\Constraint;
16
use JsonSchema\Validator;
17
use Ray\Aop\MethodInterceptor;
18
use Ray\Aop\MethodInvocation;
19
use Ray\Aop\WeavedInterface;
20
use Ray\Di\Di\Named;
21
22
final class JsonSchemaInterceptor implements MethodInterceptor
23
{
24
    /**
25
     * @var string
26
     */
27
    private $schemaDir;
28
29
    /**
30
     * @var string
31
     */
32
    private $validateDir;
33
34
    /**
35
     * @param string $schemaDir
36
     * @param string $validateDir
37
     *
38
     * @Named("schemaDir=json_schema_dir,validateDir=json_validate_dir")
39
     */
40 7
    public function __construct($schemaDir, $validateDir)
41
    {
42 7
        $this->schemaDir = $schemaDir;
43 7
        $this->validateDir = $validateDir;
44 7
    }
45
46
    /**
47
     * {@inheritdoc}
48
     */
49 7
    public function invoke(MethodInvocation $invocation)
50
    {
51
        $jsonSchema = $invocation->getMethod()->getAnnotation(JsonSchema::class);
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class ReflectionMethod as the method getAnnotation() does only exist in the following sub-classes of ReflectionMethod: Ray\Aop\ReflectionMethod. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
52 7
        if (! $jsonSchema instanceof JsonSchema) {
53 7
            throw new JsonSchemaException($invocation->getMethod()->name);
54 3
        }
55 3
        if ($jsonSchema->params) {
56
            $arguments = $this->getNamedArguments($invocation);
57
            $this->validateRequest($jsonSchema, $arguments);
58 6
        }
59 6
        $ro = $invocation->proceed();
60 1
        if (! $ro instanceof ResourceObject) {
61
            throw new JsonSchemaException($invocation->getMethod()->name);
62 5
        }
63
        if ($ro->code === 200 || $ro->code == 201) {
64 2
            $this->validateResponse($jsonSchema, $ro);
65
        }
66
67 3
        return $ro;
68
    }
69 3
70 3
    private function validateRequest(JsonSchema $jsonSchema, array $arguments)
71 3
    {
72 2
        $schemaFile = $this->validateDir . '/' . $jsonSchema->params;
73
        $this->validateFileExists($schemaFile);
74 5
        $this->validate($arguments, $schemaFile);
75
    }
76 5
77 4
    private function validateResponse(JsonSchema $jsonSchema, ResourceObject $ro)
78 4
    {
79 2
        $schemeFile = $this->getSchemaFile($jsonSchema, $ro);
80
        $body = isset($ro->body[$jsonSchema->key]) ? $ro->body[$jsonSchema->key] : $ro->body;
81 5
        $this->validate($body, $schemeFile);
82
    }
83 5
84 5
    private function validate($scanObject, $schemaFile)
85 5
    {
86 5
        $validator = new Validator;
87 5
        $schema = (object) ['$ref' => 'file://' . $schemaFile];
88 3
        $validator->validate($scanObject, $schema, Constraint::CHECK_MODE_TYPE_CAST);
89
        $isValid = $validator->isValid();
90 3
        if ($isValid) {
91 3
            return;
92 3
        }
93 3
        $e = null;
94
        foreach ($validator->getErrors() as $error) {
95 3
            $msg = sprintf('[%s] %s', $error['property'], $error['message']);
96
            $e = $e ? new JsonSchemaErrorException($msg, 0, $e) : new JsonSchemaErrorException($msg);
97
        }
98 5
        throw new JsonSchemaException($schemaFile, Code::ERROR, $e);
99
    }
100 5
101
    private function getSchemaFile(JsonSchema $jsonSchema, ResourceObject $ro) : string
102 1
    {
103 1
        if (! $jsonSchema->schema) {
104 1
            // for BC only
105 1
            $ref = new \ReflectionClass($ro);
106 1
            $roFileName = $ro instanceof WeavedInterface ? $roFileName = $ref->getParentClass()->getFileName() : $ref->getFileName();
107
            $bcFile = str_replace('.php', '.json', $roFileName);
108
            if (file_exists($bcFile)) {
109 4
                return $bcFile;
110 4
            }
111
        }
112 3
        $schemaFile = $this->schemaDir . '/' . $jsonSchema->schema;
113
        $this->validateFileExists($schemaFile);
114
115 5
        return $schemaFile;
116
    }
117 5
118 1
    private function validateFileExists(string $schemaFile)
119
    {
120 4
        if (! file_exists($schemaFile) || is_dir($schemaFile)) {
121
            throw new JsonSchemaNotFoundException($schemaFile);
122 3
        }
123
    }
124 3
125 3
    private function getNamedArguments(MethodInvocation $invocation)
126 3
    {
127 3
        $parameters = $invocation->getMethod()->getParameters();
128 3
        $values = $invocation->getArguments();
129
        $arguments = [];
130
        foreach ($parameters as $index => $parameter) {
131 3
            $arguments[$parameter->name] = $values[$index] ?? $parameter->getDefaultValue();
132
        }
133
134
        return $arguments;
135
    }
136
}
137