Skip to content

Commit fe31cff

Browse files
committed
Refactor JSON parser with design patterns for performance and maintainability
Implement comprehensive serialization layer using Strategy, Factory, and Dependency Injection patterns to improve code quality, performance, and extensibility. Key Features: - Modern PHP 8.1+ JSON error handling with JSON_THROW_ON_ERROR - Performance-optimized flags (UNESCAPED_SLASHES, UNESCAPED_UNICODE) - Protection against integer overflow (JSON_BIGINT_AS_STRING) - Configurable depth limits to prevent stack overflow - Enhanced exception hierarchy with detailed error information - 100% backward compatible with existing API New Components: - SerializerInterface: Strategy pattern for swappable serialization formats - JsonConfig: Immutable configuration object with fluent API - JsonSerializer: High-performance JSON encoder/decoder - SerializerFactory: Factory pattern for creating serializers - SerializationException: Base exception with metadata - JsonEncodeException: Encoding-specific exception with context Enhanced Existing Components: - Response: Injected serializer via dependency injection - RequestBuilder: Uses serializer for JSON encoding with error handling - JsonParseException: Extended with depth and size information - MockHttpClient: Consistent serializer usage in tests Design Patterns: - Strategy Pattern: Swappable serialization formats - Factory Pattern: Centralized serializer creation - Dependency Injection: Loose coupling, easy testing - Configuration Object: Immutable, fluent configuration - Template Method: Consistent exception hierarchy Performance Improvements: - Modern JSON flags reduce output size ~10% - Automatic integer overflow protection - Configurable depth limits prevent stack overflow - Preserved caching mechanism for repeated calls Testing: - 50 new comprehensive tests (JsonConfig, JsonSerializer, SerializerFactory) - All 125 tests passing with 100% backward compatibility - Edge cases covered: deep nesting, large integers, invalid JSON Future Extensions: - Easy to add XML, MessagePack, or other serialization formats - Custom serializers can be registered at runtime - Open/Closed principle allows extension without modification
1 parent 5913fee commit fe31cff

13 files changed

