Completed
Branch master (9dd8d4)
by
unknown
12:41
created

Parser::parseData()   C

Complexity

Conditions 9
Paths 5

Size

Total Lines 50
Code Lines 28

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 33
CRAP Score 9

Importance

Changes 10
Bugs 0 Features 2
Metric Value
c 10
b 0
f 2
dl 0
loc 50
ccs 33
cts 33
cp 1
rs 6
cc 9
eloc 28
nc 5
nop 0
crap 9
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 \InvalidArgumentException;
21
use \Neomerx\JsonApi\Factories\Exceptions;
22
use \Neomerx\JsonApi\I18n\Translator as T;
23
use \Neomerx\JsonApi\Contracts\Schema\ContainerInterface;
24
use \Neomerx\JsonApi\Contracts\Encoder\Stack\StackInterface;
25
use \Neomerx\JsonApi\Contracts\Schema\SchemaFactoryInterface;
26
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserInterface;
27
use \Neomerx\JsonApi\Contracts\Schema\ResourceObjectInterface;
28
use \Neomerx\JsonApi\Contracts\Schema\SchemaProviderInterface;
29
use \Neomerx\JsonApi\Contracts\Schema\RelationshipObjectInterface;
30
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserReplyInterface;
31
use \Neomerx\JsonApi\Contracts\Encoder\Stack\StackFactoryInterface;
32
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserFactoryInterface;
33
use \Neomerx\JsonApi\Contracts\Encoder\Parser\ParserManagerInterface;
34
use \Neomerx\JsonApi\Contracts\Encoder\Stack\StackFrameReadOnlyInterface;
35
36
/**
37
 * The main purpose of the parser is to reach **every resource** that is targeted for inclusion and its
38
 * relations if data schema describes them as 'included'. Parser manager is managing this decision making.
39
 * Parser helps to filter resource attributes at the moment of their creation.
40
 *   ^^^^
41
 *     This is 'sparse' JSON API feature and 'fields set' feature (for attributes)
42
 *
43
 * Parser does not decide if particular resource or its relationships are actually added to final JSON document.
44
 * Parsing reply interpreter does this job. Parser interpreter might not include some intermediate resources
45
 * that parser has found while reaching targets.
46
 *   ^^^^
47
 *     This is 'sparse' JSON API feature again and 'fields set' feature (for relationships)
48
 *
49
 * The final JSON view of an element is chosen by document which uses settings to decide if 'self', 'meta', and
50
 * other members should be rendered.
51
 *   ^^^^
52
 *     This is generic JSON API features
53
 *
54
 * Once again, it basically works this way:
55
 *   - Parser finds all targeted relationships and outputs them with all intermediate results (looks like a tree).
56
 *     Resource attributes are already filtered.
57
 *   - Reply interpreter filters intermediate results and resource relationships and then send it to document.
58
 *   - The document is just a renderer which saves the input data in one of a few variations depending on settings.
59
 *   - When all data are parsed the document converts collected data to json.
60
 *
61
 * @package Neomerx\JsonApi
62
 */
