Complex classes like MethodPattern 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 MethodPattern, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
15 | class MethodPattern implements PatternInterface { |
||
16 | |||
17 | const OUTPUT_BODY = 0; |
||
18 | |||
19 | const OUTPUT_FULL = 1; |
||
20 | |||
21 | const OUTPUT_DOC_COMMENT = 2; |
||
22 | |||
23 | |||
24 | /** |
||
25 | * @var QueryStrategy |
||
26 | */ |
||
27 | private $nameQuery; |
||
28 | |||
29 | /** |
||
30 | * @var callable[] |
||
31 | */ |
||
32 | private $modifierChecker = []; |
||
33 | |||
34 | /** |
||
35 | * @var callable |
||
36 | */ |
||
37 | private $bodyChecker; |
||
38 | |||
39 | /** |
||
40 | * @var callable |
||
41 | */ |
||
42 | private $docCommentChecker; |
||
43 | |||
44 | /** |
||
45 | * @var int |
||
46 | */ |
||
47 | private $outputType = self::OUTPUT_BODY; |
||
48 | |||
49 | /** |
||
50 | * @var ParametersPattern |
||
51 | */ |
||
52 | private $argumentsPattern; |
||
53 | |||
54 | |||
55 | /** |
||
56 | * |
||
57 | */ |
||
58 | 27 | public function __construct() { |
|
73 | |||
74 | |||
75 | /** |
||
76 | * @return $this |
||
77 | */ |
||
78 | 3 | public function outputFull() { |
|
82 | |||
83 | |||
84 | /** |
||
85 | * @return $this |
||
86 | */ |
||
87 | 3 | public function outputBody() { |
|
91 | |||
92 | |||
93 | /** |
||
94 | * @return $this |
||
95 | */ |
||
96 | 3 | public function outputDocComment() { |
|
100 | |||
101 | |||
102 | /** |
||
103 | * @param string|QueryStrategy $name |
||
104 | * @return $this |
||
105 | */ |
||
106 | 27 | public function withName($name) { |
|
107 | 27 | if (is_string($name)) { |
|
108 | 3 | $this->nameQuery = Strict::create()->valueIs($name); |
|
109 | 18 | } elseif ($name instanceof QueryStrategy) { |
|
110 | 27 | $this->nameQuery = $name; |
|
111 | 18 | } else { |
|
112 | 3 | throw new \InvalidArgumentException('Invalid name format. Expect string or Query'); |
|
113 | } |
||
114 | |||
115 | 27 | return $this; |
|
116 | } |
||
117 | |||
118 | |||
119 | /** |
||
120 | * @param callable $check |
||
121 | * @return $this |
||
122 | */ |
||
123 | 27 | public function withBody(callable $check) { |
|
124 | 27 | $this->bodyChecker = $check; |
|
125 | 27 | return $this; |
|
126 | } |
||
127 | |||
128 | |||
129 | /** |
||
130 | * @param Collection $body |
||
131 | * @return bool |
||
132 | * @throws \Exception |
||
133 | */ |
||
134 | 24 | private function isValidBody(Collection $body) { |
|
135 | 24 | $checker = $this->bodyChecker; |
|
136 | 24 | $result = $checker($body); |
|
137 | 24 | if (!is_bool($result)) { |
|
138 | throw new \Exception('Body checker should return boolean result'); |
||
139 | } |
||
140 | |||
141 | 24 | return $result; |
|
142 | } |
||
143 | |||
144 | |||
145 | /** |
||
146 | * @param callable $check |
||
147 | * @return $this |
||
148 | */ |
||
149 | 27 | public function withDocComment(callable $check = null) { |
|
150 | |||
151 | 27 | if ($check === null) { |
|
152 | $check = function (Token $token) { |
||
153 | 3 | return $token->getType() === T_DOC_COMMENT; |
|
154 | 3 | }; |
|
155 | 2 | } |
|
156 | 27 | $this->docCommentChecker = $check; |
|
157 | |||
158 | 27 | return $this; |
|
159 | } |
||
160 | |||
161 | |||
162 | /** |
||
163 | * Find functions without doc comments |
||
164 | */ |
||
165 | 3 | public function withoutDocComment() { |
|
172 | |||
173 | |||
174 | /** |
||
175 | * @param ParametersPattern $pattern |
||
176 | * @return $this |
||
177 | */ |
||
178 | 3 | public function withParameters(ParametersPattern $pattern) { |
|
179 | 3 | $this->argumentsPattern = $pattern; |
|
180 | 3 | return $this; |
|
181 | } |
||
182 | |||
183 | |||
184 | /** |
||
185 | * @return $this |
||
186 | */ |
||
187 | 27 | public function withAnyModifier() { |
|
188 | 27 | $this->modifierChecker = []; |
|
189 | $this->modifierChecker[] = function () { |
||
190 | 24 | return true; |
|
191 | }; |
||
192 | 27 | return $this; |
|
193 | } |
||
194 | |||
195 | |||
196 | /** |
||
197 | * @param string $modifier |
||
198 | * @return $this |
||
199 | */ |
||
200 | 3 | public function withModifier($modifier) { |
|
207 | |||
208 | |||
209 | /** |
||
210 | * @param string $modifier |
||
211 | * @return $this |
||
212 | */ |
||
213 | public function withoutModifier($modifier) { |
||
221 | |||
222 | |||
223 | /** |
||
224 | * @param array $modifiers Array<String> |
||
225 | * @return bool |
||
226 | * @throws \Exception |
||
227 | */ |
||
228 | 24 | private function isValidModifiers(array $modifiers) { |
|
242 | |||
243 | |||
244 | /** |
||
245 | * @param QuerySequence $querySequence |
||
246 | * @return Collection|null |
||
247 | */ |
||
248 | 24 | public function __invoke(QuerySequence $querySequence) { |
|
249 | 8 | static $availableModifiers = [ |
|
250 | T_STATIC, |
||
251 | T_PRIVATE, |
||
252 | T_PUBLIC, |
||
253 | T_ABSTRACT, |
||
254 | T_FINAL, |
||
255 | 16 | ]; |
|
256 | |||
257 | |||
258 | # detect function |
||
259 | 24 | $functionKeyword = $querySequence->strict('function'); |
|
260 | 24 | $querySequence->strict(T_WHITESPACE); |
|
261 | 24 | $querySequence->process($this->nameQuery); |
|
262 | 24 | $arguments = $querySequence->section('(', ')'); |
|
263 | 24 | $querySequence->possible(T_WHITESPACE); |
|
264 | 24 | $body = $querySequence->section('{', '}'); |
|
265 | |||
266 | 24 | if (!$querySequence->isValid()) { |
|
267 | 24 | return null; |
|
268 | } |
||
269 | |||
270 | 24 | $collection = $querySequence->getCollection(); |
|
271 | 24 | $start = $collection->extractByTokens($collection->getFirst(), $functionKeyword); |
|
272 | 24 | $start->slice(0, -1); // remove last function keyword |
|
273 | |||
274 | # start reverse search |
||
275 | 24 | $items = array_reverse($start->getItems()); |
|
276 | 24 | $startFrom = null; |
|
277 | |||
278 | 24 | $docComment = new Token(); |
|
279 | |||
280 | 24 | $modifiers = []; |
|
281 | |||
282 | |||
283 | /** @var Token[] $items */ |
||
284 | 24 | foreach ($items as $item) { |
|
285 | |||
286 | 24 | if ($item->getType() === T_WHITESPACE) { |
|
287 | 21 | $startFrom = $item; |
|
288 | 21 | continue; |
|
289 | } |
||
290 | |||
291 | 24 | if ($item->getType() === T_DOC_COMMENT and $docComment->isValid() === false) { |
|
292 | # Detect only first doc comment |
||
293 | 6 | $startFrom = $item; |
|
294 | 6 | $docComment = $item; |
|
295 | 6 | continue; |
|
296 | } |
||
297 | |||
298 | |||
299 | 24 | if (in_array($item->getType(), $availableModifiers)) { |
|
300 | 15 | $startFrom = $item; |
|
301 | 15 | $modifiers[] = $item->getValue(); |
|
302 | 15 | continue; |
|
303 | } |
||
304 | |||
305 | 24 | break; |
|
306 | 16 | } |
|
307 | |||
308 | 24 | if ($this->isValidModifiers($modifiers) === false) { |
|
309 | 3 | return null; |
|
310 | } |
||
311 | |||
312 | 24 | if ($this->isValidBody($body) === false) { |
|
313 | 3 | return null; |
|
314 | } |
||
315 | |||
316 | 24 | if ($this->isValidDocComment($docComment) === false) { |
|
317 | 3 | return null; |
|
318 | } |
||
319 | |||
320 | 24 | if ($this->isValidArguments($arguments) === false) { |
|
321 | 3 | return null; |
|
322 | } |
||
323 | |||
324 | 24 | if ($startFrom === null) { |
|
325 | 3 | $startFrom = $functionKeyword; |
|
326 | 2 | } |
|
327 | |||
328 | |||
329 | 24 | if ($this->outputType === self::OUTPUT_FULL) { |
|
330 | # all conditions are ok, so extract full function |
||
331 | 3 | $fullFunction = $collection->extractByTokens($startFrom, $body->getLast()); |
|
332 | 3 | if ($fullFunction->getFirst()->getType() === T_WHITESPACE) { |
|
333 | 3 | $fullFunction->slice(1); |
|
334 | 2 | } |
|
335 | 3 | return $fullFunction; |
|
336 | |||
337 | 24 | } elseif ($this->outputType === self::OUTPUT_DOC_COMMENT) { |
|
338 | 3 | return new Collection([$docComment]); |
|
339 | } |
||
340 | |||
341 | # body by default |
||
342 | 24 | $body->slice(0, -1); |
|
343 | 24 | return $body; |
|
344 | } |
||
345 | |||
346 | |||
347 | /** |
||
348 | * @param Token $token |
||
349 | * @return mixed |
||
350 | * @throws \Exception |
||
351 | */ |
||
352 | 24 | private function isValidDocComment(Token $token) { |
|
361 | |||
362 | |||
363 | /** |
||
364 | * @param Collection $parameters |
||
365 | * @return bool |
||
366 | */ |
||
367 | 24 | private function isValidArguments(Collection $parameters) { |
|
376 | |||
377 | |||
378 | } |