Complex classes like Parser often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Parser, and based on these observations, apply Extract Interface, too.
1 | <?php namespace Neomerx\JsonApi\Encoder\Parser; |
||
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
|
|||
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); |
|
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); |
|
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() |
|
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 && |
|
319 | |||
320 | /** |
||
321 | * @param string $resourceType |
||
322 | * |
||
323 | * @return array <string, int>|null |
||
324 | */ |
||
325 | 50 | private function getFieldSet($resourceType) |
|
329 | } |
||
330 |
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.