63
class Parser implements ParserInterface
64
{
65
    /**
66
     * @var ParserFactoryInterface
67
     */
68
    protected $parserFactory;
69
70
    /**
71
     * @var StackFactoryInterface
72
     */
73
    protected $stackFactory;
74
75
    /**
76
     * @var SchemaFactoryInterface
77
     */
78
    protected $schemaFactory;
79
80
    /**
81
     * @var StackInterface
82
     */
83
    protected $stack;
84
85
    /**
86
     * @var ParserManagerInterface
87
     */
88
    protected $manager;
89
90
    /**
91
     * @var ContainerInterface
92
     */
93
    protected $container;
94
95
    /**
96
     * @param ParserFactoryInterface $parserFactory
97
     * @param StackFactoryInterface  $stackFactory
98
     * @param SchemaFactoryInterface $schemaFactory
99
     * @param ContainerInterface     $container
100
     * @param ParserManagerInterface $manager
101
     */
102 57
    public function __construct(
103
        ParserFactoryInterface $parserFactory,
104
        StackFactoryInterface $stackFactory,
105
        SchemaFactoryInterface $schemaFactory,
106
        ContainerInterface $container,
107
        ParserManagerInterface $manager
108 1
    ) {
109 57
        $this->manager       = $manager;
110 57
        $this->container     = $container;
111 57
        $this->stackFactory  = $stackFactory;
112 57
        $this->parserFactory = $parserFactory;
113 57
        $this->schemaFactory = $schemaFactory;
114 57
    }
115
116
    /**
117
     * @inheritdoc
118
     */
119 57
    public function parse($data)
120
    {
121 57
        $this->stack = $this->stackFactory->createStack();
122 57
        $rootFrame   = $this->stack->push();
123 57
        $rootFrame->setRelationship(
124 57
            $this->schemaFactory->createRelationshipObject(null, $data, [], null, true, true)
125 57
        );
126
127 57
        foreach ($this->parseData() as $parseReply) {
128 57
            yield $parseReply;
129 57
        }
130
131 56
        $this->stack = null;
132 56
    }
133
134
    /**
135
     * @return Iterator
1 ignored issue
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use \Generator.

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...
136
     */
137 57
    private function parseData()
138
    {
139 57
        list($isEmpty, $isOriginallyArrayed, $traversableData) = $this->analyzeCurrentData();
140
141
        /** @var bool $isEmpty */
142
        /** @var bool $isOriginallyArrayed */
143
144 57
        if ($isEmpty === true) {
145 19
            yield $this->createReplyForEmptyData($traversableData);
0 ignored issues
show
Bug introduced by
It seems like $traversableData can also be of type object<Iterator>; 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...
146 19
        } else {
147 50
            $curFrame = $this->stack->end();
148
149
            // duplicated are allowed in data however they shouldn't be in includes
150 50
            $isDupAllowed = $curFrame->getLevel() < 2;
151
152 50
            foreach ($traversableData as $resource) {
153 50
                $schema         = $this->getSchema($resource, $curFrame);
0 ignored issues
show
Bug introduced by
It seems like $curFrame defined by $this->stack->end() on line 147 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...
154 50
                $fieldSet       = $this->getFieldSet($schema->getResourceType());
155 50
                $resourceObject = $schema->createResourceObject($resource, $isOriginallyArrayed, $fieldSet);
156 50
                $isCircular     = $this->checkCircular($resourceObject);
157
158 50
                $this->stack->setCurrentResource($resourceObject);
159 50
                yield $this->createReplyResourceStarted();
160
161 50
                if ($isCircular === true && $isDupAllowed === false) {
162 6
                    continue;
163
                }
164
165 50
                if ($this->shouldParseRelationships() === true) {
166 50
                    $relationships = $this->getIncludeRelationships();
167 50
                    foreach ($schema->getRelationshipObjectIterator($resource, $relationships) as $relationship) {
168
                        /** @var RelationshipObjectInterface $relationship */
169 37
                        $nextFrame = $this->stack->push();
170 37
                        $nextFrame->setRelationship($relationship);
171
                        try {
172 37
                            if ($this->isRelationshipInFieldSet() === true) {
173 33
                                foreach ($this->parseData() as $parseResult) {
174 33
                                    yield $parseResult;
175 33
                                }
176 33
                            }
177 37
                        } finally {
178 37
                            $this->stack->pop();
179
                        }
180 50
                    }
181 49
                }
182
183 49
                yield $this->createReplyResourceCompleted();
184 49
            }
185
        }
186 57
    }
187
188
    /**
189
     * @return array
190
     */
191 57
    protected function analyzeCurrentData()
192
    {
193 57
        $relationship = $this->stack->end()->getRelationship();
194 57
        $data = $relationship->isShowData() === true ? $relationship->getData() : null;
195
196 57
        $isCollection    = true;
197 57
        $isEmpty         = true;
198 57
        $traversableData = null;
199
200 57
        $isOk = (is_array($data) === true || is_object($data) === true || $data === null || $data instanceof Iterator);
201 57
        $isOk ?: Exceptions::throwInvalidArgument('data', $data);
202
203 57
        if (is_array($data) === true) {
204
            /** @var array $data */
205 39
            $isEmpty = empty($data);
206 39
            $traversableData = $data;
207 57
        } elseif ($data instanceof Iterator) {
208
            /** @var Iterator $data */
209 2
            $data->rewind();
210 2
            $isEmpty = ($data->valid() === false);
211 2
            if ($isEmpty === false) {
212 1
                $traversableData = $data;
213 1
            } else {
214 1
                $traversableData = [];
215
            }
216 45
        } elseif (is_object($data) === true) {
217
            /** @var object $data */
218 41
            $isEmpty         = ($data === null);
219 41
            $isCollection    = false;
220 41
            $traversableData = [$data];
221 43
        } elseif ($data === null) {
222 10
            $isCollection = false;
223 10
            $isEmpty      = true;
224 10
        }
225
226 57
        return [$isEmpty, $isCollection, $traversableData];
227
    }
228
229
    /**
230
     * @param mixed                       $resource
231
     * @param StackFrameReadOnlyInterface $frame
232
     *
233
     * @return SchemaProviderInterface
234
     */
235 50
    private function getSchema($resource, StackFrameReadOnlyInterface $frame)
236
    {
237
        try {
238 50
            $schema = $this->container->getSchema($resource);
239 50
        } catch (InvalidArgumentException $exception) {
240 1
            $message = T::t('Schema is not registered for a resource at path \'%s\'.', [$frame->getPath()]);
241 1
            throw new InvalidArgumentException($message, 0, $exception);
242
        }
243
244 50
        return $schema;
245
    }
246
247
    /**
248
     * @param array|null $data
249
     *
250
     * @return ParserReplyInterface
251
     */
252 19
    private function createReplyForEmptyData($data)
253
    {
254 19
        ($data === null || (is_array($data) === true && empty($data) === true)) ?: Exceptions::throwLogicException();
255
256 19
        $replyType = ($data === null ? ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED :
257 19
            ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED);
258
259 19
        return $this->parserFactory->createEmptyReply($replyType, $this->stack);
260
    }
261
262
    /**
263
     * @return ParserReplyInterface
264
     */
265 50
    private function createReplyResourceStarted()
266
    {
267 50
        return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED, $this->stack);
268
    }
