Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like JsonEncoder 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 JsonEncoder, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
21 | class JsonEncoder |
||
22 | { |
||
23 | /** |
||
24 | * Encode a value as JSON array. |
||
25 | */ |
||
26 | const JSON_ARRAY = 1; |
||
27 | |||
28 | /** |
||
29 | * Encode a value as JSON object. |
||
30 | */ |
||
31 | const JSON_OBJECT = 2; |
||
32 | |||
33 | /** |
||
34 | * Encode a value as JSON string. |
||
35 | */ |
||
36 | const JSON_STRING = 3; |
||
37 | |||
38 | /** |
||
39 | * Encode a value as JSON integer or float. |
||
40 | */ |
||
41 | const JSON_NUMBER = 4; |
||
42 | |||
43 | /** |
||
44 | * @var JsonValidator |
||
45 | */ |
||
46 | private $validator; |
||
47 | |||
48 | /** |
||
49 | * @var int |
||
50 | */ |
||
51 | private $arrayEncoding = self::JSON_ARRAY; |
||
52 | |||
53 | /** |
||
54 | * @var int |
||
55 | */ |
||
56 | private $numericEncoding = self::JSON_STRING; |
||
57 | |||
58 | /** |
||
59 | * @var bool |
||
60 | */ |
||
61 | private $gtLtEscaped = false; |
||
62 | |||
63 | /** |
||
64 | * @var bool |
||
65 | */ |
||
66 | private $ampersandEscaped = false; |
||
67 | |||
68 | /** |
||
69 | * @var bool |
||
70 | */ |
||
71 | private $singleQuoteEscaped = false; |
||
72 | |||
73 | /** |
||
74 | * @var bool |
||
75 | */ |
||
76 | private $doubleQuoteEscaped = false; |
||
77 | |||
78 | /** |
||
79 | * @var bool |
||
80 | */ |
||
81 | private $slashEscaped = true; |
||
82 | |||
83 | /** |
||
84 | * @var bool |
||
85 | */ |
||
86 | private $unicodeEscaped = true; |
||
87 | |||
88 | /** |
||
89 | * @var bool |
||
90 | */ |
||
91 | private $prettyPrinting = false; |
||
92 | |||
93 | /** |
||
94 | * @var bool |
||
95 | */ |
||
96 | private $terminatedWithLineFeed = false; |
||
97 | |||
98 | /** |
||
99 | * @var int |
||
100 | */ |
||
101 | private $maxDepth = 512; |
||
102 | |||
103 | /** |
||
104 | * Creates a new encoder. |
||
105 | * |
||
106 | 55 | * @param null|JsonValidator $validator |
|
107 | */ |
||
108 | 55 | public function __construct(JsonValidator $validator = null) |
|
109 | 55 | { |
|
110 | $this->validator = $validator ?: new JsonValidator(); |
||
111 | } |
||
112 | |||
113 | /** |
||
114 | * Encodes data as JSON. |
||
115 | * |
||
116 | * If a schema is passed, the value is validated against that schema before |
||
117 | * encoding. The schema may be passed as file path or as object returned |
||
118 | * from `JsonDecoder::decodeFile($schemaFile)`. |
||
119 | * |
||
120 | * You can adjust the decoding with the various setters in this class. |
||
121 | * |
||
122 | * @param mixed $data The data to encode. |
||
123 | * @param string|object $schema The schema file or object. |
||
|
|||
124 | * |
||
125 | * @return string The JSON string. |
||
126 | * |
||
127 | * @throws EncodingFailedException If the data could not be encoded. |
||
128 | * @throws ValidationFailedException If the data fails schema validation. |
||
129 | 47 | * @throws InvalidSchemaException If the schema is invalid. |
|
130 | */ |
||
131 | 47 | public function encode($data, $schema = null) |
|
132 | 6 | { |
|
133 | View Code Duplication | if (null !== $schema) { |
|
134 | 6 | $errors = $this->validator->validate($data, $schema); |
|
135 | 4 | ||
136 | if (count($errors) > 0) { |
||
137 | 2 | throw ValidationFailedException::fromErrors($errors); |
|
138 | } |
||
139 | 43 | } |
|
140 | |||
141 | 43 | $options = 0; |
|
142 | 1 | ||
143 | 1 | if (self::JSON_OBJECT === $this->arrayEncoding) { |
|
144 | $options |= JSON_FORCE_OBJECT; |
||
145 | 43 | } |
|
146 | 2 | ||
147 | 2 | if (self::JSON_NUMBER === $this->numericEncoding) { |
|
148 | $options |= JSON_NUMERIC_CHECK; |
||
149 | 43 | } |
|
150 | 1 | ||
151 | 1 | if ($this->gtLtEscaped) { |
|
152 | $options |= JSON_HEX_TAG; |
||
153 | 43 | } |
|
154 | 1 | ||
155 | 1 | if ($this->ampersandEscaped) { |
|
156 | $options |= JSON_HEX_AMP; |
||
157 | 43 | } |
|
158 | 1 | ||
159 | 1 | if ($this->singleQuoteEscaped) { |
|
160 | $options |= JSON_HEX_APOS; |
||
161 | 43 | } |
|
162 | 1 | ||
163 | 1 | if ($this->doubleQuoteEscaped) { |
|
164 | $options |= JSON_HEX_QUOT; |
||
165 | 43 | } |
|
166 | 43 | ||
167 | 1 | if (PHP_VERSION_ID >= 50400) { |
|
168 | 1 | if (!$this->slashEscaped) { |
|
169 | $options |= JSON_UNESCAPED_SLASHES; |
||
170 | 43 | } |
|
171 | 1 | ||
172 | 1 | if (!$this->unicodeEscaped) { |
|
173 | $options |= JSON_UNESCAPED_UNICODE; |
||
174 | 43 | } |
|
175 | 1 | ||
176 | 1 | if ($this->prettyPrinting) { |
|
177 | 43 | $options |= JSON_PRETTY_PRINT; |
|
178 | } |
||
179 | 43 | } |
|
180 | |||
181 | if (PHP_VERSION_ID >= 50500) { |
||
182 | $maxDepth = $this->maxDepth; |
||
183 | |||
184 | // We subtract 1 from the max depth to make JsonDecoder and |
||
185 | // JsonEncoder consistent. json_encode() and json_decode() behave |
||
186 | // differently for their depth values. See the test cases for |
||
187 | // examples. |
||
188 | // HHVM does not have this inconsistency. |
||
189 | if (!defined('HHVM_VERSION')) { |
||
190 | --$maxDepth; |
||
191 | } |
||
192 | |||
193 | 43 | $encoded = json_encode($data, $options, $maxDepth); |
|
194 | } else { |
||
195 | $encoded = json_encode($data, $options); |
||
196 | 43 | } |
|
197 | |||
198 | if (PHP_VERSION_ID < 50400 && !$this->slashEscaped) { |
||
199 | // PHP below 5.4 does not allow to turn off slash escaping. Let's |
||
200 | // unescape slashes manually. |
||
201 | $encoded = str_replace('\\/', '/', $encoded); |
||
202 | 43 | } |
|
203 | 1 | ||
204 | 1 | if (JSON_ERROR_NONE !== json_last_error()) { |
|
205 | 1 | throw new EncodingFailedException(sprintf( |
|
206 | 1 | 'The data could not be encoded as JSON: %s', |
|
207 | JsonError::getLastErrorMessage() |
||
208 | ), json_last_error()); |
||
209 | 42 | } |
|
210 | 1 | ||
211 | 1 | if ($this->terminatedWithLineFeed) { |
|
212 | $encoded .= "\n"; |
||
213 | 42 | } |
|
214 | |||
215 | return $encoded; |
||
216 | } |
||
217 | |||
218 | /** |
||
219 | * Encodes data into a JSON file. |
||
220 | * |
||
221 | * @param mixed $data The data to encode. |
||
222 | * @param string $path The path where the JSON file will be stored. |
||
223 | * @param string|object $schema The schema file or object. |
||
224 | * |
||
225 | * @throws EncodingFailedException If the data could not be encoded. |
||
226 | * @throws ValidationFailedException If the data fails schema validation. |
||
227 | * @throws InvalidSchemaException If the schema is invalid. |
||
228 | * |
||
229 | 5 | * @see encode |
|
230 | */ |
||
231 | 5 | public function encodeFile($data, $path, $schema = null) |
|
232 | 1 | { |
|
233 | 1 | if (!file_exists($dir = dirname($path))) { |
|
234 | mkdir($dir, 0777, true); |
||
235 | } |
||
236 | |||
237 | try { |
||
238 | // Right now, it's sufficient to just write the file. In the future, |
||
239 | 5 | // this will diff existing files with the given data and only do |
|
240 | 5 | // in-place modifications where necessary. |
|
241 | $content = $this->encode($data, $schema); |
||
242 | } catch (EncodingFailedException $e) { |
||
243 | // Add the file name to the exception |
||
244 | throw new EncodingFailedException(sprintf( |
||
245 | 'An error happened while encoding %s: %s', |
||
246 | $path, |
||
247 | 2 | $e->getMessage() |
|
248 | ), $e->getCode(), $e); |
||
249 | 2 | } catch (ValidationFailedException $e) { |
|
250 | 2 | // Add the file name to the exception |
|
251 | 2 | throw new ValidationFailedException(sprintf( |
|
252 | 2 | "Validation failed while encoding %s:\n%s", |
|
253 | 2 | $path, |
|
254 | $e->getErrorsAsString() |
||
255 | ), $e->getErrors(), $e->getCode(), $e); |
||
256 | } catch (InvalidSchemaException $e) { |
||
257 | // Add the file name to the exception |
||
258 | throw new InvalidSchemaException(sprintf( |
||
259 | 'An error happened while encoding %s: %s', |
||
260 | $path, |
||
261 | $e->getMessage() |
||
262 | ), $e->getCode(), $e); |
||
263 | 3 | } |
|
264 | 3 | ||
265 | $errorMessage = null; |
||
266 | 3 | $errorCode = 0; |
|
267 | 1 | ||
268 | 1 | set_error_handler(function ($errno, $errstr) use (&$errorMessage, &$errorCode) { |
|
269 | 3 | $errorMessage = $errstr; |
|
270 | $errorCode = $errno; |
||
271 | 3 | }); |
|
272 | |||
273 | 3 | file_put_contents($path, $content); |
|
274 | |||
275 | 3 | restore_error_handler(); |
|
276 | 1 | ||
277 | View Code Duplication | if (null !== $errorMessage) { |
|
278 | 1 | if (false !== $pos = strpos($errorMessage, '): ')) { |
|
279 | 1 | // cut "file_put_contents(%path%):" to make message more readable |
|
280 | $errorMessage = substr($errorMessage, $pos + 3); |
||
281 | 1 | } |
|
282 | 1 | ||
283 | 1 | throw new IOException(sprintf( |
|
284 | 1 | 'Could not write %s: %s (%s)', |
|
285 | $path, |
||
286 | 1 | $errorMessage, |
|
287 | $errorCode |
||
288 | 2 | ), $errorCode); |
|
289 | } |
||
290 | } |
||
291 | |||
292 | /** |
||
293 | * Returns the encoding of non-associative arrays. |
||
294 | * |
||
295 | * @return int One of the constants {@link JSON_OBJECT} and {@link JSON_ARRAY}. |
||
296 | */ |
||
297 | public function getArrayEncoding() |
||
298 | { |
||
299 | return $this->arrayEncoding; |
||
300 | } |
||
301 | |||
302 | /** |
||
303 | * Sets the encoding of non-associative arrays. |
||
304 | * |
||
305 | * By default, non-associative arrays are decoded as JSON arrays. |
||
306 | * |
||
307 | * @param int $encoding One of the constants {@link JSON_OBJECT} and {@link JSON_ARRAY}. |
||
308 | * |
||
309 | 5 | * @throws \InvalidArgumentException If the passed encoding is invalid. |
|
310 | */ |
||
311 | 5 | View Code Duplication | public function setArrayEncoding($encoding) |
323 | |||
324 | /** |
||
325 | * Returns the encoding of numeric strings. |
||
326 | * |
||
327 | * @return int One of the constants {@link JSON_STRING} and {@link JSON_NUMBER}. |
||
328 | */ |
||
329 | public function getNumericEncoding() |
||
330 | { |
||
331 | return $this->numericEncoding; |
||
332 | } |
||
333 | |||
334 | /** |
||
335 | * Sets the encoding of numeric strings. |
||
336 | * |
||
337 | * By default, non-associative arrays are decoded as JSON strings. |
||
338 | * |
||
339 | * @param int $encoding One of the constants {@link JSON_STRING} and {@link JSON_NUMBER}. |
||
340 | * |
||
341 | 6 | * @throws \InvalidArgumentException If the passed encoding is invalid. |
|
342 | */ |
||
343 | 6 | View Code Duplication | public function setNumericEncoding($encoding) |
355 | |||
356 | /** |
||
357 | * Returns whether ampersands (&) are escaped. |
||
358 | * |
||
359 | * If `true`, ampersands will be escaped as "\u0026". |
||
360 | * |
||
361 | * By default, ampersands are not escaped. |
||
362 | * |
||
363 | * @return bool Whether ampersands are escaped. |
||
364 | */ |
||
365 | public function isAmpersandEscaped() |
||
369 | |||
370 | /** |
||
371 | * Sets whether ampersands (&) should be escaped. |
||
372 | * |
||
373 | * If `true`, ampersands will be escaped as "\u0026". |
||
374 | * |
||
375 | * By default, ampersands are not escaped. |
||
376 | * |
||
377 | 2 | * @param bool $enabled Whether ampersands should be escaped. |
|
378 | */ |
||
379 | 2 | public function setEscapeAmpersand($enabled) |
|
383 | |||
384 | /** |
||
385 | * Returns whether double quotes (") are escaped. |
||
386 | * |
||
387 | * If `true`, double quotes will be escaped as "\u0022". |
||
388 | * |
||
389 | * By default, double quotes are not escaped. |
||
390 | * |
||
391 | * @return bool Whether double quotes are escaped. |
||
392 | */ |
||
393 | public function isDoubleQuoteEscaped() |
||
397 | |||
398 | /** |
||
399 | * Sets whether double quotes (") should be escaped. |
||
400 | * |
||
401 | * If `true`, double quotes will be escaped as "\u0022". |
||
402 | * |
||
403 | * By default, double quotes are not escaped. |
||
404 | * |
||
405 | 2 | * @param bool $enabled Whether double quotes should be escaped. |
|
406 | */ |
||
407 | 2 | public function setEscapeDoubleQuote($enabled) |
|
411 | |||
412 | /** |
||
413 | * Returns whether single quotes (') are escaped. |
||
414 | * |
||
415 | * If `true`, single quotes will be escaped as "\u0027". |
||
416 | * |
||
417 | * By default, single quotes are not escaped. |
||
418 | * |
||
419 | * @return bool Whether single quotes are escaped. |
||
420 | */ |
||
421 | public function isSingleQuoteEscaped() |
||
425 | |||
426 | /** |
||
427 | * Sets whether single quotes (") should be escaped. |
||
428 | * |
||
429 | * If `true`, single quotes will be escaped as "\u0027". |
||
430 | * |
||
431 | * By default, single quotes are not escaped. |
||
432 | * |
||
433 | 2 | * @param bool $enabled Whether single quotes should be escaped. |
|
434 | */ |
||
435 | 2 | public function setEscapeSingleQuote($enabled) |
|
439 | |||
440 | /** |
||
441 | * Returns whether forward slashes (/) are escaped. |
||
442 | * |
||
443 | * If `true`, forward slashes will be escaped as "\/". |
||
444 | * |
||
445 | * By default, forward slashes are not escaped. |
||
446 | * |
||
447 | * @return bool Whether forward slashes are escaped. |
||
448 | */ |
||
449 | public function isSlashEscaped() |
||
453 | |||
454 | /** |
||
455 | * Sets whether forward slashes (") should be escaped. |
||
456 | * |
||
457 | * If `true`, forward slashes will be escaped as "\/". |
||
458 | * |
||
459 | * By default, forward slashes are not escaped. |
||
460 | * |
||
461 | 2 | * @param bool $enabled Whether forward slashes should be escaped. |
|
462 | */ |
||
463 | 2 | public function setEscapeSlash($enabled) |
|
467 | |||
468 | /** |
||
469 | * Returns whether greater than/less than symbols (>, <) are escaped. |
||
470 | * |
||
471 | * If `true`, greater than will be escaped as "\u003E" and less than as |
||
472 | * "\u003C". |
||
473 | * |
||
474 | * By default, greater than/less than symbols are not escaped. |
||
475 | * |
||
476 | * @return bool Whether greater than/less than symbols are escaped. |
||
477 | */ |
||
478 | public function isGtLtEscaped() |
||
482 | |||
483 | /** |
||
484 | * Sets whether greater than/less than symbols (>, <) should be escaped. |
||
485 | * |
||
486 | * If `true`, greater than will be escaped as "\u003E" and less than as |
||
487 | * "\u003C". |
||
488 | * |
||
489 | * By default, greater than/less than symbols are not escaped. |
||
490 | * |
||
491 | 2 | * @param bool $enabled Whether greater than/less than should be escaped. |
|
492 | */ |
||
493 | 2 | public function setEscapeGtLt($enabled) |
|
497 | |||
498 | /** |
||
499 | * Returns whether unicode characters are escaped. |
||
500 | * |
||
501 | * If `true`, unicode characters will be escaped as hexadecimals strings. |
||
502 | * For example, "ü" will be escaped as "\u00fc". |
||
503 | * |
||
504 | * By default, unicode characters are escaped. |
||
505 | * |
||
506 | * @return bool Whether unicode characters are escaped. |
||
507 | */ |
||
508 | public function isUnicodeEscaped() |
||
512 | |||
513 | /** |
||
514 | * Sets whether unicode characters should be escaped. |
||
515 | * |
||
516 | * If `true`, unicode characters will be escaped as hexadecimals strings. |
||
517 | * For example, "ü" will be escaped as "\u00fc". |
||
518 | * |
||
519 | * By default, unicode characters are escaped. |
||
520 | * |
||
521 | 2 | * @param bool $enabled Whether unicode characters should be escaped. |
|
522 | */ |
||
523 | 2 | public function setEscapeUnicode($enabled) |
|
527 | |||
528 | /** |
||
529 | * Returns whether JSON strings are formatted for better readability. |
||
530 | * |
||
531 | * If `true`, line breaks will be added after object properties and array |
||
532 | * entries. Each new nesting level will be indented by four spaces. |
||
533 | * |
||
534 | * By default, pretty printing is not enabled. |
||
535 | * |
||
536 | * @return bool Whether JSON strings are formatted. |
||
537 | */ |
||
538 | public function isPrettyPrinting() |
||
542 | |||
543 | /** |
||
544 | * Sets whether JSON strings should be formatted for better readability. |
||
545 | * |
||
546 | * If `true`, line breaks will be added after object properties and array |
||
547 | * entries. Each new nesting level will be indented by four spaces. |
||
548 | * |
||
549 | * By default, pretty printing is not enabled. |
||
550 | * |
||
551 | 2 | * @param bool $prettyPrinting Whether JSON strings should be formatted. |
|
552 | */ |
||
553 | 2 | public function setPrettyPrinting($prettyPrinting) |
|
557 | |||
558 | /** |
||
559 | * Returns whether JSON strings are terminated with a line feed. |
||
560 | * |
||
561 | * By default, JSON strings are not terminated with a line feed. |
||
562 | * |
||
563 | * @return bool Whether JSON strings are terminated with a line feed. |
||
564 | */ |
||
565 | public function isTerminatedWithLineFeed() |
||
569 | |||
570 | /** |
||
571 | * Sets whether JSON strings should be terminated with a line feed. |
||
572 | * |
||
573 | * By default, JSON strings are not terminated with a line feed. |
||
574 | * |
||
575 | * @param bool $enabled Whether JSON strings should be terminated with a |
||
576 | 2 | * line feed. |
|
577 | */ |
||
578 | 2 | public function setTerminateWithLineFeed($enabled) |
|
582 | |||
583 | /** |
||
584 | * Returns the maximum recursion depth. |
||
585 | * |
||
586 | * A depth of zero means that objects are not allowed. A depth of one means |
||
587 | * only one level of objects or arrays is allowed. |
||
588 | * |
||
589 | * @return int The maximum recursion depth. |
||
590 | */ |
||
591 | public function getMaxDepth() |
||
595 | |||
596 | /** |
||
597 | * Sets the maximum recursion depth. |
||
598 | * |
||
599 | * If the depth is exceeded during encoding, an {@link EncodingFailedException} |
||
600 | * will be thrown. |
||
601 | * |
||
602 | * A depth of zero means that objects are not allowed. A depth of one means |
||
603 | * only one level of objects or arrays is allowed. |
||
604 | * |
||
605 | * @param int $maxDepth The maximum recursion depth. |
||
606 | * |
||
607 | * @throws \InvalidArgumentException If the depth is not an integer greater |
||
608 | 4 | * than or equal to zero. |
|
609 | */ |
||
610 | 4 | View Code Duplication | public function setMaxDepth($maxDepth) |
628 | } |
||
629 |
This check looks for
@param
annotations where the type inferred by our type inference engine differs from the declared type.It makes a suggestion as to what type it considers more descriptive.
Most often this is a case of a parameter that can be null in addition to its declared types.