Completed
Branch master (401508)
by
unknown
06:29
created

Parser::getSchema()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 6
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 11
ccs 6
cts 6
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 7
nc 2
nop 2
crap 2
1
<?php namespace Neomerx\JsonApi\Encoder\Parser;
2
3
/**
4
 * Copyright 2015 [email protected] (www.neomerx.com)
5
 *
6
 * Licensed under the Apache License, Version 2.0 (the "License");
7
 * you may not use this file except in compliance with the License.
8
 * You may obtain a copy of the License at
9
 *
10
 * http://www.apache.org/licenses/LICENSE-2.0
11
 *
12
 * Unless required by applicable law or agreed to in writing, software
13
 * distributed under the License is distributed on an "AS IS" BASIS,
14
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
 * See the License for the specific language governing permissions and
16
 * limitations under the License.
17
 */
18
19
use \Iterator;
20
use \IteratorAggregate;
21
use \InvalidArgumentException;
22
use \Psr\Log\LoggerAwareTrait;
23
use \Psr\Log\LoggerAwareInterface;
24
use \Neomerx\JsonApi\Factories\Exceptions;
25
use \Neomerx\JsonApi\I18n\Translator as T;
26
use \Neomerx\JsonApi\Contracts\Schema\ContainerInterface;
27
use \Neomerx\JsonApi\Contracts\Encoder\Stack\StackInterface;
28
use \Neomerx\JsonApi\Contracts\Schema\SchemaFactoryInterface;
29
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserInterface;
30
use \Neomerx\JsonApi\Contracts\Schema\ResourceObjectInterface;
31
use \Neomerx\JsonApi\Contracts\Schema\SchemaProviderInterface;
32
use \Neomerx\JsonApi\Contracts\Schema\RelationshipObjectInterface;
33
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserReplyInterface;
34
use \Neomerx\JsonApi\Contracts\Encoder\Stack\StackFactoryInterface;
35
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserFactoryInterface;
36
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserManagerInterface;
37
use \Neomerx\JsonApi\Contracts\Encoder\Stack\StackFrameReadOnlyInterface;
38
39
/**
40
 * The main purpose of the parser is to reach **every resource** that is targeted for inclusion and its
41
 * relations if data schema describes them as 'included'. Parser manager is managing this decision making.
42
 * Parser helps to filter resource attributes at the moment of their creation.
43
 *   ^^^^
44
 *     This is 'sparse' JSON API feature and 'fields set' feature (for attributes)
45
 *
46
 * Parser does not decide if particular resource or its relationships are actually added to final JSON document.
47
 * Parsing reply interpreter does this job. Parser interpreter might not include some intermediate resources
48
 * that parser has found while reaching targets.
49
 *   ^^^^
50
 *     This is 'sparse' JSON API feature again and 'fields set' feature (for relationships)
51
 *
52
 * The final JSON view of an element is chosen by document which uses settings to decide if 'self', 'meta', and
53
 * other members should be rendered.
54
 *   ^^^^
55
 *     This is generic JSON API features
56
 *
57
 * Once again, it basically works this way:
58
 *   - Parser finds all targeted relationships and outputs them with all intermediate results (looks like a tree).
59
 *     Resource attributes are already filtered.
60
 *   - Reply interpreter filters intermediate results and resource relationships and then send it to document.
61
 *   - The document is just a renderer which saves the input data in one of a few variations depending on settings.
62
 *   - When all data are parsed the document converts collected data to json.
63
 *
64
 * @package Neomerx\JsonApi
65
 */
