316 lines
8.2 KiB
PHP
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();
|
|
}
|
|
}
|