Completed
Pull Request — master (#126)
by John
03:13
created

Parser::getFieldSet()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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 \ArrayAccess;
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 60
    public function __construct(
108
        ParserFactoryInterface $parserFactory,
109
        StackFactoryInterface $stackFactory,
110
        SchemaFactoryInterface $schemaFactory,
111
        ContainerInterface $container,
112
        ParserManagerInterface $manager
113
    ) {
114 60
        $this->manager       = $manager;
115 60
        $this->container     = $container;
116 60
        $this->stackFactory  = $stackFactory;
117 60
        $this->parserFactory = $parserFactory;
118 60
        $this->schemaFactory = $schemaFactory;
119 60
    }
120
121
    /**
122
     * @inheritdoc
123
     */
124 60
    public function parse($data)
125
    {
126 60
        $this->stack = $this->stackFactory->createStack();
127 60
        $rootFrame   = $this->stack->push();
128 60
        $rootFrame->setRelationship(
129 60
            $this->schemaFactory->createRelationshipObject(null, $data, [], null, true, true)
130 60
        );
131
132 60
        foreach ($this->parseData() as $parseReply) {
133 59
            yield $parseReply;
134 60
        }
135
136 59
        $this->stack = null;
137 59
    }
138
139
    /**
140
     * @return Iterator
141
     */
142 60
    private function parseData()
143
    {
144 60
        list($isEmpty, $isOriginallyArrayed, $traversableData) = $this->analyzeCurrentData();
145
146
        /** @var bool $isEmpty */
147
        /** @var bool $isOriginallyArrayed */
148
149 60
        if ($isEmpty === true) {
150 20
            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...
151 20
        } else {
152 54
            $curFrame = $this->stack->end();
153
154
            // if resource(s) is in primary data section (not in included)
155 54
            $isPrimary = $curFrame->getLevel() < 2;
156
157 54
            foreach ($traversableData as $resource) {
158 53
                $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 53
                $fieldSet       = $this->getFieldSet($schema->getResourceType());
160 53
                $resourceObject = $schema->createResourceObject($resource, $isOriginallyArrayed, $fieldSet);
161 53
                $isCircular     = $this->checkCircular($resourceObject);
162
163 53
                $this->stack->setCurrentResource($resourceObject);
164 53
                yield $this->createReplyResourceStarted();
165
166
                // duplicated are allowed in data however they shouldn't be in includes
167 53
                if ($isCircular === true && $isPrimary === false) {
168 7
                    continue;
169
                }
170
171 53
                if ($this->shouldParseRelationships() === true) {
172 53
                    $relationships     = $this->getIncludeRelationships();
173 53
                    $relObjectIterator = $schema->getRelationshipObjectIterator($resource, $isPrimary, $relationships);
174 53
                    foreach ($relObjectIterator as $relationship) {
175
                        /** @var RelationshipObjectInterface $relationship */
176 40
                        $nextFrame = $this->stack->push();
177 40
                        $nextFrame->setRelationship($relationship);
178
                        try {
179 40
                            if ($this->isRelationshipIncludedOrInFieldSet() === true) {
180 38
                                foreach ($this->parseData() as $parseResult) {
181 38
                                    yield $parseResult;
182 38
                                }
183 38
                            }
184 40
                        } finally {
185 40
                            $this->stack->pop();
186
                        }
187 53
                    }
188 52
                }
189
190 52
                yield $this->createReplyResourceCompleted();
191 53
            }
192
        }
193 60
    }
194
195
    /**
196
     * @return array
197
     */
198 60
    protected function analyzeCurrentData()
199
    {
200 60
        $relationship = $this->stack->end()->getRelationship();
201 60
        $data = $relationship->isShowData() === true ? $relationship->getData() : null;
202
203 60
        $isCollection    = true;
204 60
        $isEmpty         = true;
205 60
        $traversableData = null;
206
207 60
        $isOk = (is_array($data) === true || is_object($data) === true || $data === null || $data instanceof Iterator);
208 60
        $isOk ?: Exceptions::throwInvalidArgument('data', $data);
209
210 60
        if (is_array($data) === true || $data instanceof ArrayAccess) {
211
            /** @var array $data */
212 44
            $isEmpty = empty($data);
213 44
            $traversableData = $data;
214 60
        } elseif ($data instanceof Iterator) {
215
            /** @var Iterator $data */
216
            $data->rewind();
217
            $isEmpty = ($data->valid() === false);
218
            if ($isEmpty === false) {
219
                $traversableData = $data;
220
            } else {
221
                $traversableData = [];
222
            }
223 47
        } elseif (is_object($data) === true) {
224
            /** @var object $data */
225 45
            $isEmpty         = ($data === null);
226 45
            $isCollection    = false;
227 45
            $traversableData = [$data];
228 47
        } elseif ($data === null) {
229 12
            $isCollection = false;
230 12
            $isEmpty      = true;
231 12
        }
232
233 60
        return [$isEmpty, $isCollection, $traversableData];
234
    }
235
236
    /**
237
     * @param mixed                       $resource
238
     * @param StackFrameReadOnlyInterface $frame
239
     *
240
     * @return SchemaProviderInterface
241
     */
242 53
    private function getSchema($resource, StackFrameReadOnlyInterface $frame)
243
    {
244
        try {
245 53
            $schema = $this->container->getSchema($resource);
246 53
        } catch (InvalidArgumentException $exception) {
247 1
            $message = T::t('Schema is not registered for a resource at path \'%s\'.', [$frame->getPath()]);
248 1
            throw new InvalidArgumentException($message, 0, $exception);
249
        }
250
251 53
        return $schema;
252
    }
253
254
    /**
255
     * @param array|null $data
256
     *
257
     * @return ParserReplyInterface
258
     */
259 20
    private function createReplyForEmptyData($data)
260
    {
261 20
        ($data === null || (is_array($data) === true && empty($data) === true)) ?: Exceptions::throwLogicException();
262
263 20
        $replyType = ($data === null ? ParserReplyInterface::REPLY_TYPE_NULL_RESOURCE_STARTED :
264 20
            ParserReplyInterface::REPLY_TYPE_EMPTY_RESOURCE_STARTED);
265
266 20
        return $this->parserFactory->createEmptyReply($replyType, $this->stack);
267
    }
268
269
    /**
270
     * @return ParserReplyInterface
271
     */
272 53
    private function createReplyResourceStarted()
273
    {
274 53
        return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_STARTED, $this->stack);
275
    }
