diff --git a/README.md b/README.md index 77a4e54..91dfc13 100644 --- a/README.md +++ b/README.md @@ -239,31 +239,50 @@ para poder revisar el problema sobre la información enviada. Esta librería genera mensajes utilizando *PSR-3: Logger Interface*, y se utiliza dentro del objeto `SoapFactory` para crear un `SoapCaller`. Este objeto envía dos tipos de mensajes: `LogLevel::ERROR` cuando ocurre un error al momento de establecer comunicación con los servicios, y `LogLevel::DEBUG` cuando se ejecutó una llamada SOAP. -Ambos mensajes están representados como una cadena en formato JSON, por lo que, para leerla correctamente +Ambos mensajes están representados como una cadena en formato JSON, por lo que, para leerla fácilmente es importante decodificarla. -La clase [`PhpCfdi\Finkok\Tests\LoggerPrinter`](https://github.com/phpcfdi/finkok/blob/main/tests/LoggerPrinter.php) -es un *ejemplo de implementación* de `LoggerInterface` que manda los mensajes recibidos a la salida estándar o -a un archivo. Es importante notar que el objeto `LoggerPrinter` no está disponible en el paquete, sin embargo, -lo puedes descargar y poner dentro de tu proyecto con tu espacio de nombres. +El formato JSON es mejor dado que permite analizar el texto y encontrar caracteres especiales, +mientras que, al convertirlo a un texto más entendible para el humano, estos caracteres especiales +se pueden esconder o interpretar de forma errónea. -De igual forma, se puede utilizar cualquier objeto que implemente `LoggerInterface`, por ejemplo, en Laravel se -puede usar `$logger = app(Psr\Log\LoggerInterface::class)`. Pero recuerda que, una vez que tengas el mensaje, -deberás decodificarlo de JSON a texto plano. +Se ofrece la clase `PhpCfdi\Finkok\Helpers\FileLogger` como una utilería de `LoggerInterface` +que manda los mensajes recibidos a la salida estándar o a un archivo. -Para establecer el objeto `Logger` es recomendable hacerlo de la siguiente forma: +También se ofrece la clase `PhpCfdi\Finkok\Helpers\JsonDecoderLogger` como una utilería de `LoggerInterface` +que decodifica el mensaje JSON y luego lo convierte a cadena de caracteres usando la función `print_r()`, +para después mandarlo a otro objeto `LoggerInterface`. + +En el siguiente ejemplo se muestra la forma recomendada para establecer el objeto `Logger`, +también se muestra el uso de `JsonDecoderLogger` para realizar la conversión de JSON a texto plano y +`FileLogger` para enviar el mensaje a un archivo específico. + +La clase `JsonDecoderLogger` puede generar pérdida de información, pero los mensajes son más entendibles, +si deseas también incluir el mensaje JSON puedes usar `JsonDecoderLogger::setAlsoLogJsonMessage(true)`. ```php use PhpCfdi\Finkok\FinkokEnvironment; use PhpCfdi\Finkok\FinkokSettings; -use PhpCfdi\Finkok\Tests\LoggerPrinter; +use PhpCfdi\Finkok\Helpers\FileLogger; +use PhpCfdi\Finkok\Helpers\JsonDecoderLogger; -$logger = new LoggerPrinter('/tmp/finkok.log'); +$logger = new JsonDecoderLogger(new FileLogger('/tmp/finkok.log')); +$logger->setAlsoLogJsonMessage(true); // enviar en texto simple y también en formato JSON $settings = new FinkokSettings('user@host.com', 'secret', FinkokEnvironment::makeProduction()); $settings->soapFactory()->setLogger($logger); ``` +Si estás usando Laravel, ya cuentas con una implementación de `LoggerInterface`, por lo que te recomiendo usar: + +```php +/** @var \Psr\Log\LoggerInterface $logger */ +$logger = app(\Psr\Log\LoggerInterface::class); + +// Encapsular el logger en el decodificador JSON: +$logger = new \PhpCfdi\Finkok\Helpers\JsonDecoderLogger($logger); +``` + ## Compatibilidad Esta librería se mantendrá compatible con al menos la versión con diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 278ef0a..79308de 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -6,6 +6,18 @@ Nos apegamos a [SEMVER](SEMVER.md), revisa la información para entender mejor e Estos cambios se aplican y se publican, pero aún no son parte de una versión liberada. +## Versión 0.5.5 2024-05-24 + +Se mueve `PhpCfdi\Finkok\Tests\LoggerPrinter` a `PhpCfdi\Finkok\Helpers\FileLogger` para permitir la distribución +de la herramienta dentro de la librería. + +Se crea la utilería `PhpCfdi\Finkok\Helpers\JsonDecoderLogger` para transformar un mensaje JSON a texto simple +generado por la función `print_r`. Se puede configurar para enviar también el mensaje JSON. + +Se normaliza el formato de los mensajes JSON para usar `JSON_PRETTY_PRINT` y `JSON_UNESCAPED_SLASHES`. + +Se actualiza la documentación en el `README`. + ## Versión 0.5.4 2024-04-12 Se actualiza el año de la licencia a 2024. diff --git a/docs/TODO.md b/docs/TODO.md index 5769ee9..96250a4 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,5 +1,8 @@ # phpcfdi/finkok To Do List +- Modificar la clase `SoapCaller` para que no dependa de `LoggerInterface` y en su lugar introducir + una interfaz para capturar los eventos de llamada exitosa y llamada con error. + - Agregar la ejecución de test de integración al flujo de trabajo `.github/workflows/build.yml`; es necesario entender cómo funcionan los secretos para poder crear un archivo de entorno seguro. diff --git a/tests/LoggerPrinter.php b/src/Helpers/FileLogger.php similarity index 62% rename from tests/LoggerPrinter.php rename to src/Helpers/FileLogger.php index 9458efe..5fb623d 100644 --- a/tests/LoggerPrinter.php +++ b/src/Helpers/FileLogger.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpCfdi\Finkok\Tests; +namespace PhpCfdi\Finkok\Helpers; use Psr\Log\AbstractLogger; use Psr\Log\LoggerInterface; -final class LoggerPrinter extends AbstractLogger implements LoggerInterface +final class FileLogger extends AbstractLogger implements LoggerInterface { /** @var string */ public $outputFile; @@ -24,10 +24,6 @@ public function __construct(string $outputFile = 'php://stdout') */ public function log($level, $message, array $context = []): void { - file_put_contents( - $this->outputFile, - PHP_EOL . print_r(json_decode(strval($message)), true), - FILE_APPEND - ); + file_put_contents($this->outputFile, $message . PHP_EOL, FILE_APPEND); } } diff --git a/src/Helpers/JsonDecoderLogger.php b/src/Helpers/JsonDecoderLogger.php new file mode 100644 index 0000000..94995ce --- /dev/null +++ b/src/Helpers/JsonDecoderLogger.php @@ -0,0 +1,119 @@ +logger = $logger; + } + + /** + * Define si se utilizará la función \json_validate en caso de estar disponible. + * + * @param bool|null $value El nuevo estado, si se establece NULL entonces solo devuelve el espado previo. + * @return bool El estado previo + */ + public function setUseJsonValidateIfAvailable(bool $value = null): bool + { + $previous = $this->useJsonValidateIfAvailable; + if (null !== $value) { + $this->useJsonValidateIfAvailable = $value; + } + return $previous; + } + + /** + * Define si también se mandará el mensaje JSON al Logger. + * + * @param bool|null $value El nuevo estado, si se establece NULL entonces solo devuelve el espado previo. + * @return bool El estado previo + */ + public function setAlsoLogJsonMessage(bool $value = null): bool + { + $previous = $this->alsoLogJsonMessage; + if (null !== $value) { + $this->alsoLogJsonMessage = $value; + } + return $previous; + } + + public function lastMessageWasJsonValid(): bool + { + return $this->lastMessageWasJsonValid; + } + + /** + * @inheritDoc + * @param string|\Stringable $message + * @param mixed[] $context + */ + public function log($level, $message, array $context = []): void + { + $this->logger->log($level, $this->jsonDecode($message), $context); + if ($this->lastMessageWasJsonValid && $this->alsoLogJsonMessage) { + $this->logger->log($level, $message, $context); + } + } + + /** @param string|\Stringable $string */ + private function jsonDecode($string): string + { + $this->lastMessageWasJsonValid = false; + $string = strval($string); + + // json_validate and json_decode + if ($this->useJsonValidateIfAvailable && function_exists('\json_validate')) { + if (\json_validate($string)) { + $this->lastMessageWasJsonValid = true; + return $this->varDump(json_decode($string)); + } + + return $string; + } + + // json_decode only + $decoded = json_decode($string); + if (JSON_ERROR_NONE === json_last_error()) { + $this->lastMessageWasJsonValid = true; + return $this->varDump($decoded); + } + + return $string; + } + + /** @param mixed $var */ + private function varDump($var): string + { + return print_r($var, true); + } +} diff --git a/src/SoapCaller.php b/src/SoapCaller.php index 6811f11..4d2353d 100644 --- a/src/SoapCaller.php +++ b/src/SoapCaller.php @@ -59,7 +59,7 @@ public function call(string $methodName, array $parameters): stdClass $result = $soap->__soapCall($methodName, [$finalParameters]); $this->logger->debug(strval(json_encode([ $methodName => $this->extractSoapClientTrace($soap), - ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE))); + ], JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES))); /** @var stdClass $result */ return $result; } catch (Throwable $exception) { @@ -69,7 +69,7 @@ public function call(string $methodName, array $parameters): stdClass $this->extractSoapClientTrace($soap), ['exception' => ($exception instanceof JsonSerializable) ? $exception : print_r($exception, true)] ), - JSON_PRETTY_PRINT + JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES ))); throw new RuntimeException(sprintf('Fail soap call to %s', $methodName), 0, $exception); } @@ -77,16 +77,20 @@ public function call(string $methodName, array $parameters): stdClass /** * @param SoapClient $soapClient - * @return array + * @return array> * @noinspection PhpUsageOfSilenceOperatorInspection */ protected function extractSoapClientTrace(SoapClient $soapClient): array { return [ - 'request.headers' => (string) @$soapClient->__getLastRequestHeaders(), - 'request.body' => (string) @$soapClient->__getLastRequest(), - 'response.headers' => (string) @$soapClient->__getLastResponseHeaders(), - 'response.body' => (string) @$soapClient->__getLastResponse(), + 'request' => [ + 'headers' => (string) @$soapClient->__getLastRequestHeaders(), + 'body' => (string) @$soapClient->__getLastRequest(), + ], + 'response' => [ + 'headers' => (string) @$soapClient->__getLastResponseHeaders(), + 'body' => (string) @$soapClient->__getLastResponse(), + ], ]; } diff --git a/tests/TestCase.php b/tests/TestCase.php index 5d223c6..a1baeec 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -8,6 +8,8 @@ use PhpCfdi\Credentials\Credential; use PhpCfdi\Finkok\FinkokEnvironment; use PhpCfdi\Finkok\FinkokSettings; +use PhpCfdi\Finkok\Helpers\FileLogger; +use PhpCfdi\Finkok\Helpers\JsonDecoderLogger; use PhpCfdi\Finkok\SoapFactory; abstract class TestCase extends \PHPUnit\Framework\TestCase @@ -31,7 +33,8 @@ public function createSettingsFromEnvironment(SoapFactory $soapFactory = null): $this->getName(), uniqid() ); - $settings->soapFactory()->setLogger(new LoggerPrinter($loggerOutputFile)); + $logger = new JsonDecoderLogger(new FileLogger($loggerOutputFile)); + $settings->soapFactory()->setLogger($logger); } return $settings; } diff --git a/tests/Unit/Helpers/JsonDecoderLoggerTest.php b/tests/Unit/Helpers/JsonDecoderLoggerTest.php new file mode 100644 index 0000000..0c8f631 --- /dev/null +++ b/tests/Unit/Helpers/JsonDecoderLoggerTest.php @@ -0,0 +1,111 @@ +assertSame(false, $decoder->setAlsoLogJsonMessage(null)); + $this->assertSame(false, $decoder->setAlsoLogJsonMessage(true)); + $this->assertSame(true, $decoder->setAlsoLogJsonMessage(true)); + $this->assertSame(true, $decoder->setAlsoLogJsonMessage(null)); + $this->assertSame(true, $decoder->setAlsoLogJsonMessage(false)); + $this->assertSame(false, $decoder->setAlsoLogJsonMessage(false)); + $this->assertSame(false, $decoder->setAlsoLogJsonMessage(null)); + } + + public function testSetUseJsonValidateIfAvailable(): void + { + $decoder = new JsonDecoderLogger(new NullLogger()); + $this->assertSame(true, $decoder->setUseJsonValidateIfAvailable(null)); + $this->assertSame(true, $decoder->setUseJsonValidateIfAvailable(false)); + $this->assertSame(false, $decoder->setUseJsonValidateIfAvailable(false)); + $this->assertSame(false, $decoder->setUseJsonValidateIfAvailable(null)); + $this->assertSame(false, $decoder->setUseJsonValidateIfAvailable(true)); + $this->assertSame(true, $decoder->setUseJsonValidateIfAvailable(true)); + $this->assertSame(true, $decoder->setUseJsonValidateIfAvailable(null)); + } + + public function testLastMessageWasJsonValidReturnFalseWithoutCall(): void + { + $decoder = new JsonDecoderLogger(new NullLogger()); + $this->assertSame(false, $decoder->lastMessageWasJsonValid()); + } + + /** @return array */ + public function providerUseJsonValidateIfAvailable(): array + { + return [ + 'use json_validate' => [true], + 'do not use json_validate' => [false], + ]; + } + + /** @dataProvider providerUseJsonValidateIfAvailable */ + public function testLogSendValidJsonMessageToLogger(bool $useJsonValidateIfAvailable): void + { + /** @var string $jsonMessage */ + $jsonMessage = json_encode(['foo' => 'bar']); + $textMessage = print_r(json_decode($jsonMessage), true); + /** @var NullLogger&MockObject $logger */ + $logger = $this->createMock(NullLogger::class); + $logger->expects($this->once())->method('log')->with('debug', $textMessage, []); + + $decoder = new JsonDecoderLogger($logger); + $decoder->setUseJsonValidateIfAvailable($useJsonValidateIfAvailable); + $decoder->debug($jsonMessage); + $this->assertTrue($decoder->lastMessageWasJsonValid()); + } + + /** @dataProvider providerUseJsonValidateIfAvailable */ + public function testLogSendInvalidJsonMessageToLogger(bool $useJsonValidateIfAvailable): void + { + $invalidJsonMessage = 'this is not a valid json message'; + $expectedMessage = $invalidJsonMessage; + /** @var NullLogger&MockObject $logger */ + $logger = $this->createMock(NullLogger::class); + $logger->expects($this->once())->method('log')->with('error', $expectedMessage, []); + + $decoder = new JsonDecoderLogger($logger); + $decoder->setUseJsonValidateIfAvailable($useJsonValidateIfAvailable); + $decoder->error($invalidJsonMessage); + $this->assertFalse($decoder->lastMessageWasJsonValid()); + } + + public function testLogSendTextMessageToLoggerAndJson(): void + { + /** @var string $jsonMessage */ + $jsonMessage = json_encode(['foo' => 'bar']); + $textMessage = print_r(json_decode($jsonMessage), true); + /** @var NullLogger&MockObject $logger */ + $logger = $this->createMock(NullLogger::class); + $expectedParameters = [ + $textMessage, + $jsonMessage, + ]; + $matcher = $this->exactly(count($expectedParameters)); + $logger->expects($matcher)->method('log')->with( + 'debug', + $this->callback( + function ($message) use ($matcher, $expectedParameters) { + $this->assertSame($expectedParameters[$matcher->getInvocationCount() - 1], $message); + return true; + } + ), + [] + ); + + $decoder = new JsonDecoderLogger($logger); + $decoder->setAlsoLogJsonMessage(true); + $decoder->debug($jsonMessage); + } +} diff --git a/tests/stamp-precfdi-devenv.php b/tests/stamp-precfdi-devenv.php index 91ccdf2..49a13f1 100644 --- a/tests/stamp-precfdi-devenv.php +++ b/tests/stamp-precfdi-devenv.php @@ -8,6 +8,8 @@ use Exception; use PhpCfdi\Finkok\FinkokEnvironment; use PhpCfdi\Finkok\FinkokSettings; +use PhpCfdi\Finkok\Helpers\FileLogger; +use PhpCfdi\Finkok\Helpers\JsonDecoderLogger; use PhpCfdi\Finkok\QuickFinkok; use Throwable; @@ -44,13 +46,13 @@ public function __invoke(string $preCfdiPath): int FinkokEnvironment::makeDevelopment() ); if ($debug) { - $settings->soapFactory()->setLogger(new LoggerPrinter()); + $settings->soapFactory()->setLogger(new JsonDecoderLogger(new FileLogger())); } $quickFinkok = new QuickFinkok($settings); $stamp = $quickFinkok->stamp($preCfdiContents); - echo 'WS-Response: ', json_encode($stamp->rawData(), JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE), PHP_EOL; + echo 'WS-Response: ', json_encode($stamp->rawData(), JSON_PRETTY_PRINT), PHP_EOL; if ('' === $stamp->uuid()) { throw new Exception("Stamp on $preCfdiPath did not return an UUID");