66
class Parser implements ParserInterface, LoggerAwareInterface
67
{
68
    use LoggerAwareTrait;
69
70
    /**
71
     * @var ParserFactoryInterface
72
     */
73
    protected $parserFactory;
74
75
    /**
76
     * @var StackFactoryInterface
77
     */
78
    protected $stackFactory;
79
80
    /**
81
     * @var SchemaFactoryInterface
82
     */
83
    protected $schemaFactory;
84
85
    /**
86
     * @var StackInterface
87
     */
88
    protected $stack;
89
90
    /**
91
     * @var ParserManagerInterface
92
     */
93
    protected $manager;
94
95
    /**
96
     * @var ContainerInterface
97
     */
98
    protected $container;
99
100
    /**
101
     * @param ParserFactoryInterface $parserFactory
102
     * @param StackFactoryInterface  $stackFactory
103
     * @param SchemaFactoryInterface $schemaFactory
104
     * @param ContainerInterface     $container
105
     * @param ParserManagerInterface $manager
106
     */
107 65
    public function __construct(
108
        ParserFactoryInterface $parserFactory,
109
        StackFactoryInterface $stackFactory,
110
        SchemaFactoryInterface $schemaFactory,
111
        ContainerInterface $container,
112
        ParserManagerInterface $manager
113
    ) {
114 65
        $this->manager       = $manager;
115 65
        $this->container     = $container;
116 65
        $this->stackFactory  = $stackFactory;
117 65
        $this->parserFactory = $parserFactory;
118 65
        $this->schemaFactory = $schemaFactory;
119 65
    }
120
121
    /**
122
     * @inheritdoc
123
     */
124 65
    public function parse($data)
125
    {
126 65
        $this->stack = $this->stackFactory->createStack();
127 65
        $rootFrame   = $this->stack->push();
128 65
        $rootFrame->setRelationship(
129 65
            $this->schemaFactory->createRelationshipObject(null, $data, [], null, true, true)
130 65
        );
131
132 65
        foreach ($this->parseData() as $parseReply) {
133 65
            yield $parseReply;
134 65
        }
135
136 64
        $this->stack = null;
137 64
    }
138
139
    /**
140
     * @return Iterator
141
     */
142 65
    private function parseData()
143
    {
144 65
        list($isEmpty, $isOriginallyArrayed, $traversableData) = $this->analyzeCurrentData();
145
146
        /** @var bool $isEmpty */
147
        /** @var bool $isOriginallyArrayed */
148
149 65
        if ($isEmpty === true) {
150 24
            yield $this->createReplyForEmptyData($traversableData);
0 ignored issues
show
Bug introduced by
It seems like $traversableData can also be of type object; however, Neomerx\JsonApi\Encoder\...eateReplyForEmptyData() does only seem to accept array|null, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
151 24
        } else {
152 58
            $curFrame = $this->stack->end();
153
154
            // if resource(s) is in primary data section (not in included)
155 58
            $isPrimary = $curFrame->getLevel() < 2;
156
157 58
            foreach ($traversableData as $resource) {
0 ignored issues
show
Bug introduced by
The expression $traversableData of type null|object|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
158 58
                $schema         = $this->getSchema($resource, $curFrame);
0 ignored issues
show
Bug introduced by
It seems like $curFrame defined by $this->stack->end() on line 152 can be null; however, Neomerx\JsonApi\Encoder\Parser\Parser::getSchema() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
159 58
                $fieldSet       = $this->getFieldSet($schema->getResourceType());
160 58
                $resourceObject = $schema->createResourceObject($resource, $isOriginallyArrayed, $fieldSet);
161 58
                $isCircular     = $this->checkCircular($resourceObject);
162
163 58
                $this->stack->setCurrentResource($resourceObject);
164 58
                yield $this->createReplyResourceStarted();
165
166
                // duplicated are allowed in data however they shouldn't be in includes
167 58
                if ($isCircular === true && $isPrimary === false) {
168 7
                    continue;
169
                }
170
171 58
                if ($this->shouldParseRelationships() === true) {
172 58
                    $relationships     = $this->getIncludeRelationships();
173 58
                    $relObjectIterator = $schema->getRelationshipObjectIterator($resource, $isPrimary, $relationships);
174 58
                    foreach ($relObjectIterator as $relationship) {
175
                        /** @var RelationshipObjectInterface $relationship */
176 44
                        $nextFrame = $this->stack->push();
177 44
                        $nextFrame->setRelationship($relationship);
178
                        try {
179 44
                            if ($this->isRelationshipIncludedOrInFieldSet() === true) {
180 41
                                foreach ($this->parseData() as $parseResult) {
181 41
                                    yield $parseResult;
182 41
                                }
183 41
                            }
184 44
                        } finally {
185 44
                            $this->stack->pop();
186
                        }
187 58
                    }
188 57
                }
189
190 57
                yield $this->createReplyResourceCompleted();
191 57
            }
192
        }
193 65
    }
194
195
    /**
196
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<boolean|null|object|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
197
     */
198 65
    protected function analyzeCurrentData()
199
    {
200 65
        $data   = $this->getCurrentData();
201 65
        $result = $this->analyzeData($data);
202
203 65
        return $result;
204
    }
205
206
    /**
207
     * @return array|null|object
208
     */
209 65
    protected function getCurrentData()
210
    {
211 65
        $relationship = $this->stack->end()->getRelationship();
212 65
        $data         = $relationship->isShowData() === true ? $relationship->getData() : null;
213
214 65
        return $data;
215
    }
216
217
    /**
218
     * @param array|null|object $data
219
     *
220
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<boolean|null|object|array>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
221
     */
222 65
    protected function analyzeData($data)
223
    {
224 65
        $isCollection    = true;
225 65
        $isEmpty         = true;
226 65
        $traversableData = null;
227
228 65
        $isOk = (is_array($data) === true || is_object($data) === true || $data === null);
229 65
        $isOk ?: Exceptions::throwInvalidArgument('data', $data);
230
231 65
        if (is_array($data) === true) {
232
            /** @var array $data */
233 42
            $isEmpty = empty($data);
234 42
            $traversableData = $data;
235 65
        } elseif (($data instanceof Iterator && ($iterator = $data) !== null) ||
236 52
            ($data instanceof IteratorAggregate && ($iterator = $data->getIterator()) !== null)
237 54
        ) {
238
            /** @var Iterator $iterator */
239 3
            $iterator->rewind();
240 3
            $isEmpty = ($iterator->valid() === false);
241 3
            if ($isEmpty === false) {
242 2
                $traversableData = $data;
243 2
            } else {
244 1
                $traversableData = [];
245
            }
246 54
        } elseif (is_object($data) === true) {
247
            /** @var object $data */
248 49
            $isEmpty         = ($data === null);
249 49
            $isCollection    = false;
250 49
            $traversableData = [$data];
251 51
        } elseif ($data === null) {
252 15
            $isCollection = false;
253 15
            $isEmpty      = true;
254 15
        }
255
256 65
        return [$isEmpty, $isCollection, $traversableData];
257
    }
258
259
    /**
260
     * @param mixed                       $resource
261
     * @param StackFrameReadOnlyInterface $frame
262
     *
263
     * @return SchemaProviderInterface
264
     */
265 58
    private function getSchema($resource, StackFrameReadOnlyInterface $frame)
266
    {
267
        try {
268 58
            $schema = $this->container->getSchema($resource);
269 58
        } catch (InvalidArgumentException $exception) {
270 1
            $message = T::t('Schema is not registered for a resource at path \'%s\'.', [$frame->getPath()]);
271 1
            throw new InvalidArgumentException($message, 0, $exception);
272
        }
273
274 58
        return $schema;
275
    }
276
277
    /**
278
     * @param array|null $data
279
     *
280
     * @return ParserReplyInterface
281
     */
282 24
    private function createReplyForEmptyData($data)
283
    {
284 24
        ($data === null || (is_array($data) === true && empty($data) === true)) ?: Exceptions::throwLogicException();
285
286 24
        $replyType = ($data === null ? ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED :
287 24
            ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED);
288
289 24
        return $this->parserFactory->createEmptyReply($replyType, $this->stack);
290
    }
291
292
    /**
293
     * @return ParserReplyInterface
294
     */
295 58
    private function createReplyResourceStarted()
296
    {
297 58
        return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED, $this->stack);
298
    }
