Skip to content

Commit

Permalink
Modernize Encoders
Browse files Browse the repository at this point in the history
  • Loading branch information
bzikarsky committed Apr 5, 2022
1 parent 5237435 commit 89c6906
Show file tree
Hide file tree
Showing 6 changed files with 37 additions and 183 deletions.
16 changes: 4 additions & 12 deletions src/Gelf/Encoder/CompressedJsonEncoder.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);

/*
* This file is part of the php-gelf package.
Expand All @@ -23,25 +24,16 @@ class CompressedJsonEncoder implements EncoderInterface
{
const DEFAULT_COMPRESSION_LEVEL = -1;

/**
* @var int
*/
protected $compressionLevel;

/** @var JsonEncoder */
private $jsonEncoder;
private JsonEncoder $jsonEncoder;

/**
* Class constructor
*
* Allows the specification of the gzip compression-level
*
* @param int $compressionLevel
*/
public function __construct(
$compressionLevel = self::DEFAULT_COMPRESSION_LEVEL
private int $compressionLevel = self::DEFAULT_COMPRESSION_LEVEL
) {
$this->compressionLevel = $compressionLevel;
$this->jsonEncoder = new JsonEncoder();
}

Expand All @@ -51,7 +43,7 @@ public function __construct(
* @param MessageInterface $message
* @return string
*/
public function encode(MessageInterface $message)
public function encode(MessageInterface $message): string
{
$json = $this->jsonEncoder->encode($message);

Expand Down
6 changes: 2 additions & 4 deletions src/Gelf/Encoder/EncoderInterface.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);

/*
* This file is part of the php-gelf package.
Expand All @@ -25,9 +26,6 @@ interface EncoderInterface

/**
* Encodes a given message
*
* @param MessageInterface $message
* @return mixed
*/
public function encode(MessageInterface $message);
public function encode(MessageInterface $message): string;
}
132 changes: 6 additions & 126 deletions src/Gelf/Encoder/JsonEncoder.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);

/*
* This file is part of the php-gelf package.
Expand All @@ -21,137 +22,16 @@
*/
class JsonEncoder implements NoNullByteEncoderInterface
{

/**
* Encodes a given message
*
* @param MessageInterface $message
* @return string
*/
public function encode(MessageInterface $message)
{
return $this->toJson($message->toArray());
}

/**
* Return the JSON representation of a value
*
* @param mixed $data
* @return string
* @throws \RuntimeException if encoding fails and errors are not ignored
* @inheritDoc
*/
protected function toJson($data)
public function encode(MessageInterface $message): string
{
$json = $this->jsonEncode($data);
if ($json === false) {
$json = $this->handleJsonError(json_last_error(), $data);
}
return $json;
return $this->jsonEncode($message->toArray());
}

/**
* @param mixed $data
* @return string JSON encoded data or null on failure
*/
private function jsonEncode($data)
{
if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
}
return json_encode($data);
}

/**
* Handle a json_encode failure.
*
* If the failure is due to invalid string encoding, try to clean the
* input and encode again. If the second encoding attempt fails, the
* inital error is not encoding related or the input can't be cleaned then
* raise a descriptive exception.
*
* @param int $code return code of json_last_error function
* @param mixed $data data that was meant to be encoded
* @return string JSON encoded data after error correction
* @throws \RuntimeException if failure can't be corrected
*/
private function handleJsonError($code, $data)
{
if ($code !== JSON_ERROR_UTF8) {
$this->throwEncodeError($code, $data);
}
if (is_string($data)) {
$this->detectAndCleanUtf8($data);
} elseif (is_array($data)) {
array_walk_recursive($data, array($this, 'detectAndCleanUtf8'));
} else {
$this->throwEncodeError($code, $data);
}
$json = $this->jsonEncode($data);
if ($json === false) {
$this->throwEncodeError(json_last_error(), $data);
}
return $json;
}

/**
* Throws an exception according to a given code with a customized message
*
* @param int $code return code of json_last_error function
* @param mixed $data data that was meant to be encoded
* @throws \RuntimeException
*/
private function throwEncodeError($code, $data)
{
switch ($code) {
case JSON_ERROR_DEPTH:
$msg = 'Maximum stack depth exceeded';
break;
case JSON_ERROR_STATE_MISMATCH:
$msg = 'Underflow or the modes mismatch';
break;
case JSON_ERROR_CTRL_CHAR:
$msg = 'Unexpected control character found';
break;
case JSON_ERROR_UTF8:
$msg = 'Malformed UTF-8 characters, possibly incorrectly encoded';
break;
default:
$msg = 'Unknown error';
}
throw new \RuntimeException('JSON encoding failed: ' . $msg . '. Encoding: ' . var_export($data, true));
}

