Skip to content

Commit

Permalink
Json Handler V1
Browse files Browse the repository at this point in the history
  • Loading branch information
andreypostal committed Aug 17, 2024
1 parent 0f3027f commit 232495a
Show file tree
Hide file tree
Showing 17 changed files with 623 additions and 1 deletion.
12 changes: 12 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
root = true

[*]
charset = utf-8
indent_size = 4
indent_style = space
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

[*.{yml,yaml}]
indent_size = 2
42 changes: 42 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: "Tests"

on:
push:
branches:
- 'main'
pull_request:

permissions:
contents: read

jobs:
tests:
name: "Package Tests"
runs-on: ubuntu-20.04
continue-on-error: false

steps:
- name: "Checkout"
uses: "actions/checkout@v4"
with:
ref: ${{ github.event.pull_request.head.ref }}
fetch-depth: 100

- name: "Install PHP"
uses: "shivammathur/setup-php@v2"
with:
ini-values: "memory_limit=-1,display_errors=1"
php-version: "8.3"
coverage: "pcov"

- name: "Install Dependencies"
run: "composer install"

- name: "Run Tests"
run: "vendor/bin/phpunit --coverage-php /tmp/${{ github.sha }}_coverage.cov"

- uses: "actions/upload-artifact@v4"
with:
name: "tests_coverage"
path: "/tmp/${{ github.sha }}_coverage.cov"
retention-days: 1
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
composer.phar
/vendor/
.idea
phpunit.xml
composer.lock
.DS_Store
.php-cs-fixer.cache
.hg
.phpunit.result.cache
.phpunit.cache

# 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
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2024 Andrey Postal
Copyright (c) Andrey Postal

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
24 changes: 24 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "andreypostal/json-handler-php",
"description": "Just a light and simple JSON helper that will make it easy for you to deal with json and objects",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Andrey Postal",
"email": "[email protected]"
}
],
"minimum-stability": "dev",
"autoload": {
"psr-4": {
"Andrey\\JsonHandler\\": "src/"
}
},
"require-dev": {
"phpunit/phpunit": "^11.4@dev"
},
"scripts": {
"test": "phpunit"
}
}
26 changes: 26 additions & 0 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>

<phpunit
backupGlobals="false"
backupStaticProperties="false"
defaultTestSuite="Test Suite"
colors="true"
processIsolation= "false"
stopOnFailure="false"
bootstrap = "tests/bootstrap.php"
>
<testsuites>
<testsuite name="Test Suite">
<directory>tests</directory>
<exclude>src/</exclude>
</testsuite>
</testsuites>

<coverage/>

<source>
<include>
<directory suffix=".php">src/</directory>
</include>
</source>
</phpunit>
27 changes: 27 additions & 0 deletions src/JsonHandler.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

namespace Andrey\JsonHandler;

use JsonException;

final class JsonHandler
{
use JsonSerializerTrait;
use JsonHydratorTrait;

/**
* @throws JsonException
*/
public static function Decode(string $json): array
{
return json_decode($json, true, 512, JSON_THROW_ON_ERROR);
}

/**
* @throws JsonException
*/
public static function Encode(array $jsonArr): string
{
return json_encode($jsonArr, JSON_THROW_ON_ERROR);
}
}
92 changes: 92 additions & 0 deletions src/JsonHydratorTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
<?php
namespace Andrey\JsonHandler;

use InvalidArgumentException;
use JsonException;
use LogicException;
use ReflectionClass;
use ReflectionProperty;

trait JsonHydratorTrait
{
/**
* @throws JsonException
*/
public function hydrateObjectImmutable(string|array $json, object $obj): object
{
return $this->hydrateObject($json, clone $obj);
}

/**
* @throws JsonException
*/
public function hydrateObject(string|array $json, object $obj): 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;
}

/**
* @throws JsonException
*/
private function processClass(ReflectionClass $class, array $jsonArr): array
{
$output = [];
$properties = $class->getProperties();
foreach ($properties as $property) {
$output[$property->getName()] = $this->processProperty($property, $jsonArr);
}
return $output;
}

/**
* @throws JsonException
*/
private function processProperty(ReflectionProperty $property, array $jsonArr): mixed
{
$attributes = $property->getAttributes(JsonItemAttribute::class);
$attr = $attributes[0] ?? null;
if ($attr === null) {
return null;
}

/** @var JsonItemAttribute $item */
$item = $attr->newInstance();
$key = $item->key ?? $property->getName();
if ($item->required && !array_key_exists($key, $jsonArr)) {
throw new InvalidArgumentException(sprintf('required item <%s> not found', $key));
}

if ($property->getType()?->isBuiltin()) {
if ($item->type !== null && $property->getType()?->getName() === 'array') {
$output = [];
$classExists = class_exists($item->type);
foreach ($jsonArr[$key] ?? [] as $k => $v) {
$value = $v;
if ($classExists) {
$value = $this->hydrateObject($v, new $item->type);
} elseif (gettype($v) !== $item->type) {
throw new LogicException(sprintf('expected array with items of type <%s> but found <%s>', $item->type, gettype($v)));
}
$output[$k] = $value;
}
return $output;
}
return $jsonArr[$key] ?? $property->getDefaultValue();
}

return $this->hydrateObject(
$jsonArr[$key],
new ($property->getType()?->getName())(),
);
}
}
10 changes: 10 additions & 0 deletions src/JsonItemAttribute.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php
namespace Andrey\JsonHandler;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class JsonItemAttribute
{
public function __construct(public ?string $key = null, public bool $required = false, public ?string $type = null) {}
}
31 changes: 31 additions & 0 deletions src/JsonSerializerTrait.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
namespace Andrey\JsonHandler;

use ReflectionClass;

trait JsonSerializerTrait
{
public function serialize(object $obj): array
{
$class = new ReflectionClass($obj);
$output = [];
$properties = $class->getProperties();
foreach ($properties as $property) {
$attributes = $property->getAttributes(JsonItemAttribute::class);
$attr = $attributes[0] ?? null;
if ($attr === null) {
continue;
}
/** @var JsonItemAttribute $item */
$item = $attr->newInstance();
$key = $item->key ?? $property->name;

if ($property->getType()?->isBuiltin()) {
$output[$key] = $property->getValue($obj);
continue;
}
$output[$key] = $this->serialize($property->getValue($obj));
}
return $output;
}
}
Loading

0 comments on commit 232495a

Please sign in to comment.