Completed
Push — master ( 1ce816...c7fd8f )
by Cees-Jan
02:35
created

AsyncTable::hasMethodAnnotation()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
ccs 3
cts 3
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 2
crap 1
1
<?php
2
3
namespace WyriHaximus\React\Cake\Orm;
4
5
use Cake\ORM\Query;
6
use Cake\ORM\TableRegistry;
7
use Doctrine\Common\Annotations\AnnotationReader;
8
use phpDocumentor\Reflection\DocBlock;
9
use phpDocumentor\Reflection\DocBlockFactory;
10
use React\Promise\PromiseInterface;
11
use WyriHaximus\React\Cake\Orm\Annotations\Async;
12
use WyriHaximus\React\Cake\Orm\Annotations\Sync;
13
14
class AsyncTable
15
{
16
    /**
17
     * @var Pool
18
     */
19
    protected $pool;
20
21
    /**
22
     * @var string
23
     */
24
    protected $tableName;
25
26
    /**
27
     * @var AnnotationReader
28
     */
29
    protected $annotationReader;
30
31
    /**
32
     * @var \ReflectionClass
33
     */
34
    protected $reflectionClass;
35
36
    /**
37
     * @param Pool $pool
38
     * @param string $tableName
39
     */
40 15
    public function __construct(Pool $pool, $tableName, $tableClass)
41
    {
42 15
        $this->pool = $pool;
43 15
        $this->tableName = $tableName;
44 15
        $this->annotationReader = new AnnotationReader();
45 15
        $this->reflectionClass = new \ReflectionClass($tableClass);
46 15
    }
47
48
    /**
49
     * @param $function
50
     * @param array $arguments
51
     * @return PromiseInterface
52
     */
53 14
    public function __call($function, array $arguments = [])
54
    {
55 1
        if (
56 14
            $this->returnsQuery($function) ||
57 12
            $this->hasMethodAnnotation($function, Async::class) ||
58
            (
59 9
                $this->hasClassAnnotation(Async::class) &&
60 2
                $this->hasNoMethodAnnotation($function)
61 9
            ) ||
62 8
            strpos(strtolower($function), 'save') === 0 ||
63 8
            strpos(strtolower($function), 'find') === 0 ||
64 7
            strpos(strtolower($function), 'fetch') === 0 ||
65 8
            strpos(strtolower($function), 'retrieve') === 0
66 14
        ) {
67 8
            return $this->callAsync($function, $arguments);
68
        }
69
70 6
        return $this->callSync($function, $arguments);
71
    }
72
73
    /**
74
     * @param $function
75
     * @param array $arguments
76
     * @return PromiseInterface
77
     */
78 1
    protected function callSync($function, array $arguments = [])
79
    {
80
        $table = TableRegistry::get($this->tableName);
81
        if (isset(class_uses($table)[TableRegistryTrait::class])) {
82
            $table->setRegistry(AsyncTableRegistry::class);
83
        }
84 1
        return \React\Promise\resolve(
85
            call_user_func_array(
86
                [
87 1
                    $table,
88
                    $function
89
                ],
90
                $arguments
91
            )
92
        );
93 1
    }
94
95
    /**
96
     * @param $function
97
     * @param array $arguments
98
     * @return PromiseInterface
99
     */
100
    protected function callAsync($function, array $arguments = [])
101
    {
102
        $unSerialize = function ($input) {
103
            if (is_string($input)) {
104
                return unserialize($input);
105
            }
106
107
            return $input;
108
        };
109
        return $this->
110
            pool->
111
            call($this->tableName, $function, $arguments)->
112
            then($unSerialize, $unSerialize, $unSerialize);
113
    }
114
115
    /**
116
     * @param $class
117
     * @return bool
118
     */
119 9
    protected function hasClassAnnotation($class)
120
    {
121 9
        return is_a($this->annotationReader->getClassAnnotation($this->reflectionClass, $class), $class);
122
    }
123
124
    /**
125
     * @param $method
126
     * @param $class
127
     * @return bool
128
     */
129 12
    protected function hasMethodAnnotation($method, $class)
130
    {
131 12
        $methodReflection = $this->reflectionClass->getMethod($method);
132 12
        return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class);
133
    }
134
135
    /**
136
     * @param $method
137
     * @return bool
138
     */
139 2
    protected function hasNoMethodAnnotation($method)
140
    {
141 2
        $methodReflection = $this->reflectionClass->getMethod($method);
142
        return (
143 2
            $this->annotationReader->getMethodAnnotation($methodReflection, Async::class) === null &&
144 2
            $this->annotationReader->getMethodAnnotation($methodReflection, Sync::class) === null
145 2
        );
146
    }
147
148
    /**
149
     * @param $function
150
     * @return bool
151
     */
152 14
    protected function returnsQuery($function)
153
    {
154 14
        $docBlockContents = $this->reflectionClass->getMethod($function)->getDocComment();
155 14
        if (!is_string($docBlockContents)) {
156 6
            return false;
157
        }
158
159 8
        $docBlock = $this->getDocBlock($docBlockContents);
160 8
        foreach ($docBlock->getTags() as $tag) {
161 8
            if ($tag->getName() === 'return' && ltrim($tag->getType(), '\\') == Query::class) {
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface phpDocumentor\Reflection\DocBlock\Tag as the method getType() does only exist in the following implementations of said interface: phpDocumentor\Reflection\DocBlock\Tags\Param, phpDocumentor\Reflection\DocBlock\Tags\Property, phpDocumentor\Reflection...Block\Tags\PropertyRead, phpDocumentor\Reflection...lock\Tags\PropertyWrite, phpDocumentor\Reflection\DocBlock\Tags\Return_, phpDocumentor\Reflection\DocBlock\Tags\Throws, phpDocumentor\Reflection\DocBlock\Tags\Var_.

Let’s take a look at an example:

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

class MyUser implements 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 implementation 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 interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
162 2
                return true;
163
            }
164 6
        }
165
166 6
        return false;
167
    }
168
169
    /**
170
     * @param $docBlockContents
171
     * @return DocBlock
172
     */
173 8
    protected function getDocBlock($docBlockContents)
174
    {
175 8
        if (class_exists('phpDocumentor\Reflection\DocBlockFactory')) {
176 8
            return DocBlockFactory::createInstance()->create($docBlockContents);
177 1
        }
178
179
        return new DocBlock($docBlockContents);
180
    }
181
}
182