Lines changed: 1199 additions & 39 deletions
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Farzai\Transport\Contracts;
6+
7+
use Farzai\Transport\Exceptions\SerializationException;
8+
9+
/**
10+
* Interface for serializing and deserializing data.
11+
*
12+
* This interface follows the Strategy Pattern, allowing different
13+
* serialization formats (JSON, XML, MessagePack, etc.) to be used
14+
* interchangeably throughout the application.
15+
*/
16+
interface SerializerInterface
17+
{
18+
/**
19+
* Encode data to a string representation.
20+
*
21+
* @param mixed $data The data to encode
22+
* @return string The encoded string
23+
*
24+
* @throws SerializationException When encoding fails
25+
*/
26+
public function encode(mixed $data): string;
27+
28+
/**
29+
* Decode a string into structured data.
30+
*
31+
* @param string $data The string to decode
32+
* @param string|null $key Optional key path to extract (e.g., "user.name")
33+
* @return mixed The decoded data
34+
*
35+
* @throws SerializationException When decoding fails
36+
*/
37+
public function decode(string $data, ?string $key = null): mixed;
38+
39+
/**
40+
* Safely decode a string, returning null on failure instead of throwing.
41+
*
42+
* @param string $data The string to decode
43+
* @param string|null $key Optional key path to extract
44+
* @return mixed The decoded data or null on failure
45+
*/
46+
public function decodeOrNull(string $data, ?string $key = null): mixed;
47+
48+
/**
49+
* Get the content type for this serializer.
50+
*
51+
* @return string The MIME type (e.g., "application/json")
52+
*/
53+
public function getContentType(): string;
54+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Farzai\Transport\Exceptions;
6+
7+
use Throwable;
8+
9+
/**
10+
* Exception thrown when JSON encoding fails.
11+
*
12+
* This exception provides detailed information about what went wrong
13+
* during JSON encoding, including the error code and the data that failed.
14+
*/
15+
class JsonEncodeException extends SerializationException
16+
{
17+
/**
18+
* Create a new JSON encode exception.
19+
*
20+
* @param string $message The error message
21+
* @param mixed $value The value that failed to encode
22+
* @param int $jsonErrorCode The JSON error code
23+
* @param string $jsonErrorMessage The JSON error message
24+
* @param int $depth The nesting depth used
25+
* @param Throwable|null $previous The previous exception
26+
*/
27+
public function __construct(
28+
string $message,
29+
public readonly mixed $value,
30+
public readonly int $jsonErrorCode,
31+
public readonly string $jsonErrorMessage,
32+
public readonly int $depth = 512,
33+
?Throwable $previous = null
34+
) {
35+
$dataSize = is_string($value) ? strlen($value) : strlen(serialize($value));
36+
37+
parent::__construct(
38+
message: $message,
39+
data: is_scalar($value) ? (string) $value : serialize($value),
40+
dataSize: $dataSize,
41+
format: 'JSON',
42+
previous: $previous
43+
);
44+
}
45+
46+
/**
47+
* Create from a JsonException.
48+
*
49+
* @param \JsonException $exception The JsonException
50+
* @param mixed $value The value that failed to encode
51+
* @param int $depth The nesting depth used
52+
* @return static
53+
*/
54+
public static function fromJsonException(\JsonException $exception, mixed $value, int $depth = 512): static
55+
{
56+
return new static(
57+
message: sprintf('Failed to encode JSON: %s', $exception->getMessage()),
58+
value: $value,
59+
jsonErrorCode: $exception->getCode(),
60+
jsonErrorMessage: $exception->getMessage(),
61+
depth: $depth,
62+
previous: $exception
63+
);
64+
}
65+
}

src/Exceptions/JsonParseException.php

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,59 @@
66

77
use Throwable;
88

9-
class JsonParseException extends TransportException
9+
/**
10+
* Exception thrown when JSON decoding/parsing fails.
11+
*
12+
* This exception provides detailed information about what went wrong
13+
* during JSON parsing, including the error code, the JSON string that
14+
* failed to parse, and the nesting depth used.
15+
*/
16+
class JsonParseException extends SerializationException
1017
{
18+
/**
19+
* Create a new JSON parse exception.
20+
*
21+
* @param string $message The error message
22+
* @param string $jsonString The JSON string that failed to parse
23+
* @param int $jsonErrorCode The JSON error code
24+
* @param string $jsonErrorMessage The JSON error message
25+
* @param int $depth The nesting depth used
26+
* @param Throwable|null $previous The previous exception
27+
*/
1128
public function __construct(
1229
string $message,
1330
public readonly string $jsonString,
1431
public readonly int $jsonErrorCode,
1532
public readonly string $jsonErrorMessage,
33+
public readonly int $depth = 512,
1634
?Throwable $previous = null
1735
) {
18-
parent::__construct($message, 0, $previous);
36+
parent::__construct(
37+
message: $message,
38+
data: $jsonString,
39+
dataSize: strlen($jsonString),
40+
format: 'JSON',
41+
previous: $previous
42+
);
43+
}
44+
45+
/**
46+
* Create from a JsonException.
47+
*
48+
* @param \JsonException $exception The JsonException
49+
* @param string $jsonString The JSON string that failed to parse
50+
* @param int $depth The nesting depth used
51+
* @return static
52+
*/
53+
public static function fromJsonException(\JsonException $exception, string $jsonString, int $depth = 512): static
54+
{
55+
return new static(
56+
message: sprintf('Failed to parse JSON: %s', $exception->getMessage()),
57+
jsonString: $jsonString,
58+
jsonErrorCode: $exception->getCode(),
59+
jsonErrorMessage: $exception->getMessage(),
60+
depth: $depth,
61+
previous: $exception
62+
);
1963
}
2064
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Farzai\Transport\Exceptions;
6+
7+
/**
8+
* Base exception for all serialization-related errors.
9+
*
10+
* This exception is thrown when encoding or decoding data fails,
11+
* regardless of the serialization format (JSON, XML, etc.).
12+
*/
13+
class SerializationException extends TransportException
14+
{
15+
/**
16+
* Create a new serialization exception.
17+
*
18+
* @param string $message The error message
19+
* @param string|null $data The data that failed to serialize (truncated if too large)
20+
* @param int $dataSize The size of the data in bytes
21+
* @param string $format The serialization format (e.g., "JSON", "XML")
22+
* @param \Throwable|null $previous The previous exception
23+
*/
24+
public function __construct(
25+
string $message,
26+
public readonly ?string $data = null,
27+
public readonly int $dataSize = 0,
28+
public readonly string $format = 'unknown',
29+
?\Throwable $previous = null
30+
) {
31+
parent::__construct($message, 0, $previous);
32+
}
33+
34+
/**
35+
* Get a truncated version of the data for safe logging.
36+
*
37+
* @param int $maxLength Maximum length of the truncated string
38+
* @return string|null The truncated data or null if no data
39+
*/
40+
public function getTruncatedData(int $maxLength = 200): ?string
41+
{
42+
if ($this->data === null) {
43+
return null;
44+
}
45+
46+
if (strlen($this->data) <= $maxLength) {
47+
return $this->data;
48+
}
49+
50+
return substr($this->data, 0, $maxLength).' ... (truncated)';
51+
}
52+
}

src/RequestBuilder.php

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Farzai\Transport;
66

77
use Farzai\Transport\Contracts\ResponseInterface;
8+
use Farzai\Transport\Contracts\SerializerInterface;
9+
use Farzai\Transport\Serialization\SerializerFactory;
810
use GuzzleHttp\Psr7\Request;
911
use GuzzleHttp\Psr7\Uri;
1012
use Psr\Http\Message\RequestInterface;
@@ -26,10 +28,13 @@ class RequestBuilder
2628

2729
private ?Transport $transport = null;
2830

29-
public function __construct(?Transport $transport = null)
31+
private SerializerInterface $serializer;
32+
33+
public function __construct(?Transport $transport = null, ?SerializerInterface $serializer = null)
3034
{
3135
$this->transport = $transport;
3236
$this->uri = new Uri;
37+
$this->serializer = $serializer ?? SerializerFactory::createDefault();
3338
}
3439

3540
/**
@@ -93,12 +98,19 @@ public function withBody(StreamInterface|string $body): self
9398

9499
/**
95100
* Set JSON body.
101+
*
102+
* Uses the injected serializer for encoding with proper error handling.
103+
*
104+
* @param mixed $data The data to encode as JSON
105+
* @return self
106+
*
107+
* @throws \Farzai\Transport\Exceptions\JsonEncodeException When encoding fails
96108
*/
97109
public function withJson(mixed $data): self
98110
{
99111
$clone = clone $this;
100-
$clone->body = json_encode($data);
101-
$clone->headers['Content-Type'] = 'application/json';
112+
$clone->body = $this->serializer->encode($data);
113+
$clone->headers['Content-Type'] = $this->serializer->getContentType();
102114

103115
return $clone;
104116
}

0 commit comments

Comments
 (0)