Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Rework trait usage and add key mapping strategy #8

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ composer.lock
.hg
.phpunit.result.cache
.phpunit.cache
temp

# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
Expand Down
8 changes: 8 additions & 0 deletions src/HydratorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Andrey\JsonHandler;

interface HydratorInterface
{
public function hydrate(string|array $json, object|string $objOrClass): object;
}
27 changes: 0 additions & 27 deletions src/JsonHandler.php

This file was deleted.

53 changes: 27 additions & 26 deletions src/JsonHydratorTrait.php → src/JsonHydrator.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,56 +3,49 @@

use Andrey\JsonHandler\Attributes\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonObjectAttribute;
use Andrey\JsonHandler\KeyMapping\KeyMappingStrategy;
use Andrey\JsonHandler\KeyMapping\KeyMappingUnderscore;
use InvalidArgumentException;
use JsonException;
use LogicException;
use ReflectionClass;
use ReflectionException;
use ReflectionProperty;

trait JsonHydratorTrait
readonly class JsonHydrator implements HydratorInterface
{
/**
* @throws JsonException
* @throws ReflectionException
*/
public function hydrateObjectImmutable(string|array $json, object $obj): object
{
return $this->hydrateObject($json, clone $obj);
private KeyMappingStrategy $keyStrategy;

public function __construct(
?KeyMappingStrategy $keyStrategy = null,
) {
$this->keyStrategy = $keyStrategy ?: new KeyMappingUnderscore();
}

/**
* @throws JsonException
* @throws ReflectionException
*/
public function hydrateObject(string|array $json, object $obj): object
public function hydrate(string|array $json, object|string $objOrClass): object
{
$jsonArr = is_string($json) ? JsonHandler::Decode($json) : $json;
$reflectionClass = new ReflectionClass($obj);
$data = $this->processClass($reflectionClass, $jsonArr);
if ($reflectionClass->hasMethod('hydrate')) {
$obj->hydrate($data);
} else {
foreach ($data as $key => $value) {
$obj->{$key} = $value;
}
}
return $obj;
$jsonArr = is_string($json) ? $this->decode($json) : $json;
$reflectionClass = new ReflectionClass($objOrClass);
return $this->processClass($reflectionClass, $jsonArr);
}

/**
* @throws JsonException
* @throws ReflectionException
*/
private function processClass(ReflectionClass $class, array $jsonArr): array
private function processClass(ReflectionClass $class, array $jsonArr): object
{
$instance = $class->newInstance();
$skipAttributeCheck = ($class->getAttributes(JsonObjectAttribute::class)[0] ?? null) !== null;
$output = [];
$properties = $class->getProperties();
foreach ($properties as $property) {
$output[$property->getName()] = $this->processProperty($property, $jsonArr, $skipAttributeCheck);
$property->setValue($instance, $this->processProperty($property, $jsonArr, $skipAttributeCheck));
}
return $output;
return $instance;
}

/**
Expand All @@ -69,7 +62,7 @@ private function processProperty(ReflectionProperty $property, array $jsonArr, b

/** @var JsonItemAttribute $item */
$item = $attr?->newInstance() ?? new JsonItemAttribute();
$key = $item->key ?? $property->getName();
$key = $item->key ?? $this->keyStrategy->from($property->getName());
if ($item->required && !array_key_exists($key, $jsonArr)) {
throw new InvalidArgumentException(sprintf('required item <%s> not found', $key));
}
Expand Down Expand Up @@ -114,9 +107,17 @@ private function handleCustomType(mixed $value, string $type): mixed
if ($typeReflection->isEnum()) {
return call_user_func($type.'::tryFrom', $value);
}
return $this->hydrateObject(
return $this->hydrate(
$value,
new ($type)(),
);
}

/**
* @throws JsonException
*/
private function decode(string $json): mixed
{
return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
}
}
14 changes: 12 additions & 2 deletions src/JsonSerializerTrait.php → src/JsonSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,22 @@

use Andrey\JsonHandler\Attributes\JsonItemAttribute;
use Andrey\JsonHandler\Attributes\JsonObjectAttribute;
use Andrey\JsonHandler\KeyMapping\KeyMappingStrategy;
use Andrey\JsonHandler\KeyMapping\KeyMappingUnderscore;
use JsonException;
use ReflectionClass;
use ReflectionException;

trait JsonSerializerTrait
readonly class JsonSerializer implements SerializerInterface
{
private KeyMappingStrategy $keyStrategy;

public function __construct(
?KeyMappingStrategy $keyStrategy = null,
) {
$this->keyStrategy = $keyStrategy ?: new KeyMappingUnderscore();
}

/**
* @throws ReflectionException
* @throws JsonException
Expand All @@ -28,7 +38,7 @@ public function serialize(object $obj): array
}
/** @var JsonItemAttribute $item */
$item = $attr?->newInstance() ?? new JsonItemAttribute();
$key = $item->key ?? $property->name;
$key = $item->key ?? $this->keyStrategy->from($property->name);

if ($property->getType()?->isBuiltin()) {
$output[$key] = $this->handleArray($item, $property->getValue($obj));
Expand Down
11 changes: 11 additions & 0 deletions src/KeyMapping/KeyMappingStrategy.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

namespace Andrey\JsonHandler\KeyMapping;

interface KeyMappingStrategy
{
// -> Hydrate
public function from(string $key): string;
// <- Parse
public function to(string $key): string;
}
60 changes: 60 additions & 0 deletions src/KeyMapping/KeyMappingUnderscore.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php /** @noinspection ForeachInvariantsInspection */

namespace Andrey\JsonHandler\KeyMapping;

/**
*
*/
class KeyMappingUnderscore implements KeyMappingStrategy
{
/**
* Map keys from pascalCase to underscore_case
*
*/
public function from(string $key): string
{
$in = str_split($key);
$len = count($in);
$out = '';
for ($i = 0; $i < $len; $i++) {
if ($in[$i] < 'a') {
$out .= '_';
// if already is an underscore, just skip case conversion
// but still add another underscore before it
if ($in[$i] !== '_') {
$out .= chr((ord($in[$i]) - ord('A')) + ord('a'));
} else {
$out .= $in[$i];
}
} else {
$out .= $in[$i];
}
}
return $out;
}

public function to(string $key): string
{
$in = str_split($key);
$len = count($in);
$out = '';
// my_key => myKey
for ($i = 0; $i < $len; $i++) {
$c = $in[$i];
if ($c === '_') {
$c = $in[$i+1];
// jump to next letter (skip lowercase already dealt with)
$i++;
if ($c !== '_') {
$out .= chr((ord($c) - ord('a')) + ord('A'));
} else {
$out .= $c;
}
} else {
$out .= $c;
}
}

return $out;
}
}
8 changes: 8 additions & 0 deletions src/SerializerInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<?php

namespace Andrey\JsonHandler;

interface SerializerInterface
{
public function serialize(object $obj): array;
}
58 changes: 58 additions & 0 deletions tests/KeyMapping/KeyMappingUnderscoreTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php
namespace KeyMapping;

use Andrey\JsonHandler\KeyMapping\KeyMappingUnderscore;
use PHPUnit\Framework\Attributes\CoversTrait;
use PHPUnit\Framework\TestCase;

#[CoversTrait(KeyMappingUnderscore::class)]
final class KeyMappingUnderscoreTest extends TestCase
{
public function testFromKey(): void
{
$strategy = new KeyMappingUnderscore();
$result = $strategy->from('fromMyKey');
$this->assertEquals('from_my_key', $result);
}

public function testFromKeyStartingWithUnderscore(): void
{
$strategy = new KeyMappingUnderscore();
$result = $strategy->from('_fromMyKey');
$this->assertEquals('__from_my_key', $result);
}

/**
* For pascal case maintain behavior otherwise we cannot keep
* the equality (parser -> serializer and serializer -> parser)
*
* i.e. _from_my_key => FromMyKey but from_my_key => fromMyKey
*/
public function testFromKeyPascalCase(): void
{
$strategy = new KeyMappingUnderscore();
$result = $strategy->from('FromMyKey');
$this->assertEquals('_from_my_key', $result);
}

public function testToKey(): void
{
$strategy = new KeyMappingUnderscore();
$result = $strategy->to('from_my_key');
$this->assertEquals('fromMyKey', $result);
}

public function testToKeyStartingWithUnderscore(): void
{
$strategy = new KeyMappingUnderscore();
$result = $strategy->to('__from_my_key');
$this->assertEquals('_fromMyKey', $result);
}

public function testToKeyPascalCase(): void
{
$strategy = new KeyMappingUnderscore();
$result = $strategy->to('_from_my_key');
$this->assertEquals('FromMyKey', $result);
}
}
Loading