299
300
    /**
301
     * @return ParserReplyInterface
302
     */
303 57
    private function createReplyResourceCompleted()
304
    {
305 57
        return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED, $this->stack);
306
    }
307
308
    /**
309
     * @return bool
310
     */
311 58
    private function shouldParseRelationships()
312
    {
313 58
        return $this->manager->isShouldParseRelationships($this->stack);
314
    }
315
316
    /**
317
     * @return string[]
318
     */
319 58
    private function getIncludeRelationships()
320
    {
321 58
        return $this->manager->getIncludeRelationships($this->stack);
322
    }
323
324
    /**
325
     * @return bool
326
     */
327 44
    private function isRelationshipIncludedOrInFieldSet()
328
    {
329
        return
330 44
            $this->manager->isRelationshipInFieldSet($this->stack) === true ||
331 44
            $this->manager->isShouldParseRelationships($this->stack) === true;
332
    }
333
334
    /**
335
     * @param ResourceObjectInterface $resourceObject
336
     *
337
     * @return bool
338
     */
339 58
    private function checkCircular(ResourceObjectInterface $resourceObject)
340
    {
341 58
        foreach ($this->stack as $frame) {
342
            /** @var StackFrameReadOnlyInterface $frame */
343 58
            if (($stackResource = $frame->getResource()) !== null &&
344 58
                $stackResource->getId() === $resourceObject->getId() &&
345 58
                $stackResource->getType() === $resourceObject->getType()) {
346 11
                return true;
347
            }
348 58
        }
349 58
        return false;
350
    }
351
352
    /**
353
     * @param string $resourceType
354
     *
355
     * @return array <string, int>|null
356
     */
357 58
    private function getFieldSet($resourceType)
358
    {
359 58
        return $this->manager->getFieldSet($resourceType);
360
    }
361
}
362