/**
* Detect invalid UTF-8 string characters and convert to valid UTF-8.
*
* Valid UTF-8 input will be left unmodified, but strings containing
* invalid UTF-8 codepoints will be reencoded as UTF-8 with an assumed
* original encoding of ISO-8859-15. This conversion may result in
* incorrect output if the actual encoding was not ISO-8859-15, but it
* will be clean UTF-8 output and will not rely on expensive and fragile
* detection algorithms.
*
* Function converts the input in place in the passed variable so that it
* can be used as a callback for array_walk_recursive.
*
* @param mixed &$data Input to check and convert if needed
* @private
*/
public function detectAndCleanUtf8(&$data)
private function jsonEncode(mixed $data): string
{
if (is_string($data) && !preg_match('//u', $data)) {
$data = preg_replace_callback(
'/[\x80-\xFF]+/',
function ($m) {
return utf8_encode($m[0]);
},
$data
);
$data = str_replace(
array('¤', '¦', '¨', '´', '¸', '¼', '½', '¾'),
array('', 'Š', 'š', 'Ž', 'ž', 'Œ', 'œ', 'Ÿ'),
$data
);
}
return json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
}
}
1 change: 1 addition & 0 deletions src/Gelf/Encoder/NoNullByteEncoderInterface.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);

/*
* This file is part of the php-gelf package.
Expand Down
26 changes: 10 additions & 16 deletions tests/Gelf/Test/Encoder/CompressedJsonEncoderTest.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);

/*
* This file is part of the php-gelf package.
Expand All @@ -12,30 +13,25 @@
namespace Gelf\Test\Encoder;

use Gelf\Encoder\CompressedJsonEncoder;
use Gelf\Encoder\JsonEncoder;
use Gelf\MessageInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class CompressedJsonEncoderTest extends TestCase
{
private MockObject|MessageInterface $message;
private CompressedJsonEncoder $encoder;

/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $message;

/**
* @var CompressedJsonEncoder
*/
protected $encoder;

public function setUp()
public function setUp(): void
{
$this->message = $this->createMock('\\Gelf\\Message');
$this->message = $this->createMock(MessageInterface::class);
$this->encoder = new CompressedJsonEncoder();
}

public function testEncode()
{
$testData = array('foo' => 'bar');
$testData = ['foo' => 'bar'];

$this->message
->expects($this->once())
Expand All @@ -49,11 +45,9 @@ public function testEncode()

// check that it's uncompressable
$json = gzuncompress($bytes);
$this->assertInternalType('string', $json);

// check that there is JSON inside
$data = json_decode($json, $assoc = true);
$this->assertInternalType('array', $data);
$data = json_decode($json, associative: true);

// check that we have our data array
$this->assertEquals($testData, $data);
Expand Down
39 changes: 14 additions & 25 deletions tests/Gelf/Test/Encoder/JsonEncoderTest.php
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
<?php
declare(strict_types=1);

/*
* This file is part of the php-gelf package.
Expand All @@ -12,30 +13,24 @@
namespace Gelf\Test\Encoder;

use Gelf\Encoder\JsonEncoder;
use Gelf\MessageInterface;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

class JsonEncoderTest extends TestCase
{
private MockObject|MessageInterface $message;
private JsonEncoder $encoder;

/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $message;

/**
* @var CompressedJsonEncoder
*/
protected $encoder;

public function setUp()
public function setUp(): void
{
$this->message = $this->createMock('\\Gelf\\Message');
$this->message = $this->createMock(MessageInterface::class);
$this->encoder = new JsonEncoder();
}

public function testEncode()
public function testEncode(): void
{
$testData = array('foo' => 'bar');
$testData = ['foo' => 'bar'];

$this->message
->expects($this->once())
Expand All @@ -45,16 +40,15 @@ public function testEncode()
$json = $this->encoder->encode($this->message);

// check that there is JSON inside
$data = json_decode($json, $assoc = true);
$this->assertInternalType('array', $data);
$data = json_decode($json, associative: true);

// check that we have our data array
$this->assertEquals($testData, $data);
self::assertEquals($testData, $data);
}

public function testUnicodeEncode()
public function testUnicodeEncode(): void
{
$testData = array('foo' => 'бар');
$testData = ['foo' => 'бар'];

$this->message
->expects($this->once())
Expand All @@ -63,11 +57,6 @@ public function testUnicodeEncode()

$json = $this->encoder->encode($this->message);

if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
$this->assertEquals('{"foo":"бар"}', $json);
} else {
$this->assertEquals('{"foo":"\u0431\u0430\u0440"}', $json);
}

self::assertEquals('{"foo":"бар"}', $json);
}
}

0 comments on commit 89c6906

Please sign in to comment.