Completed
Push — master ( 296743...12eab9 )
by Kirill
36:17
created

Compiler::complete()   B

Complexity

Conditions 7
Paths 4

Size

Total Lines 34

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 34
ccs 18
cts 18
cp 1
rs 8.4426
c 0
b 0
f 0
cc 7
nc 4
nop 1
crap 7
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\Io\Readable;
13
use Railt\Parser\ParserInterface;
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\Parser\Parser;
19
use Railt\SDL\Reflection\Builder\DocumentBuilder;
20
use Railt\SDL\Reflection\Builder\Process\Compilable;
21
use Railt\SDL\Reflection\Coercion\Factory;
22
use Railt\SDL\Reflection\Coercion\TypeCoercion;
23
use Railt\SDL\Reflection\Dictionary;
24
use Railt\SDL\Reflection\Loader;
25
use Railt\SDL\Reflection\Validation\Base\ValidatorInterface;
26
use Railt\SDL\Reflection\Validation\Definitions;
27
use Railt\SDL\Reflection\Validation\Validator;
28
use Railt\SDL\Runtime\CallStack;
29
use Railt\SDL\Runtime\CallStackInterface;
30
use Railt\SDL\Schema\CompilerInterface;
31
use Railt\SDL\Schema\Configuration;
32
use Railt\SDL\Standard\GraphQLDocument;
33
use Railt\SDL\Standard\StandardType;
34
use Railt\Storage\Drivers\ArrayStorage;
35
use Railt\Storage\Proxy;
36
use Railt\Storage\Storage;
37
38
/**
39
 * Class Compiler
40
 */
