VanguardAI/app/Helpers/SerializeHelper.php
2024-10-27 12:50:51 -06:00

316 lines
8.2 KiB
PHP

<?php
namespace App\Helpers;
use DOMDocument;
use DOMElement;
use JsonException;
use RuntimeException;
use WeakMap;
use Stringable;
use ValueError;
class SerializeHelper
{
private const JSON_OPTIONS =
JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES;
private const XML_VERSION = "1.0";
private const XML_ENCODING = "UTF-8";
/**
* Caché usando WeakMap para mejor gestión de memoria
*/
private static WeakMap $cache;
public function __construct()
{
self::$cache = new WeakMap();
}
/**
* Convierte datos a JSON
*
* @template T
* @param T $data
* @return string
* @throws JsonException
*/
public static function toJson(mixed $data): string
{
try {
return json_encode(
self::normalize($data),
self::JSON_OPTIONS | JSON_THROW_ON_ERROR
);
} catch (JsonException $e) {
throw new JsonException(
"Error en la serialización JSON: {$e->getMessage()}"
);
}
}
/**
* Convierte datos a XML
*/
public static function toXml(
mixed $data,
string $rootElement = "root"
): string {
$dom = new DOMDocument(self::XML_VERSION, self::XML_ENCODING);
$dom->formatOutput = true;
$root = $dom->createElement($rootElement);
$dom->appendChild($root);
self::arrayToDomElement($data, $root, $dom);
return $dom->saveXML() ?:
throw new RuntimeException("Error generando XML");
}
/**
* Método auxiliar para convertir array a elementos DOM
*/
private static function arrayToDomElement(
mixed $data,
DOMElement $parent,
DOMDocument $dom
): void {
$data = match (true) {
$data instanceof Stringable => (string) $data,
is_object($data) => (array) $data,
default => $data,
};
foreach ((array) $data as $key => $value) {
$key = preg_replace("/[^a-z0-9_]/i", "_", (string) $key) ?: "item";
$child = match (true) {
is_array($value),
is_object($value)
=> self::createComplexElement($dom, $key, $value),
default => self::createSimpleElement($dom, $key, $value),
};
$parent->appendChild($child);
}
}
/**
* Crea un elemento XML complejo
*/
private static function createComplexElement(
DOMDocument $dom,
string $key,
mixed $value
): DOMElement {
$element = $dom->createElement($key);
self::arrayToDomElement($value, $element, $dom);
return $element;
}
/**
* Crea un elemento XML simple
*/
private static function createSimpleElement(
DOMDocument $dom,
string $key,
mixed $value
): DOMElement {
$element = $dom->createElement($key);
$element->appendChild($dom->createTextNode((string) $value));
return $element;
}
/**
* Serializa datos de manera optimizada
*/
public static function serialize(mixed $data): string
{
return match (true) {
extension_loaded("igbinary") => igbinary_serialize($data),
default => serialize($data),
};
}
/**
* Deserializa datos
*/
public static function unserialize(string $data): mixed
{
return match (true) {
extension_loaded("igbinary") &&
!str_starts_with($data, "a:") &&
!str_starts_with($data, "O:")
=> igbinary_unserialize($data),
default => unserialize($data, ["allowed_classes" => true]),
};
}
/**
* Compara objetos o arrays
*/
public static function compare(mixed $obj1, mixed $obj2): array
{
return self::calculateDifferences(
self::normalize($obj1),
self::normalize($obj2)
);
}
/**
* Calcula diferencias entre arrays
*/
private static function calculateDifferences(
array $array1,
array $array2,
string $path = ""
): array {
$differences = [];
foreach ($array1 as $key => $value) {
$currentPath = $path ? "{$path}.{$key}" : $key;
if (!array_key_exists($key, $array2)) {
$differences[$currentPath] = [
"type" => "removed",
"value" => $value,
];
continue;
}
$differences = match (true) {
is_array($value) && is_array($array2[$key]) => [
...$differences,
...self::calculateDifferences(
$value,
$array2[$key],
$currentPath
),
],
$value !== $array2[$key] => [
...$differences,
$currentPath => [
"type" => "modified",
"old" => $value,
"new" => $array2[$key],
],
],
default => $differences,
};
}
// Verificar elementos adicionales
foreach ($array2 as $key => $value) {
if (!array_key_exists($key, $array1)) {
$currentPath = $path ? "{$path}.{$key}" : $key;
$differences[$currentPath] = [
"type" => "added",
"value" => $value,
];
}
}
return $differences;
}
/**
* Normaliza datos para comparación
*/
public static function normalize(mixed $data): array
{
return match (true) {
is_object($data) => self::normalizeObject($data),
is_array($data) => self::normalizeArray($data),
default => [$data],
};
}
/**
* Normaliza un objeto
*/
private static function normalizeObject(object $object): array
{
return match (true) {
method_exists($object, "toArray") => $object->toArray(),
default => self::normalizeArray(get_object_vars($object)),
};
}
/**
* Normaliza un array
*/
private static function normalizeArray(array $array): array
{
return array_map(
fn($value) => match (true) {
is_object($value), is_array($value) => self::normalize($value),
default => $value,
},
$array
);
}
/**
* Convierte a array
*/
public static function toArray(mixed $data): array
{
return match (true) {
is_object($data) && method_exists($data, "toArray")
=> $data->toArray(),
is_object($data) => self::normalize($data),
is_array($data) => array_map([self::class, "toArray"], $data),
default => [$data],
};
}
/**
* Clonación profunda
*
* @template T of object
* @param T $object
* @return T
*/
public static function deepClone(object $object): object
{
if (self::$cache->offsetExists($object)) {
return self::$cache->offsetGet($object);
}
$clone = match (true) {
extension_loaded("igbinary") => igbinary_unserialize(
igbinary_serialize($object)
),
method_exists($object, "__clone") => self::cloneWithProperties(
$object
),
default => unserialize(serialize($object)),
};
self::$cache->offsetSet($object, $clone);
return $clone;
}
/**
* Clona un objeto y sus propiedades
*/
private static function cloneWithProperties(object $object): object
{
$clone = clone $object;
foreach (get_object_vars($clone) as $property => $value) {
if (is_object($value)) {
$clone->$property = self::deepClone($value);
}
}
return $clone;
}
/**
* Limpia la caché
*/
public static function clearCache(): void
{
self::$cache = new WeakMap();
}
}