Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/Objects/Serializers/Attributes/MapInputName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Attributes;

use Attribute;
use EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers\IPropertyMapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
class MapInputName
{
public function __construct(
public string|IPropertyMapper $mapper
) {}
}
29 changes: 29 additions & 0 deletions src/Objects/Serializers/Attributes/MapName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Attributes;

use Attribute;
use EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers\IPropertyMapper;

/**
* @property string|IPropertyMapper $input Mapper used for deserialization operations
* @property string|IPropertyMapper $output Mapper used for serialization operations. Defaults to input.
*/
#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
class MapName
{
public function __construct(
public string|IPropertyMapper $input,
public string|IPropertyMapper|null $output = null
)
{
/** @phpstan-ignore-next-line False positive from doc block */
if (is_null($this->output)) {
$this->output = $input;
}
}
}
18 changes: 18 additions & 0 deletions src/Objects/Serializers/Attributes/MapOutputName.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Attributes;

use Attribute;
use EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers\IPropertyMapper;

#[Attribute(Attribute::TARGET_PROPERTY | Attribute::TARGET_CLASS)]
class MapOutputName
{
public function __construct(
public string|IPropertyMapper $mapper
) {}
}
169 changes: 169 additions & 0 deletions src/Objects/Serializers/JsonSerializer.php
Original file line number Diff line number Diff line change
@@ -1,12 +1,26 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers;

use EncoreDigitalGroup\StdLib\Objects\Serializers\Attributes\MapInputName;
use EncoreDigitalGroup\StdLib\Objects\Serializers\Attributes\MapName;
use EncoreDigitalGroup\StdLib\Objects\Serializers\Attributes\MapOutputName;
use EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers\IPropertyMapper;
use Illuminate\Support\Collection;
use ReflectionClass;
use ReflectionProperty;
use RuntimeException;
use Symfony\Component\Serializer\Encoder\JsonEncoder;

class JsonSerializer extends AbstractSerializer
{
private const string MAPPER_DIRECTION_INPUT = "input";
private const string MAPPER_DIRECTION_OUTPUT = "output";

protected static Collection $normalizers;

protected static function format(): string
Expand All @@ -18,4 +32,159 @@ protected static function encoders(): array
{
return [(new JsonEncoder)];
}

public static function serialize(object $object): string
{
if (self::hasMapNameAttributes($object)) {
return self::serializeWithMapName($object);
}

return parent::serialize($object);
}

/** @param class-string $class */
public static function deserialize(string $class, string $data): mixed
{
if (self::hasMapNameAttributes($class)) {
return self::deserializeWithMapName($class, $data);
}

return parent::deserialize($class, $data);
}

/**
* @param class-string|object $classOrObject
*/
private static function hasMapNameAttributes(string|object $classOrObject): bool
{
$reflection = new ReflectionClass($classOrObject);

if (!empty($reflection->getAttributes(MapName::class)) ||
!empty($reflection->getAttributes(MapInputName::class)) ||
!empty($reflection->getAttributes(MapOutputName::class))) {
return true;
}

foreach ($reflection->getProperties() as $property) {
if (!empty($property->getAttributes(MapName::class)) ||
!empty($property->getAttributes(MapInputName::class)) ||
!empty($property->getAttributes(MapOutputName::class))) {
return true;
}
}

return false;
}

private static function serializeWithMapName(object $object): string
{
$reflection = new ReflectionClass($object);
$data = [];

foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$propertyName = $property->getName();
$value = $property->getValue($object);

$mappedName = self::getMappedOutputName($property, $reflection) ?? $propertyName;
$data[$mappedName] = $value;
}

$json = json_encode($data);
if ($json === false) {
throw new RuntimeException('Failed to encode JSON');
}

return $json;
}

/**
* @param class-string $class
*/
private static function deserializeWithMapName(string $class, string $jsonData): mixed
{
$data = json_decode($jsonData, true);
$reflection = new ReflectionClass($class);
$constructor = $reflection->getConstructor();

if (!$constructor) {
return $reflection->newInstance();
}

$args = [];
foreach ($constructor->getParameters() as $parameter) {
$args[] = self::resolveParameterValue($parameter, $reflection, $data);
}

return $reflection->newInstanceArgs($args);
}

