|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +/** |
| 6 | + * Derafu: Support - Essential PHP Utilities. |
| 7 | + * |
| 8 | + * Copyright (c) 2025 Esteban De La Fuente Rubio / Derafu <https://www.derafu.org> |
| 9 | + * Licensed under the MIT License. |
| 10 | + * See LICENSE file for more details. |
| 11 | + */ |
| 12 | + |
| 13 | +namespace Derafu\Support; |
| 14 | + |
| 15 | +use DateTime; |
| 16 | +use DateTimeInterface; |
| 17 | +use JsonException; |
| 18 | +use JsonSerializable; |
| 19 | +use Stringable; |
| 20 | + |
| 21 | +/** |
| 22 | + * JSON serializer for converting PHP objects and arrays to JSON strings. |
| 23 | + * |
| 24 | + * This serializer handles the conversion of complex nested structures including |
| 25 | + * objects that implement JsonSerializable, have toArray() methods, or are |
| 26 | + * special types like DateTimeInterface and Stringable. |
| 27 | + */ |
| 28 | +final class JsonSerializer |
| 29 | +{ |
| 30 | + /** |
| 31 | + * Default flags used for JSON encoding. |
| 32 | + */ |
| 33 | + private const DEFAULT_FLAGS = [ |
| 34 | + JSON_PRETTY_PRINT, // Format JSON with whitespace for readability. |
| 35 | + JSON_INVALID_UTF8_SUBSTITUTE, // Replace invalid UTF-8 sequences with Unicode replacement character. |
| 36 | + JSON_UNESCAPED_LINE_TERMINATORS, // Don't escape line terminators. |
| 37 | + JSON_UNESCAPED_SLASHES, // Don't escape forward slashes. |
| 38 | + JSON_UNESCAPED_UNICODE, // Don't escape Unicode characters. |
| 39 | + JSON_THROW_ON_ERROR, // Throw exception on encoding errors. |
| 40 | + ]; |
| 41 | + |
| 42 | + /** |
| 43 | + * Serializes a PHP value to a JSON string. |
| 44 | + * |
| 45 | + * This method handles complex nested structures by recursively transforming |
| 46 | + * objects into arrays or scalar values that can be encoded to JSON. |
| 47 | + * |
| 48 | + * @param mixed $value The value to serialize to JSON. |
| 49 | + * @param int|null $flags JSON encoding flags (uses DEFAULT_FLAGS if null). |
| 50 | + * @param int $depth Maximum recursion depth. |
| 51 | + * @return string JSON encoded string. |
| 52 | + * @throws JsonException When encoding fails. |
| 53 | + */ |
| 54 | + public static function serialize( |
| 55 | + mixed $value, |
| 56 | + ?int $flags = null, |
| 57 | + int $depth = 512 |
| 58 | + ): string { |
| 59 | + // Use default flags if none provided. |
| 60 | + if ($flags === null) { |
| 61 | + $flags = array_reduce( |
| 62 | + self::DEFAULT_FLAGS, |
| 63 | + fn ($carry, $flag) => $carry | $flag, |
| 64 | + 0 |
| 65 | + ); |
| 66 | + } |
| 67 | + |
| 68 | + // Transform objects to arrays or scalar values. |
| 69 | + self::transformObjects($value); |
| 70 | + |
| 71 | + // Encode to JSON. |
| 72 | + return json_encode($value, $flags, $depth); |
| 73 | + } |
| 74 | + |
| 75 | + /** |
| 76 | + * Recursively transforms objects in a value to make them JSON serializable. |
| 77 | + * |
| 78 | + * This method handles: |
| 79 | + * |
| 80 | + * - Objects implementing JsonSerializable (calls jsonSerialize()). |
| 81 | + * - Objects with toArray() method (calls toArray()). |
| 82 | + * - DateTimeInterface objects (formats as ISO 8601 string). |
| 83 | + * - Stringable objects (converts to string). |
| 84 | + * - Arrays (processes each element recursively). |
| 85 | + * |
| 86 | + * @param mixed &$value The value to transform (passed by reference). |
| 87 | + * @return void |
| 88 | + */ |
| 89 | + private static function transformObjects(mixed &$value): void |
| 90 | + { |
| 91 | + // Handle arrays by processing each element recursively. |
| 92 | + if (is_array($value)) { |
| 93 | + foreach ($value as &$item) { |
| 94 | + self::transformObjects($item); |
| 95 | + } |
| 96 | + // Break the reference to prevent accidental modifications. |
| 97 | + unset($item); |
| 98 | + return; |
| 99 | + } |
| 100 | + |
| 101 | + // If not an object, no transformation needed. |
| 102 | + if (!is_object($value)) { |
| 103 | + return; |
| 104 | + } |
| 105 | + |
| 106 | + // Handle different types of objects. |
| 107 | + |
| 108 | + // Get the serialized representation and transform it recursively. |
| 109 | + if ($value instanceof JsonSerializable) { |
| 110 | + |
| 111 | + $serialized = $value->jsonSerialize(); |
| 112 | + $value = $serialized; |
| 113 | + self::transformObjects($value); |
| 114 | + } |
| 115 | + |
| 116 | + // Convert to array and transform recursively. |
| 117 | + elseif (method_exists($value, 'toArray')) { |
| 118 | + $array = $value->toArray(); |
| 119 | + $value = $array; |
| 120 | + self::transformObjects($value); |
| 121 | + } |
| 122 | + |
| 123 | + // Format DateTime objects as ISO 8601 strings. |
| 124 | + elseif ($value instanceof DateTimeInterface) { |
| 125 | + $value = $value->format(DateTime::ATOM); |
| 126 | + } |
| 127 | + |
| 128 | + // Convert Stringable objects to strings. |
| 129 | + elseif ($value instanceof Stringable) { |
| 130 | + $value = $value->__toString(); |
| 131 | + } |
| 132 | + |
| 133 | + // Objects that don't match any of the above conditions remain as is, |
| 134 | + // which may result in only public properties being encoded. |
| 135 | + } |
| 136 | +} |
0 commit comments