269
270
    /**
271
     * @return ParserReplyInterface
272
     */
273 49
    private function createReplyResourceCompleted()
274
    {
275 49
        return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED, $this->stack);
276
    }
277
278
    /**
279
     * @return bool
280
     */
281 50
    private function shouldParseRelationships()
282
    {
283 50
        return $this->manager->isShouldParseRelationships($this->stack);
284
    }
285
286
    /**
287
     * @return string[]
288
     */
289 50
    private function getIncludeRelationships()
290
    {
291 50
        return $this->manager->getIncludeRelationships($this->stack);
292
    }
293
294
    /**
295
     * @return bool
296
     */
297 37
    private function isRelationshipInFieldSet()
298
    {
299 37
        return $this->manager->isRelationshipInFieldSet($this->stack);
300
    }
301
302
    /**
303
     * @param ResourceObjectInterface $resourceObject
304
     *
305
     * @return bool
306
     */
307 50
    private function checkCircular(ResourceObjectInterface $resourceObject)
308
    {
309 50
        foreach ($this->stack as $frame) {
310
            /** @var StackFrameReadOnlyInterface $frame */
311 50
            if (($stackResource = $frame->getResource()) !== null &&
312 50
                $stackResource->getId() === $resourceObject->getId() &&
313 50
                $stackResource->getType() === $resourceObject->getType()) {
314 10
                return true;
315
            }
316 50
        }
317 50
        return false;
318
    }
319
320
    /**
321
     * @param string $resourceType
322
     *
323
     * @return array <string, int>|null
324
     */
325 50
    private function getFieldSet($resourceType)
326
    {
327 50
        return $this->manager->getFieldSet($resourceType);
328
    }
329
}
330