Completed
Push — master ( 71d8db...83e409 )
by Kirill
43:48
created

Compiler::getPersister()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 9
ccs 0
cts 5
cp 0
rs 9.6666
c 0
b 0
f 0
cc 1
eloc 5
nc 1
nop 0
crap 2
1
<?php
2
/**
3
 * This file is part of Railt package.
4
 *
5
 * For the full copyright and license information, please view the LICENSE
6
 * file that was distributed with this source code.
7
 */
8
declare(strict_types=1);
9
10
namespace Railt\SDL;
11
12
use Railt\Compiler\ParserInterface;
13
use Railt\Io\Readable;
14
use Railt\SDL\Contracts\Definitions\Definition;
15
use Railt\SDL\Contracts\Definitions\TypeDefinition;
16
use Railt\SDL\Contracts\Document;
17
use Railt\SDL\Exceptions\CompilerException;
18
use Railt\SDL\Exceptions\UnexpectedTokenException;
19
use Railt\SDL\Exceptions\UnrecognizedTokenException;
20
use Railt\SDL\Parser\Factory as ParserFactory;
21
use Railt\SDL\Reflection\Builder\DocumentBuilder;
22
use Railt\SDL\Reflection\Builder\Process\Compilable;
23
use Railt\SDL\Reflection\Coercion\Factory;
24
use Railt\SDL\Reflection\Coercion\TypeCoercion;
25
use Railt\SDL\Reflection\Dictionary;
26
use Railt\SDL\Reflection\Loader;
27
use Railt\SDL\Reflection\Validation\Base\ValidatorInterface;
28
use Railt\SDL\Reflection\Validation\Definitions;
29
use Railt\SDL\Reflection\Validation\Validator;
30
use Railt\SDL\Runtime\CallStack;
31
use Railt\SDL\Runtime\CallStackInterface;
32
use Railt\SDL\Schema\CompilerInterface;
33
use Railt\SDL\Schema\Configuration;
34
use Railt\SDL\Standard\GraphQLDocument;
35
use Railt\SDL\Standard\StandardType;
36
use Railt\Storage\Drivers\ArrayStorage;
37
use Railt\Storage\Proxy;
38
use Railt\Storage\Storage;
39
40
/**
41
 * Class Compiler
42
 */
43
class Compiler implements CompilerInterface, Configuration
44
{
45
    use Support;
46
47
    /**
48
     * @var Dictionary
49
     */
50
    private $loader;
51
52
    /**
53
     * @var ParserFactory
54
     */
55
    private $parser;
56
57
    /**
58
     * @var Storage|ArrayStorage
59
     */
60
    private $storage;
61
62
    /**
63
     * @var Validator
64
     */
65
    private $typeValidator;
66
67
    /**
68
     * @var Factory|TypeCoercion
69
     */
70
    private $typeCoercion;
71
72
    /**
73
     * @var CallStack
74
     */
75
    private $stack;
76
77
    /**
78
     * Compiler constructor.
79
     * @param Storage|null $storage
80
     * @throws \OutOfBoundsException
81
     * @throws CompilerException
82
     */
83 283
    public function __construct(Storage $storage = null)
84
    {
85 283
        $this->stack         = new CallStack();
86 283
        $this->parser        = (new ParserFactory())->getParser();
0 ignored issues
show
Documentation Bug introduced by
It seems like (new \Railt\SDL\Parser\Factory())->getParser() of type object<Railt\Compiler\ParserInterface> is incompatible with the declared type object<Railt\SDL\Parser\Factory> of property $parser.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
87 283
        $this->loader        = new Loader($this, $this->stack);
88 283
        $this->typeValidator = new Validator($this->stack);
89 283
        $this->typeCoercion  = new Factory();
90
91 283
        $this->storage = $this->bootStorage($storage);
92
93 283
        $this->add($this->getStandardLibrary());
94 283
    }
95
96
    /**
97
     * @param Document $document
98
     * @return CompilerInterface
99
     * @throws \Railt\SDL\Exceptions\CompilerException
100
     */
101 283
    public function add(Document $document): CompilerInterface
102
    {
103
        try {
104 283
            $this->complete($document);
105
        } catch (\OutOfBoundsException $fatal) {
106
            throw CompilerException::wrap($fatal);
107
        }
108
109 283
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Railt\SDL\Compiler) is incompatible with the return type declared by the interface Railt\SDL\Schema\CompilerInterface::add of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
110
    }
111
112
    /**
113
     * @param null|Storage $storage
114
     * @return Storage
115
     */
116 283
    private function bootStorage(?Storage $storage): Storage
117
    {
118 283
        if ($storage === null) {
119 283
            return new ArrayStorage();
120
        }
121
122 283
        if ($storage instanceof Proxy || $storage instanceof ArrayStorage) {
123 283
            return $storage;
124
        }
125
126 283
        return new Proxy(new ArrayStorage(), $storage);
127
    }
128
129
    /**
130
     * @param array $extensions
131
     * @return GraphQLDocument|Document
132
     * @throws \OutOfBoundsException
133
     */
134 283
    private function getStandardLibrary(array $extensions = []): GraphQLDocument
135
    {
136 283
        return new GraphQLDocument($this->getDictionary(), $extensions);
137
    }
