Completed
Pull Request — master (#5)
by Cees-Jan
09:29
created

AsyncTable   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 176
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 4

Test Coverage

Coverage 0%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 4
dl 0
loc 176
ccs 0
cts 99
cp 0
rs 10
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A setUpAsyncTable() 0 7 1
C callAsyncOrSync() 0 23 10
A callSync() 0 19 2
A callAsync() 0 15 2
A hasClassAnnotation() 0 4 1
A hasMethodAnnotation() 0 5 1
A hasNoMethodAnnotation() 0 8 2
B returnsQuery() 0 16 5
A getDocBlock() 0 8 2
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
trait AsyncTable
15
{
16
    /**
17
     * @var Pool
18
     */
19
    private $pool;
20
21
    /**
22
     * @var string
23
     */
24
    private $tableName;
25
26
    /**
27
     * @var AnnotationReader
28
     */
29
    private $annotationReader;
30
31
    /**
32
     * @var \ReflectionClass
33
     */
34
    private $reflectionClass;
35
36
    /**
37
     * @param Pool $pool
38
     * @param string $tableName
39
     */
40
    public function setUpAsyncTable(Pool $pool, $tableName, $tableClass)
41
    {
42
        $this->pool = $pool;
43
        $this->tableName = $tableName;
44
        $this->annotationReader = new AnnotationReader();
45
        $this->reflectionClass = new \ReflectionClass($tableClass);
46
    }
47
48
    /**
49
     * @param $function
50
     * @param array $arguments
51
     * @return PromiseInterface
52
     */
53
    protected function callAsyncOrSync($function, $arguments)
54
    {
55
        if ($this->pool === null) {
56
            return (new $this->tableName)->$function(...$arguments);
57
        }
58
59
        if (
60
            $this->returnsQuery($function) ||
61
            $this->hasMethodAnnotation($function, Async::class) ||
62
            (
63
                $this->hasClassAnnotation(Async::class) &&
64
                $this->hasNoMethodAnnotation($function)
65
            ) ||
66
            strpos(strtolower($function), 'save') === 0 ||
67
            strpos(strtolower($function), 'find') === 0 ||
68
            strpos(strtolower($function), 'fetch') === 0 ||
69
            strpos(strtolower($function), 'retrieve') === 0
70
        ) {
71
            return $this->callAsync($function, $arguments);
72
        }
73
74
        return $this->callSync($function, $arguments);
75
    }
76
77
    /**
78
     * @param $function
79
     * @param array $arguments
80
     * @return PromiseInterface
81
     */
82
    private function callSync($function, array $arguments = [])
83
    {
84
        $table = TableRegistry::get(md5($this->tableName), [
85
            'className' => $this->tableName,
86
            'table' => 'screenshots',
87
        ]);
88
        if (isset(class_uses($table)[TableRegistryTrait::class])) {
89
            $table->setRegistry(AsyncTableRegistry::class);
90
        }
91
        return \React\Promise\resolve(
92
            call_user_func_array(
93
                [
94
                    $table,
95
                    $function
96
                ],
97
                $arguments
98
            )
99
        );
100
    }
101
102
    /**
103
     * @param $function
104
     * @param array $arguments
105
     * @return PromiseInterface
106
     */
107
    private function callAsync($function, array $arguments = [])
108
    {
109
        $unSerialize = function ($input) {
110
            if (is_string($input)) {
111
                return unserialize($input);
112
            }
113
114
            return $input;
115
        };
116
        return $this->
117
            pool->
118
            call(get_parent_class($this), $this->tableName, $function, $arguments)->
119
            then($unSerialize, $unSerialize, $unSerialize)
120
        ;
121
    }
122
123
    /**
124
     * @param $class
125
     * @return bool
126
     */
127
    private function hasClassAnnotation($class)
128
    {
129
        return is_a($this->annotationReader->getClassAnnotation($this->reflectionClass, $class), $class);
130
    }
131
132
    /**
133
     * @param $method
134
     * @param $class
135
     * @return bool
136
     */
137
    private function hasMethodAnnotation($method, $class)
138
    {
139
        $methodReflection = $this->reflectionClass->getMethod($method);
140
        return is_a($this->annotationReader->getMethodAnnotation($methodReflection, $class), $class);
141
    }
142
143
    /**
144
     * @param $method
145
     * @return bool
146
     */
147
    private function hasNoMethodAnnotation($method)
148
    {
149
        $methodReflection = $this->reflectionClass->getMethod($method);
150
        return (
151
            $this->annotationReader->getMethodAnnotation($methodReflection, Async::class) === null &&
152
            $this->annotationReader->getMethodAnnotation($methodReflection, Sync::class) === null
153
        );
154
    }
155
156
    /**
157
     * @param $function
158
     * @return bool
159
     */
160
    private function returnsQuery($function)
161
    {
162
        $docBlockContents = $this->reflectionClass->getMethod($function)->getDocComment();
163
        if (!is_string($docBlockContents)) {
164
            return false;
165
        }
166
167
        $docBlock = $this->getDocBlock($docBlockContents);
168
        foreach ($docBlock->getTags() as $tag) {
169
            if ($tag->getName() === 'return' && ltrim($tag->getType(), '\\') == Query::class) {
0 ignored issues
show
Bug introduced by
It seems like you code against a specific sub-type and not the parent class phpDocumentor\Reflection\DocBlock\Tag as the method getType() does only exist in the following sub-classes of phpDocumentor\Reflection\DocBlock\Tag: phpDocumentor\Reflection\DocBlock\Tag\MethodTag, phpDocumentor\Reflection\DocBlock\Tag\ParamTag, phpDocumentor\Reflection...ock\Tag\PropertyReadTag, phpDocumentor\Reflection\DocBlock\Tag\PropertyTag, phpDocumentor\Reflection...ck\Tag\PropertyWriteTag, phpDocumentor\Reflection\DocBlock\Tag\ReturnTag, phpDocumentor\Reflection\DocBlock\Tag\ThrowsTag, phpDocumentor\Reflection\DocBlock\Tag\VarTag. 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...
170
                return true;
171
            }
172
        }
173
174
        return false;
175
    }
176
177
    /**
178
     * @param $docBlockContents
179
     * @return DocBlock
180
     */
181
    private function getDocBlock($docBlockContents)
182
    {
183
        if (class_exists('phpDocumentor\Reflection\DocBlockFactory')) {
184
            return DocBlockFactory::createInstance()->create($docBlockContents);
185
        }
186
187
        return new DocBlock($docBlockContents);
188
    }
189
}
190