Complex classes like JSONText 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 JSONText, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
34 | class JSONText extends \StringField |
||
35 | { |
||
36 | /** |
||
37 | * @var int |
||
38 | */ |
||
39 | const JSONTEXT_QUERY_OPERATOR = 1; |
||
40 | |||
41 | /** |
||
42 | * @var int |
||
43 | */ |
||
44 | const JSONTEXT_QUERY_JSONPATH = 2; |
||
45 | |||
46 | /** |
||
47 | * Which RDBMS backend are we using? The value set here changes the actual operators and operator-routines for the |
||
48 | * given backend. |
||
49 | * |
||
50 | * @var string |
||
51 | * @config |
||
52 | */ |
||
53 | private static $backend = 'postgres'; |
||
54 | |||
55 | /** |
||
56 | * @var array |
||
57 | * @config |
||
58 | * |
||
59 | * [<backend>] => [ |
||
60 | * [<method> => <operator>] |
||
61 | * ]; // For use in query() method. |
||
62 | */ |
||
63 | private static $allowed_operators = [ |
||
|
|||
64 | 'postgres' => [ |
||
65 | 'matchOnInt' => '->', |
||
66 | 'matchOnStr' => '->>', |
||
67 | 'matchOnPath' => '#>' |
||
68 | ] |
||
69 | ]; |
||
70 | |||
71 | /** |
||
72 | * Legitimate query return types. |
||
73 | * |
||
74 | * @var array |
||
75 | */ |
||
76 | private static $return_types = [ |
||
77 | 'json', 'array', 'silverstripe' |
||
78 | ]; |
||
79 | |||
80 | /** |
||
81 | * @var string |
||
82 | */ |
||
83 | protected $returnType = 'json'; |
||
84 | |||
85 | /** |
||
86 | * @var \Peekmo\JsonPath\JsonStore |
||
87 | */ |
||
88 | protected $jsonStore; |
||
89 | |||
90 | /** |
||
91 | * Taken from {@link Text}. |
||
92 | * |
||
93 | * @see DBField::requireField() |
||
94 | * @return void |
||
95 | */ |
||
96 | public function requireField() |
||
118 | |||
119 | /** |
||
120 | * @param string $title |
||
121 | * @return \HiddenField |
||
122 | */ |
||
123 | public function scaffoldSearchField($title = null) |
||
127 | |||
128 | /** |
||
129 | * @param string $title |
||
130 | * @return \HiddenField |
||
131 | */ |
||
132 | public function scaffoldFormField($title = null) |
||
136 | |||
137 | /** |
||
138 | * Tell all class methods to return data as JSON , an array or an array of SilverStripe DBField subtypes. |
||
139 | * |
||
140 | * @param string $type |
||
141 | * @return JSONText |
||
142 | * @throws \JSONText\Exceptions\JSONTextException |
||
143 | */ |
||
144 | public function setReturnType($type) |
||
155 | |||
156 | /** |
||
157 | * Returns the value of this field as an iterable. |
||
158 | * |
||
159 | * @return \Peekmo\JsonPath\JsonStore |
||
160 | * @throws \JSONText\Exceptions\JSONTextException |
||
161 | */ |
||
162 | public function getJSONStore() |
||
177 | |||
178 | /** |
||
179 | * Returns the JSON value of this field as an array. |
||
180 | * |
||
181 | * @return array |
||
182 | */ |
||
183 | public function getStoreAsArray() |
||
192 | |||
193 | /** |
||
194 | * Convert an array to JSON via json_encode(). |
||
195 | * |
||
196 | * @param array $value |
||
197 | * @return string null|string |
||
198 | */ |
||
199 | public function toJson(array $value) |
||
200 | { |
||
201 | if (!is_array($value)) { |
||
202 | $value = (array) $value; |
||
203 | } |
||
204 | |||
205 | return json_encode($value, JSON_UNESCAPED_SLASHES); |
||
206 | } |
||
207 | |||
208 | /** |
||
209 | * Convert an array's values into an array of SilverStripe DBField subtypes ala: |
||
210 | * |
||
211 | * - {@link Int} |
||
212 | * - {@link Float} |
||
213 | * - {@link Boolean} |
||
214 | * - {@link Varchar} |
||
215 | * |
||
216 | * @param array $data |
||
217 | * @return array |
||
218 | */ |
||
219 | public function toSSTypes(array $data) |
||
220 | { |
||
221 | $newList = []; |
||
222 | foreach ($data as $key => $val) { |
||
223 | if (is_array($val)) { |
||
224 | $newList[$key] = $this->toSSTypes($val); |
||
225 | } else { |
||
226 | $newList[$key] = $this->castToDBField($val); |
||
227 | } |
||
228 | } |
||
229 | |||
230 | return $newList; |
||
231 | } |
||
232 | |||
233 | /** |
||
234 | * @param mixed $value |
||
235 | * @return array |
||
236 | * @throws \JSONText\Exceptions\JSONTextException |
||
237 | */ |
||
238 | public function toArray($value) |
||
239 | { |
||
240 | $decode = json_decode($value, true); |
||
241 | |||
242 | if (is_null($decode)) { |
||
243 | $msg = 'Decoded JSON is invalid.'; |
||
244 | throw new JSONTextException($msg); |
||
245 | } |
||
246 | |||
247 | return $decode; |
||
248 | } |
||
249 | |||
250 | /** |
||
251 | * Return an array of the JSON key + value represented as first (top-level) JSON node. |
||
252 | * |
||
253 | * @return array |
||
254 | */ |
||
255 | public function first() |
||
256 | { |
||
257 | $data = $this->getStoreAsArray(); |
||
258 | |||
259 | if (empty($data)) { |
||
260 | return $this->returnAsType([]); |
||
261 | } |
||
262 | |||
263 | $key = array_keys($data)[0]; |
||
264 | $val = array_values($data)[0]; |
||
265 | |||
266 | return $this->returnAsType([$key => $val]); |
||
267 | } |
||
268 | |||
269 | /** |
||
270 | * Return an array of the JSON key + value represented as last JSON node. |
||
271 | * |
||
272 | * @return array |
||
273 | */ |
||
274 | public function last() |
||
275 | { |
||
276 | $data = $this->getStoreAsArray(); |
||
277 | |||
278 | if (empty($data)) { |
||
279 | return $this->returnAsType([]); |
||
280 | } |
||
281 | |||
282 | $count = count($data) -1; |
||
283 | $key = array_keys($data)[$count]; |
||
284 | $val = array_values($data)[$count]; |
||
285 | |||
286 | return $this->returnAsType([$key => $val]); |
||
287 | } |
||
288 | |||
289 | /** |
||
290 | * Return an array of the JSON key + value represented as the $n'th JSON node. |
||
291 | * |
||
292 | * @param int $n |
||
293 | * @return mixed array |
||
294 | * @throws \JSONText\Exceptions\JSONTextException |
||
295 | */ |
||
296 | public function nth($n) |
||
297 | { |
||
298 | $data = $this->getStoreAsArray(); |
||
299 | |||
300 | if (empty($data)) { |
||
301 | return $this->returnAsType([]); |
||
302 | } |
||
303 | |||
304 | if (!is_int($n)) { |
||
305 | $msg = 'Argument passed to ' . __FUNCTION__ . '() must be an integer.'; |
||
306 | throw new JSONTextException($msg); |
||
307 | } |
||
308 | |||
309 | $i = 0; |
||
310 | foreach ($data as $key => $val) { |
||
311 | if ($i === $n) { |
||
312 | return $this->returnAsType([$key => $val]); |
||
313 | } |
||
314 | $i++; |
||
315 | } |
||
316 | |||
317 | return $this->returnAsType($data); |
||
318 | } |
||
319 | |||
320 | /** |
||
321 | * Return the key(s) + value(s) represented by $operator extracting relevant result from the source JSON's structure. |
||
322 | * N.b when using the path match operator '#>' with duplicate keys, an indexed array of results is returned. |
||
323 | * |
||
324 | * @param string $operator One of the legitimate operators for the current backend or a valid JSONPath expression. |
||
325 | * @param string $operand |
||
326 | * @return mixed null|array |
||
327 | * @throws \JSONText\Exceptions\JSONTextException |
||
328 | */ |
||
329 | public function query($operator, $operand = null) |
||
330 | { |
||
331 | $data = $this->getStoreAsArray(); |
||
332 | |||
333 | if (empty($data)) { |
||
334 | return $this->returnAsType([]); |
||
335 | } |
||
336 | |||
337 | $isOperator = !is_null($operand) && $this->isValidOperator($operator); |
||
338 | $isExpresssion = is_null($operand) && $this->isValidExpression($operator); |
||
339 | |||
340 | if ($isOperator) { |
||
341 | $type = self::JSONTEXT_QUERY_OPERATOR; |
||
342 | } else if ($isExpresssion) { |
||
343 | $type = self::JSONTEXT_QUERY_JSONPATH; |
||
344 | } else { |
||
345 | $msg = 'JSON expression: ' . $operator . ' is invalid.'; |
||
346 | throw new JSONTextException($msg); |
||
347 | } |
||
348 | |||
349 | if ($marshalled = $this->marshallQuery(func_get_args(), $type)) { |
||
350 | return $this->returnAsType($marshalled); |
||
351 | } |
||
352 | |||
353 | return $this->returnAsType([]); |
||
354 | } |
||
355 | |||
356 | /** |
||
357 | * Based on the passed operator or expression, it marshalls the correct backend matcher method into account. |
||
358 | * |
||
359 | * @param array $args |
||
360 | * @param integer $type |
||
361 | * @return array |
||
362 | * @throws \JSONText\Exceptions\JSONTextException |
||
363 | */ |
||
364 | private function marshallQuery($args, $type = 1) |
||
365 | { |
||
366 | $backend = $this->config()->backend; |
||
367 | $operator = $expression = $args[0]; |
||
368 | $operand = isset($args[1]) ? $args[1] : null; |
||
369 | $operators = $this->config()->allowed_operators[$backend]; |
||
370 | $operatorParamIsValid = $type === self::JSONTEXT_QUERY_OPERATOR; |
||
371 | $expressionParamIsValid = $type === self::JSONTEXT_QUERY_JSONPATH; |
||
372 | |||
373 | if ($operatorParamIsValid) { |
||
374 | $dbBackendInst = $this->createBackendInst($operand); |
||
375 | foreach ($operators as $routine => $backendOperator) { |
||
376 | if ($operator === $backendOperator && $result = $dbBackendInst->$routine()) { |
||
377 | return $result; |
||
378 | } |
||
379 | } |
||
380 | } else if($expressionParamIsValid) { |
||
381 | $dbBackendInst = $this->createBackendInst($expression); |
||
382 | if ($result = $dbBackendInst->matchOnExpr()) { |
||
383 | return $result; |
||
384 | } |
||
385 | } |
||
386 | |||
387 | return []; |
||
388 | } |
||
389 | |||
390 | /** |
||
391 | * Same as standard setValue() method except we can also accept a JSONPath expression. This expression will |
||
392 | * conditionally update the parts of the field's source JSON referenced by $expr with $value |
||
393 | * then re-set the entire JSON string as the field's new value. |
||
394 | * |
||
395 | * Note: The $expr parameter can only accept JSONPath expressions. Using Postgres operators will not work and will |
||
396 | * throw an instance of JSONTextException. |
||
397 | * |
||
398 | * @param mixed $value |
||
399 | * @param array $record |
||
400 | * @param string $expr A valid JSONPath expression. |
||
401 | * @return JSONText |
||
402 | * @throws JSONTextException |
||
403 | */ |
||
404 | public function setValue($value, $record = null, $expr = '') |
||
405 | { |
||
406 | if (empty($expr)) { |
||
407 | if (!$this->isValidDBValue($value)) { |
||
408 | $msg = 'Invalid data passed to ' . __FUNCTION__ . '()'; |
||
409 | throw new JSONTextException($msg); |
||
410 | } |
||
411 | |||
412 | $this->value = $value; |
||
413 | } else { |
||
414 | if (!$this->isValidExpression($expr)) { |
||
415 | $msg = 'Invalid JSONPath expression: ' . $expr . ' passed to ' . __FUNCTION__ . '()'; |
||
416 | throw new JSONTextException($msg); |
||
417 | } |
||
418 | |||
419 | if (!$this->getJSONStore()->set($expr, $value)) { |
||
420 | $msg = 'Failed to properly set custom data to the JSONStore in ' . __FUNCTION__ . '()'; |
||
421 | throw new JSONTextException($msg); |
||
422 | } |
||
423 | |||
424 | $this->value = $this->jsonStore->toString(); |
||
425 | } |
||
426 | |||
427 | parent::setValue($this->value, $record); |
||
428 | |||
429 | return $this; |
||
430 | } |
||
431 | |||
432 | /** |
||
433 | * Determine the desired userland format to return all query API method results in. |
||
434 | * |
||
435 | * @param mixed |
||
436 | * @return mixed array|null |
||
437 | * @throws \JSONText\Exceptions\JSONTextException |
||
438 | */ |
||
439 | private function returnAsType($data) |
||
440 | { |
||
441 | $data = (array) $data; |
||
442 | $type = $this->returnType; |
||
443 | if ($type === 'array') { |
||
444 | if (!count($data)) { |
||
445 | return []; |
||
446 | } |
||
447 | |||
448 | return $data; |
||
449 | } |
||
450 | |||
451 | if ($type === 'json') { |
||
452 | if (!count($data)) { |
||
453 | return '[]'; |
||
454 | } |
||
455 | |||
456 | return $this->toJson($data); |
||
457 | } |
||
458 | |||
459 | if ($type === 'silverstripe') { |
||
460 | if (!count($data)) { |
||
461 | return null; |
||
462 | } |
||
463 | |||
464 | return $this->toSSTypes($data); |
||
465 | } |
||
466 | |||
467 | $msg = 'Bad argument passed to ' . __FUNCTION__ . '()'; |
||
468 | throw new JSONTextException($msg); |
||
469 | } |
||
470 | |||
471 | /** |
||
472 | * Create an instance of {@link JSONBackend} according to the value of JSONText::backend defined in SS config. |
||
473 | * |
||
474 | * @param string operand |
||
475 | * @return JSONBackend |
||
476 | * @throws JSONTextException |
||
477 | */ |
||
478 | protected function createBackendInst($operand) |
||
479 | { |
||
480 | $backend = $this->config()->backend; |
||
481 | $dbBackendClass = '\JSONText\Backends\\' . ucfirst($backend) . 'JSONBackend'; |
||
482 | |||
483 | if (!class_exists($dbBackendClass)) { |
||
484 | $msg = 'JSONText backend class ' . $dbBackendClass . ' not found.'; |
||
485 | throw new JSONTextException($msg); |
||
486 | } |
||
487 | |||
488 | return \Injector::inst()->createWithArgs( |
||
489 | $dbBackendClass, [ |
||
490 | $operand, |
||
491 | $this |
||
492 | ]); |
||
493 | } |
||
494 | |||
495 | /** |
||
496 | * Utility method to determine whether a value is really valid JSON or not. |
||
497 | * The Peekmo JSONStore lib won't accept otherwise valid JSON values like `true`, `false` & `""` so these need |
||
498 | * to be disallowed. |
||
499 | * |
||
500 | * @param string $value |
||
501 | * @return boolean |
||
502 | */ |
||
503 | public function isValidJson($value) |
||
504 | { |
||
505 | if (!isset($value)) { |
||
506 | return false; |
||
507 | } |
||
508 | |||
509 | $value = trim($value); |
||
510 | return !is_null(json_decode($value, true)); |
||
511 | } |
||
512 | |||
513 | /** |
||
514 | * @return boolean |
||
515 | */ |
||
516 | public function isValidDBValue($value) { |
||
517 | $value = trim($value); |
||
518 | |||
519 | if (in_array($value, ['true', 'false'])) { |
||
520 | return false; |
||
521 | } |
||
522 | |||
523 | if (is_string($value) && strlen($value) === 0) { |
||
524 | return true; |
||
525 | } |
||
526 | |||
527 | return $this->isValidJson($value); |
||
528 | } |
||
529 | |||
530 | /** |
||
531 | * Is the passed JSON operator valid? |
||
532 | * |
||
533 | * @param string $operator |
||
534 | * @return boolean |
||
535 | */ |
||
536 | public function isValidOperator($operator) |
||
546 | |||
547 | /** |
||
548 | * Is the passed JSPONPath expression valid? |
||
549 | * |
||
550 | * @param string $expression |
||
551 | * @return bool |
||
552 | */ |
||
553 | public function isValidExpression($expression) |
||
554 | { |
||
555 | return (bool) preg_match("#^\\$\.#", $expression); |
||
556 | } |
||
557 | |||
558 | /** |
||
559 | * Casts a value to a {@link DBField} subclass. |
||
560 | * |
||
561 | * @param mixed $val |
||
562 | * @return mixed DBField|array |
||
563 | */ |
||
564 | private function castToDBField($val) |
||
580 | |||
581 | } |
||
582 |
This check marks private properties in classes that are never used. Those properties can be removed.