138
139
    /**
140
     * @param Document|DocumentBuilder $document
141
     * @return Document
142
     * @throws \OutOfBoundsException
143
     */
144 7165
    private function complete(Document $document): Document
145
    {
146 7165
        $this->load($document);
147
148 7165
        $build = function (Definition $definition): void {
149 7165
            $this->stack->push($definition);
150
151 7165
            if ($definition instanceof Compilable) {
152 7164
                $definition->compile();
153
            }
154
155 7159
            if ($definition instanceof TypeDefinition) {
156 7159
                $this->typeCoercion->apply($definition);
157
            }
158
159 7159
            if (! ($definition instanceof StandardType)) {
160 7155
                $this->typeValidator->group(Definitions::class)->validate($definition);
161
            }
162
163 6775
            $this->stack->pop();
164 7165
        };
165
166 7165
        foreach ($document->getDefinitions() as $definition) {
167 7165
            $build($definition);
168
        }
169
170 4555
        if ($document instanceof DocumentBuilder) {
171 4401
            foreach ($document->getInvocableTypes() as $definition) {
172 1170
                $build($definition);
173
            }
174
        }
175
176 3667
        return $document;
177
    }
178
179
    /**
180
     * @param Document $document
181
     * @return Document|DocumentBuilder
182
     */
183 7165
    private function load(Document $document): Document
184
    {
185 7165
        foreach ($document->getTypeDefinitions() as $type) {
186 7165
            $this->stack->push($type);
187 7165
            $this->loader->register($type);
188 7165
            $this->stack->pop();
189
        }
190
191 7165
        return $document;
192
    }
193
194
    /**
195
     * @param \Closure $then
196
     * @return CompilerInterface
197
     */
198 6
    public function autoload(\Closure $then): CompilerInterface
199
    {
200 6
        $this->loader->autoload($then);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Railt\SDL\Reflection\Dictionary as the method autoload() does only exist in the following implementations of said interface: Railt\SDL\Reflection\Loader.

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...
201
202 6
        return $this;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this; (Railt\SDL\Compiler) is incompatible with the return type declared by the interface Railt\SDL\Schema\CompilerInterface::autoload of type self.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

    public function __construct($name) {
        $this->name = $name;
    }

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
203
    }
204
205
    /**
206
     * @param Readable $readable
207
     * @return Document
208
     * @throws \OutOfBoundsException
209
     * @throws \Railt\SDL\Exceptions\UnrecognizedTokenException
210
     * @throws \Railt\SDL\Exceptions\UnexpectedTokenException
211
     * @throws CompilerException
212
     */
213 7164
    public function compile(Readable $readable): Document
214
    {
215
        /** @var DocumentBuilder $document */
216 7164
        $document = $this->storage->remember($readable, $this->onCompile());
217
218
        /** @var TypeDefinition $type */
219 3513
        foreach ($document->getTypeDefinitions() as $type) {
220 3513
            if (! $this->loader->has($type->getName())) {
221
                $this->stack->push($type);
222
                $this->loader->register($type);
223 3513
                $this->stack->pop();
224
            }
225
        }
226
227 3513
        return $document->withCompiler($this);
228
    }
229
230
    /**
231
     * @return \Closure
232
     * @throws \OutOfBoundsException
233
     * @throws UnexpectedTokenException
234
     * @throws UnrecognizedTokenException
235
     * @throws CompilerException
236
     */
237
    private function onCompile(): \Closure
238
    {
239 7164
        return function (Readable $readable): Document {
240 7164
            $ast = $this->parser->parse($readable);
241
242 7164
            return $this->complete(new DocumentBuilder($ast, $readable, $this));
243 7164
        };
244
    }
245
246
    /**
247
     * @param string $group
248
     * @return ValidatorInterface
249
     * @throws \OutOfBoundsException
250
     */
251 7164
    public function getValidator(string $group): ValidatorInterface
252
    {
253 7164
        return $this->typeValidator->group($group);
254
    }
255
256
    /**
257
     * @return ParserInterface
258
     */
259
    public function getParser(): ParserInterface
260
    {
261
        return $this->parser;
262
    }
263
264
    /**
265
     * @return TypeCoercion
266
     */
267 6563
    public function getTypeCoercion(): TypeCoercion
268
    {
269 6563
        return $this->typeCoercion;
270
    }
271
272
    /**
273
     * @return Storage
274
     * @deprecated Since 1.2: Use getStorage() method instead.
275
     */
276
    public function getPersister(): Storage
277
    {
278
        \trigger_error(
279
            __METHOD__ . ' was renamed to getStorage and will be deleted on next release',
280
            \E_USER_DEPRECATED
281
        );
282
283
        return $this->getStorage();
284
    }
285
286
    /**
287
     * @return Storage
288
     */
289
    public function getStorage(): Storage
290
    {
291
        return $this->storage;
292
    }
293
294
    /**
295
     * @return Dictionary
296
     */
297 7153
    public function getDictionary(): Dictionary
298
    {
299 7153
        return $this->loader;
300
    }
301
302
    /**
303
     * @return CallStackInterface
304
     */
305 6563
    public function getCallStack(): CallStackInterface
306
    {
307 6563
        return $this->stack;
308
    }
309
}
310