41
class Compiler implements CompilerInterface, Configuration
42
{
43
    use Support;
44
45
    /**
46
     * @var Dictionary
47
     */
48
    private $loader;
49
50
    /**
51
     * @var ParserInterface
52
     */
53
    private $parser;
54
55
    /**
56
     * @var Storage|ArrayStorage
57
     */
58
    private $storage;
59
60
    /**
61
     * @var Validator
62
     */
63
    private $typeValidator;
64
65
    /**
66
     * @var Factory|TypeCoercion
67
     */
68
    private $typeCoercion;
69
70
    /**
71
     * @var CallStack
72
     */
73
    private $stack;
74
75
    /**
76
     * Compiler constructor.
77
     * @param Storage|null $storage
78
     * @throws CompilerException
79
     * @throws Exceptions\TypeConflictException
80
     */
81 283
    public function __construct(Storage $storage = null)
82
    {
83 283
        $this->parser        = new Parser();
84 283
        $this->stack         = new CallStack();
85 283
        $this->loader        = new Loader($this, $this->stack);
86 283
        $this->typeValidator = new Validator($this->stack);
87 283
        $this->typeCoercion  = new Factory();
88
89 283
        $this->storage = $this->bootStorage($storage);
90
91 283
        $this->add($this->getStandardLibrary());
92 283
    }
93
94
    /**
95
     * @param Document $document
96
     * @return CompilerInterface
97
     * @throws \Railt\SDL\Exceptions\CompilerException
98
     * @throws Exceptions\TypeConflictException
99
     */
100 283
    public function add(Document $document): CompilerInterface
101
    {
102
        try {
103 283
            $this->complete($document);
104
        } catch (\OutOfBoundsException $fatal) {
105
            throw CompilerException::wrap($fatal);
106
        }
107
108 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...
109
    }
110
111
    /**
112
     * @param null|Storage $storage
113
     * @return Storage
114
     */
115 283
    private function bootStorage(?Storage $storage): Storage
116
    {
117 283
        if ($storage === null) {
118 283
            return new ArrayStorage();
119
        }
120
121 283
        if ($storage instanceof Proxy || $storage instanceof ArrayStorage) {
122 283
            return $storage;
123
        }
124
125 283
        return new Proxy(new ArrayStorage(), $storage);
126
    }
127
128
    /**
129
     * @param array $extensions
130
     * @return GraphQLDocument
131
     */
132 283
    private function getStandardLibrary(array $extensions = []): GraphQLDocument
133
    {
134 283
        return new GraphQLDocument($this->getDictionary(), $extensions);
135
    }
136
137
    /**
138
     * @param Document $document
139
     * @return Document
140
     * @throws Exceptions\TypeConflictException
141
     */
142 6577
    private function complete(Document $document): Document
143
    {
144 6577
        $this->load($document);
145
146 6577
        $build = function (Definition $definition): void {
147 6577
            $this->stack->push($definition);
148
149 6577
            if ($definition instanceof Compilable) {
150 6576
                $definition->compile();
151
            }
152
153 6571
            if ($definition instanceof TypeDefinition) {
154 6571
                $this->typeCoercion->apply($definition);
155
            }
156
157 6571
            if (! ($definition instanceof StandardType)) {
158 6567
                $this->typeValidator->group(Definitions::class)->validate($definition);
159
            }
160
161 6547
            $this->stack->pop();
162 6577
        };
163
164 6577
        foreach ($document->getDefinitions() as $definition) {
165 6577
            $build($definition);
166
        }
167
168 4327
        if ($document instanceof DocumentBuilder) {
169 4173
            foreach ($document->getInvocableTypes() as $definition) {
170 1170
                $build($definition);
171
            }
172
        }
173
174 3439
        return $document;
175
    }
176
177
    /**
178
     * @param Document $document
179
     * @return Document|DocumentBuilder
180
     * @throws Exceptions\TypeConflictException
181
     */
182 6577
    private function load(Document $document): Document
183
    {
184 6577
        foreach ($document->getTypeDefinitions() as $type) {
185 6577
            $this->stack->push($type);
186 6577
            $this->loader->register($type);
187 6577
            $this->stack->pop();
188
        }
189
190 6577
        return $document;
191
    }
192
193
    /**
194
     * @param \Closure $then
195
     * @return CompilerInterface
196
     */
197 6
    public function autoload(\Closure $then): CompilerInterface
198
    {
199 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...
200
201 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...
202
    }
203
204
    /**
205
     * @param Readable $readable
206
     * @return Document
207
     */
208 6576
    public function compile(Readable $readable): Document
209
    {
210
        /** @var DocumentBuilder $document */
211 6576
        $document = $this->storage->remember($readable, $this->onCompile());
212
213 3285
        return $document->withCompiler($this);
214
    }
215
216
    /**
217
     * @return \Closure
218
     */
219
    private function onCompile(): \Closure
220
    {
221 6576
        return function (Readable $readable): Document {
222 6576
            $ast = $this->parser->parse($readable);
223
224 6576
            return $this->complete(new DocumentBuilder($ast, $readable, $this));
225 6576
        };
226
    }
227
228
    /**
229
     * @param string $group
230
     * @return ValidatorInterface
231
     * @throws \OutOfBoundsException
232
     */
233 6576
    public function getValidator(string $group): ValidatorInterface
234
    {
235 6576
        return $this->typeValidator->group($group);
236
    }
237
238
    /**
239
     * @return ParserInterface
240
     */
241
    public function getParser(): ParserInterface
242
    {
243
        return $this->parser;
244
    }
245
246
    /**
247
     * @return TypeCoercion
248
     */
249 5975
    public function getTypeCoercion(): TypeCoercion
250
    {
251 5975
        return $this->typeCoercion;
252
    }
253
254
    /**
255
     * @return Storage
256
     */
257
    public function getStorage(): Storage
258
    {
259
        return $this->storage;
260
    }
261
262
    /**
263
     * @return Dictionary
264
     */
265 6565
    public function getDictionary(): Dictionary
266
    {
267 6565
        return $this->loader;
268
    }
269
270
    /**
271
     * @return CallStackInterface
272
     */
273 5975
    public function getCallStack(): CallStackInterface
274
    {
275 5975
        return $this->stack;
276
    }
277
}
278