276
277
    /**
278
     * @return ParserReplyInterface
279
     */
280 52
    private function createReplyResourceCompleted()
281
    {
282 52
        return $this->parserFactory->createReply(ParserReplyInterface::REPLY_TYPE_RESOURCE_COMPLETED, $this->stack);
283
    }
284
285
    /**
286
     * @return bool
287
     */
288 53
    private function shouldParseRelationships()
289
    {
290 53
        return $this->manager->isShouldParseRelationships($this->stack);
291
    }
292
293
    /**
294
     * @return string[]
295
     */
296 53
    private function getIncludeRelationships()
297
    {
298 53
        return $this->manager->getIncludeRelationships($this->stack);
299
    }
300
301
    /**
302
     * @return bool
303
     */
304 40
    private function isRelationshipIncludedOrInFieldSet()
305
    {
306
        return
307 40
            $this->manager->isRelationshipInFieldSet($this->stack) === true ||
308 40
            $this->manager->isShouldParseRelationships($this->stack) === true;
309
    }
310
311
    /**
312
     * @param ResourceObjectInterface $resourceObject
313
     *
314
     * @return bool
315
     */
316 53
    private function checkCircular(ResourceObjectInterface $resourceObject)
317
    {
318 53
        foreach ($this->stack as $frame) {
319
            /** @var StackFrameReadOnlyInterface $frame */
320 53
            if (($stackResource = $frame->getResource()) !== null &&
321 53
                $stackResource->getId() === $resourceObject->getId() &&
322 53
                $stackResource->getType() === $resourceObject->getType()) {
323 11
                return true;
324
            }
325 53
        }
326 53
        return false;
327
    }
328
329
    /**
330
     * @param string $resourceType
331
     *
332
     * @return array <string, int>|null
333
     */
334 53
    private function getFieldSet($resourceType)
335
    {
336 53
        return $this->manager->getFieldSet($resourceType);
337
    }
338
}
339