diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..4126d6c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,14 @@ +/tests export-ignore +/vendor export-ignore + +/README.md export-ignore +/LICENSE export-ignore +/Makefile export-ignore +/phpmd.xml export-ignore +/phpunit.xml export-ignore +/phpstan.neon.dist export-ignore +/infection.json.dist export-ignore + +/.github export-ignore +/.gitignore export-ignore +/.gitattributes export-ignore diff --git a/composer.json b/composer.json index 9258894..64e9596 100644 --- a/composer.json +++ b/composer.json @@ -10,8 +10,6 @@ "psr", "json", "array", - "psr-4", - "psr-12", "serializer", "tiny-blocks" ], @@ -21,6 +19,10 @@ "homepage": "https://github.com/gustavofreze" } ], + "support": { + "issues": "https://github.com/tiny-blocks/serializer/issues", + "source": "https://github.com/tiny-blocks/serializer" + }, "config": { "sort-packages": true, "allow-plugins": { @@ -41,14 +43,16 @@ "php": "^8.2" }, "require-dev": { - "infection/infection": "^0.29", "phpmd/phpmd": "^2.15", + "phpstan/phpstan": "^1", "phpunit/phpunit": "^11", + "infection/infection": "^0.29", "squizlabs/php_codesniffer": "^3.10" }, "scripts": { "phpcs": "phpcs --standard=PSR12 --extensions=php ./src", "phpmd": "phpmd ./src text phpmd.xml --suffixes php --ignore-violations-on-exit", + "phpstan": "phpstan analyse -c phpstan.neon.dist --quiet --no-progress", "test": "phpunit --log-junit=report/coverage/junit.xml --coverage-xml=report/coverage/coverage-xml --coverage-html=report/coverage/coverage-html tests", "fix-style": "phpcbf ./src --extensions=php", "test-mutation": "infection --only-covered --logger-html=report/coverage/mutation-report.html --coverage=report/coverage --min-msi=100 --min-covered-msi=100 --threads=4", @@ -56,7 +60,8 @@ "test-mutation-no-coverage": "infection --only-covered --min-msi=100 --threads=4", "review": [ "@phpcs", - "@phpmd" + "@phpmd", + "@phpstan" ], "tests": [ "@test", diff --git a/infection.json.dist b/infection.json.dist index e9bacf3..01fde44 100644 --- a/infection.json.dist +++ b/infection.json.dist @@ -1,15 +1,15 @@ { "timeout": 10, "testFramework": "phpunit", - "tmpDir": "report/", + "tmpDir": "report/infection/", "source": { "directories": [ "src" ] }, "logs": { - "text": "report/logs/infection-text.log", - "summary": "report/logs/infection-summary.log" + "text": "report/infection/logs/infection-text.log", + "summary": "report/infection/logs/infection-summary.log" }, "mutators": { "@default": true, diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..ceb7a86 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,11 @@ +parameters: + paths: + - src + level: 9 + tmpDir: report/phpstan + ignoreErrors: + - '#method toArray#' + - '#object_or_class#' + - '#with no value type specified in#' + - '#return type has no value type specified#' + reportUnmatchedIgnoredErrors: false diff --git a/src/Internal/ArraySerializer.php b/src/Internal/ArraySerializer.php index 8d1d4c6..03861ac 100644 --- a/src/Internal/ArraySerializer.php +++ b/src/Internal/ArraySerializer.php @@ -16,7 +16,7 @@ public function __construct(private iterable $iterable) public function toArray(SerializeKeys $serializeKeys = SerializeKeys::PRESERVE): array { $mapper = new Mapper(serializeKeys: $serializeKeys); - $shouldPreserveKeys = $serializeKeys && $serializeKeys->shouldPreserveKeys(); + $shouldPreserveKeys = $serializeKeys->shouldPreserveKeys(); $elements = is_array($this->iterable) ? $this->iterable diff --git a/src/Internal/JsonSerializer.php b/src/Internal/JsonSerializer.php index d86ab4b..3c674b3 100644 --- a/src/Internal/JsonSerializer.php +++ b/src/Internal/JsonSerializer.php @@ -9,8 +9,14 @@ public function serialize(array $data): string { $isSingleItem = count($data) === 1; - $dataToSerialize = $isSingleItem ? $data[0] : $data; + $dataToSerialize = $isSingleItem ? ($data[0] ?? null) : $data; - return json_encode($dataToSerialize, JSON_PRESERVE_ZERO_FRACTION); + $json = json_encode($dataToSerialize, JSON_PRESERVE_ZERO_FRACTION); + + if (!is_string($json) || $json === 'null') { + return $isSingleItem ? '{}' : '[]'; + } + + return $json; } } diff --git a/src/Internal/Mappers/Mapper.php b/src/Internal/Mappers/Mapper.php index 2ac83b6..31b50ce 100644 --- a/src/Internal/Mappers/Mapper.php +++ b/src/Internal/Mappers/Mapper.php @@ -10,7 +10,7 @@ final class Mapper { private ValueMapper $valueMapper; - public function __construct(?SerializeKeys $serializeKeys) + public function __construct(SerializeKeys $serializeKeys) { $this->valueMapper = new ValueMapper(serializeKeys: $serializeKeys); } diff --git a/src/Serializer.php b/src/Serializer.php index c3759e4..fc8d636 100644 --- a/src/Serializer.php +++ b/src/Serializer.php @@ -15,8 +15,14 @@ interface Serializer /** * Serializes the object to a JSON string. * - * @param SerializeKeys $serializeKeys Optional parameter to define how keys - * should be serialized (default: PRESERVE). + * The key serialization behavior can be customized using the `SerializeKeys` enum: + * - `SerializeKeys::DISCARD`: Discards the array keys. + * - `SerializeKeys::PRESERVE`: Preserves the array keys. + * + * By default, `SerializeKeys::PRESERVE` is used. + * + * @param SerializeKeys $serializeKeys Optional parameter to define whether array keys + * should be preserved or discarded. * @return string The JSON string representing the object. */ public function toJson(SerializeKeys $serializeKeys = SerializeKeys::PRESERVE): string; @@ -24,8 +30,14 @@ public function toJson(SerializeKeys $serializeKeys = SerializeKeys::PRESERVE): /** * Converts the object to an array. * - * @param SerializeKeys $serializeKeys Optional parameter to define how keys - * should be serialized (default: PRESERVE). + * The key serialization behavior can be customized using the `SerializeKeys` enum: + * - `SerializeKeys::DISCARD`: Discards the array keys. + * - `SerializeKeys::PRESERVE`: Preserves the array keys. + * + * By default, `SerializeKeys::PRESERVE` is used. + * + * @param SerializeKeys $serializeKeys Optional parameter to define whether array keys + * should be preserved or discarded. * @return array The array representation of the object. */ public function toArray(SerializeKeys $serializeKeys = SerializeKeys::PRESERVE): array; diff --git a/tests/IterableSerializerTest.php b/tests/IterableSerializerTest.php index 100fcd5..903f80d 100644 --- a/tests/IterableSerializerTest.php +++ b/tests/IterableSerializerTest.php @@ -73,6 +73,18 @@ public function testSerializeToArray(iterable $iterable, array $expected): void self::assertSame($expected, $actual); } + public function testSerializeMultipleInvalidItemsReturnsEmptyJsonArray(): void + { + /** @Given multiple invalid items (e.g., functions that cannot be serialized) */ + $serializer = new IterableSerializer(iterable: [fn(): null => null, fn(): null => null]); + + /** @When attempting to serialize the invalid items */ + $actual = $serializer->toJson(); + + /** @Then the output should be an empty JSON array */ + self::assertSame('[[],[]]', $actual); + } + public static function toJsonDataProvider(): array { $spTimeZone = new DateTimeZone('America/Sao_Paulo'); diff --git a/tests/Models/Serializable/Service.php b/tests/Models/Serializable/Service.php new file mode 100644 index 0000000..b874915 --- /dev/null +++ b/tests/Models/Serializable/Service.php @@ -0,0 +1,18 @@ + 0); + + /** @When attempting to serialize the invalid item */ + $actual = $service->toJson(); + + /** @Then the output should be an empty JSON object */ + self::assertSame('{}', $actual); + } + public static function shippingDataProviderForArray(): array { $shippingWithNoAddresses = new Shipping(id: 1, addresses: new Addresses());