private static function resolveParameterValue(\ReflectionParameter $parameter, ReflectionClass $reflection, array $data): mixed
{
$parameterName = $parameter->getName();
$property = $reflection->hasProperty($parameterName) ? $reflection->getProperty($parameterName) : null;

if ($property) {
$mappedName = self::getMappedInputName($property, $reflection) ?? $parameterName;
$keyToUse = $mappedName;
} else {
$keyToUse = $parameterName;
}

if (array_key_exists($keyToUse, $data)) {
return $data[$keyToUse];
}

return $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
}

private static function getMappedOutputName(ReflectionProperty $property, ReflectionClass $classReflection): ?string
{
return self::getMapperForDirection($property, $property->getName(), self::MAPPER_DIRECTION_OUTPUT)
?? self::getMapperForDirection($classReflection, $property->getName(), self::MAPPER_DIRECTION_OUTPUT);
}

private static function getMappedInputName(ReflectionProperty $property, ReflectionClass $classReflection): ?string
{
return self::getMapperForDirection($property, $property->getName(), self::MAPPER_DIRECTION_INPUT)
?? self::getMapperForDirection($classReflection, $property->getName(), self::MAPPER_DIRECTION_INPUT);
}

private static function getMapperResult(string|IPropertyMapper $mapper, string $propertyName): string
{
if (is_string($mapper)) {
if (class_exists($mapper)) {
$mapperInstance = new $mapper();
if ($mapperInstance instanceof IPropertyMapper) {
return $mapperInstance->map($propertyName);
}
}
return $mapper;
}

return $mapper->map($propertyName);
}

private static function getMapperForDirection(ReflectionProperty|ReflectionClass $reflector, string $propertyName, string $direction): ?string
{
$specificAttributeClass = $direction === self::MAPPER_DIRECTION_INPUT ? MapInputName::class : MapOutputName::class;

$specificAttributes = $reflector->getAttributes($specificAttributeClass);
if (!empty($specificAttributes)) {
return self::getMapperResult($specificAttributes[0]->newInstance()->mapper, $propertyName);
}

$mapNameAttributes = $reflector->getAttributes(MapName::class);
if (!empty($mapNameAttributes)) {
$mapNameInstance = $mapNameAttributes[0]->newInstance();
$mapper = $direction === self::MAPPER_DIRECTION_OUTPUT
? ($mapNameInstance->output ?? $mapNameInstance->input)
: $mapNameInstance->input;
if ($mapper !== null) {
return self::getMapperResult($mapper, $propertyName);
}
}

return null;
}
}
17 changes: 17 additions & 0 deletions src/Objects/Serializers/Mappers/CamelCaseMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers;

use EncoreDigitalGroup\StdLib\Objects\Support\Types\Str;

class CamelCaseMapper implements IPropertyMapper
{
public function map(string $propertyName): string
{
return Str::camel($propertyName);
}
}
12 changes: 12 additions & 0 deletions src/Objects/Serializers/Mappers/IPropertyMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers;

interface IPropertyMapper
{
public function map(string $propertyName): string;
}
17 changes: 17 additions & 0 deletions src/Objects/Serializers/Mappers/PascalCaseMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers;

use EncoreDigitalGroup\StdLib\Objects\Support\Types\Str;

class PascalCaseMapper implements IPropertyMapper
{
public function map(string $propertyName): string
{
return Str::studly($propertyName);
}
}
17 changes: 17 additions & 0 deletions src/Objects/Serializers/Mappers/ProvidedNameMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers;

readonly class ProvidedNameMapper implements IPropertyMapper
{
public function __construct(private string $mappedName) {}

public function map(string $propertyName): string
{
return $this->mappedName;
}
}
17 changes: 17 additions & 0 deletions src/Objects/Serializers/Mappers/SnakeCaseMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<?php
/*
* Copyright (c) 2025. Encore Digital Group.
* All Rights Reserved.
*/

namespace EncoreDigitalGroup\StdLib\Objects\Serializers\Mappers;

use EncoreDigitalGroup\StdLib\Objects\Support\Types\Str;

class SnakeCaseMapper implements IPropertyMapper
{
public function map(string $propertyName): string
{
return Str::snake($propertyName);
}
}
Loading
Loading