From d55a6767a5f7163e7bb75f3bfd6f19c5a9b1d63f Mon Sep 17 00:00:00 2001 From: George Steel Date: Mon, 31 Jan 2022 20:36:44 +0000 Subject: [PATCH] Remove dependency on laminas-json - Removes deprecated JSON Expr finder compatibility in view helper and related deprecation errors - Removes composer dependency on `laminas-json` - Removes outdated documentation for Json Epr finder - Replaces usage of `Laminas\Json` with calls to native `json_encode` - Update psalm baseline Signed-off-by: George Steel --- composer.json | 1 - composer.lock | 63 +---------------------------------- docs/book/helpers/json.md | 26 --------------- psalm-baseline.xml | 14 -------- src/Helper/Json.php | 31 +++++++++-------- src/Model/JsonModel.php | 26 +++++++++++---- src/Renderer/JsonRenderer.php | 36 ++++++++++++++------ test/Helper/JsonTest.php | 28 ++++++---------- test/Model/JsonModelTest.php | 33 ++++++++++++++---- 9 files changed, 101 insertions(+), 157 deletions(-) diff --git a/composer.json b/composer.json index bdcbbaac8..ef9761530 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,6 @@ "php": "^7.4 || ~8.0.0 || ~8.1.0", "ext-json": "*", "laminas/laminas-eventmanager": "^3.4", - "laminas/laminas-json": "^3.3", "laminas/laminas-stdlib": "^3.6" }, "require-dev": { diff --git a/composer.lock b/composer.lock index 90de23f62..741948c82 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a003c15d0428178d28dd954a18766ec9", + "content-hash": "87da10378580603652a6399ab978b392", "packages": [ { "name": "laminas/laminas-eventmanager", @@ -72,67 +72,6 @@ ], "time": "2021-09-07T22:35:32+00:00" }, - { - "name": "laminas/laminas-json", - "version": "3.3.0", - "source": { - "type": "git", - "url": "https://github.com/laminas/laminas-json.git", - "reference": "9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laminas/laminas-json/zipball/9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f", - "reference": "9a0ce9f330b7d11e70c4acb44d67e8c4f03f437f", - "shasum": "" - }, - "require": { - "php": "^7.3 || ~8.0.0 || ~8.1.0" - }, - "conflict": { - "zendframework/zend-json": "*" - }, - "require-dev": { - "laminas/laminas-coding-standard": "~2.2.1", - "laminas/laminas-stdlib": "^2.7.7 || ^3.1", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "laminas/laminas-json-server": "For implementing JSON-RPC servers", - "laminas/laminas-xml2json": "For converting XML documents to JSON" - }, - "type": "library", - "autoload": { - "psr-4": { - "Laminas\\Json\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "description": "provides convenience methods for serializing native PHP to JSON and decoding JSON to native PHP", - "homepage": "https://laminas.dev", - "keywords": [ - "json", - "laminas" - ], - "support": { - "chat": "https://laminas.dev/chat", - "docs": "https://docs.laminas.dev/laminas-json/", - "forum": "https://discourse.laminas.dev", - "issues": "https://github.com/laminas/laminas-json/issues", - "rss": "https://github.com/laminas/laminas-json/releases.atom", - "source": "https://github.com/laminas/laminas-json" - }, - "funding": [ - { - "url": "https://funding.communitybridge.org/projects/laminas-project", - "type": "community_bridge" - } - ], - "time": "2021-09-02T18:02:31+00:00" - }, { "name": "laminas/laminas-stdlib", "version": "3.7.0", diff --git a/docs/book/helpers/json.md b/docs/book/helpers/json.md index 4a4056a02..655a9f788 100644 --- a/docs/book/helpers/json.md +++ b/docs/book/helpers/json.md @@ -19,29 +19,3 @@ determine how to handle the content. ```php json($this->data) ?> ``` - -> WARNING: **Deprecated** -> -> ### Enabling encoding using Laminas\Json\Expr -> -> **This feature of the Json view helper has been deprecated in version 2.16 and will be removed in version 3.0.** -> -> The JSON helper accepts an array of options that will be passed to `Laminas\Json\Json::encode()` and -> used internally to encode data. -> `Laminas\Json\Json::encode` allows the encoding of native JSON expressions using `Laminas\Json\Expr` -> objects. This option is disabled by default. To enable this option, pass a boolean `true` to the -> `enableJsonExprFinder` key of the options array: -> -> ```php -> json($this->data, ['enableJsonExprFinder' => true]) ?> -> `` -> -> The JSON helper accepts an array of options that will be passed to `Laminas\Json\Json::encode()` and -> used internally to encode data. -> `Laminas\Json\Json::encode` allows the encoding of native JSON expressions using `Laminas\Json\Expr` -> objects. This option is disabled by default. To enable this option, pass a boolean `true` to the -> `enableJsonExprFinder` key of the options array: -> -> ```php -> json($this->data, ['enableJsonExprFinder' => true]) ?> -> ``` diff --git a/psalm-baseline.xml b/psalm-baseline.xml index f7cb8e2e0..7e47f63f9 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -760,17 +760,6 @@ InlineScript - - - $response - - - null - - - $this->response instanceof Response - - null === $this->viewModelHelper @@ -1915,9 +1904,6 @@ - - void - $jsonpCallback diff --git a/src/Helper/Json.php b/src/Helper/Json.php index fa1c85b14..bc50abab5 100644 --- a/src/Helper/Json.php +++ b/src/Helper/Json.php @@ -5,37 +5,30 @@ namespace Laminas\View\Helper; use Laminas\Http\Response; -use Laminas\Json\Json as JsonFormatter; -use function trigger_error; +use function json_encode; -use const E_USER_DEPRECATED; +use const JSON_PRETTY_PRINT; +use const JSON_THROW_ON_ERROR; /** * Helper for simplifying JSON responses */ class Json extends AbstractHelper { - /** @var Response */ + /** @var Response|null */ protected $response; /** * Encode data as JSON and set response header * * @param mixed $data - * @param array $jsonOptions Options to pass to JsonFormatter::encode() - * @return string|void + * @param array{prettyPrint?: bool} $jsonOptions + * @return string */ public function __invoke($data, array $jsonOptions = []) { - if (isset($jsonOptions['enableJsonExprFinder']) && $jsonOptions['enableJsonExprFinder'] === true) { - trigger_error( - 'Json Expression functionality is deprecated and will be removed in laminas-view 3.0', - E_USER_DEPRECATED - ); - } - - $data = JsonFormatter::encode($data, null, $jsonOptions); + $data = json_encode($data, $this->optionsToFlags($jsonOptions)); if ($this->response instanceof Response) { $headers = $this->response->getHeaders(); @@ -45,6 +38,16 @@ public function __invoke($data, array $jsonOptions = []) return $data; } + /** @param array{prettyPrint?: bool} $options */ + private function optionsToFlags(array $options = []): int + { + $prettyPrint = $options['prettyPrint'] ?? false; + $flags = JSON_THROW_ON_ERROR; + $flags |= $prettyPrint ? 0 : JSON_PRETTY_PRINT; + + return $flags; + } + /** * Set the response object * diff --git a/src/Model/JsonModel.php b/src/Model/JsonModel.php index 186942003..2d4c9b761 100644 --- a/src/Model/JsonModel.php +++ b/src/Model/JsonModel.php @@ -4,10 +4,16 @@ namespace Laminas\View\Model; -use Laminas\Json\Json; +use JsonException; use Laminas\Stdlib\ArrayUtils; +use Laminas\View\Exception\DomainException; use Traversable; +use function json_encode; + +use const JSON_PRETTY_PRINT; +use const JSON_THROW_ON_ERROR; + class JsonModel extends ViewModel { /** @@ -56,13 +62,21 @@ public function serialize() $variables = ArrayUtils::iteratorToArray($variables); } - $options = [ - 'prettyPrint' => $this->getOption('prettyPrint'), - ]; + $options = (bool) $this->getOption('prettyPrint', false) ? JSON_PRETTY_PRINT : 0; if (null !== $this->jsonpCallback) { - return $this->jsonpCallback . '(' . Json::encode($variables, false, $options) . ');'; + return $this->jsonpCallback . '(' . $this->jsonEncode($variables, $options) . ');'; + } + return $this->jsonEncode($variables, $options); + } + + /** @param mixed $data */ + private function jsonEncode($data, int $options): string + { + try { + return json_encode($data, $options | JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new DomainException('Failed to encode Json', (int) $e->getCode(), $e); } - return Json::encode($variables, false, $options); } } diff --git a/src/Renderer/JsonRenderer.php b/src/Renderer/JsonRenderer.php index ecc9b115f..c296cd2bc 100644 --- a/src/Renderer/JsonRenderer.php +++ b/src/Renderer/JsonRenderer.php @@ -5,8 +5,8 @@ namespace Laminas\View\Renderer; use ArrayAccess; +use JsonException; use JsonSerializable; -use Laminas\Json\Json; use Laminas\Stdlib\ArrayUtils; use Laminas\View\Exception; use Laminas\View\Model\JsonModel; @@ -18,8 +18,11 @@ use function array_replace_recursive; use function get_object_vars; use function is_object; +use function json_encode; use function sprintf; +use const JSON_THROW_ON_ERROR; + /** * JSON renderer */ @@ -60,11 +63,13 @@ public function getEngine() * Set the resolver used to map a template name to a resource the renderer may consume. * * @todo Determine use case for resolvers when rendering JSON - * @return void + * @return self */ public function setResolver(Resolver $resolver) { $this->resolver = $resolver; + + return $this; } /** @@ -131,28 +136,27 @@ public function render($nameOrModel, $values = null) if ($nameOrModel instanceof JsonModel) { $children = $this->recurseModel($nameOrModel, false); $this->injectChildren($nameOrModel, $children); - $values = $nameOrModel->serialize(); + $output = $nameOrModel->serialize(); } else { - $values = $this->recurseModel($nameOrModel); - $values = Json::encode($values); + $output = $this->recurseModel($nameOrModel); + $output = $this->jsonEncode($output); } if ($this->hasJsonpCallback()) { - $values = $this->jsonpCallback . '(' . $values . ');'; + $output = $this->jsonpCallback . '(' . $output . ');'; } - return $values; + return $output; } // use case 2: $nameOrModel is populated, $values is not // Serialize $nameOrModel if (null === $values) { if (! is_object($nameOrModel) || $nameOrModel instanceof JsonSerializable) { - $return = Json::encode($nameOrModel); + $return = $this->jsonEncode($nameOrModel); } elseif ($nameOrModel instanceof Traversable) { - $nameOrModel = ArrayUtils::iteratorToArray($nameOrModel); - $return = Json::encode($nameOrModel); + $return = $this->jsonEncode(ArrayUtils::iteratorToArray($nameOrModel)); } else { - $return = Json::encode(get_object_vars($nameOrModel)); + $return = $this->jsonEncode(get_object_vars($nameOrModel)); } if ($this->hasJsonpCallback()) { @@ -237,4 +241,14 @@ protected function injectChildren(Model $model, array $children): void $model->setVariable($child, $value); } } + + /** @param mixed $data */ + private function jsonEncode($data): string + { + try { + return json_encode($data, JSON_THROW_ON_ERROR); + } catch (JsonException $e) { + throw new Exception\DomainException('Json encoding failed', (int) $e->getCode(), $e); + } + } } diff --git a/test/Helper/JsonTest.php b/test/Helper/JsonTest.php index b3308d9f2..f25ec8752 100644 --- a/test/Helper/JsonTest.php +++ b/test/Helper/JsonTest.php @@ -6,10 +6,13 @@ use Laminas\Http\Header\HeaderInterface; use Laminas\Http\Response; -use Laminas\Json\Json as JsonFormatter; use Laminas\View\Helper\Json as JsonHelper; use PHPUnit\Framework\TestCase; +use function json_encode; + +use const JSON_THROW_ON_ERROR; + /** * Test class for Laminas\View\Helper\Json * @@ -34,7 +37,7 @@ protected function setUp(): void $this->helper->setResponse($this->response); } - public function verifyJsonHeader(): void + private function verifyJsonHeader(): void { $headers = $this->response->getHeaders(); $this->assertTrue($headers->has('Content-Type')); @@ -51,20 +54,11 @@ public function testJsonHelperSetsResponseHeader(): void public function testJsonHelperReturnsJsonEncodedString(): void { - $data = $this->helper->__invoke('foobar'); - $this->assertIsString($data); - $this->assertEquals('foobar', JsonFormatter::decode($data)); - } - - public function testThatADeprecationErrorIsTriggeredWhenExpressionFinderOptionIsUsed(): void - { - $this->expectDeprecation(); - $this->helper->__invoke(['foo'], ['enableJsonExprFinder' => true]); - } - - public function testThatADeprecationErrorIsNotTriggeredWhenExpressionFinderOptionIsNotUsed(): void - { - $this->expectNotToPerformAssertions(); - $this->helper->__invoke(['foo'], ['enableJsonExprFinder' => 'anything other than true']); + $input = [ + 'dory' => 'blue', + 'nemo' => 'orange', + ]; + $expect = json_encode($input, JSON_THROW_ON_ERROR); + self::assertJsonStringEqualsJsonString($expect, ($this->helper)($input)); } } diff --git a/test/Model/JsonModelTest.php b/test/Model/JsonModelTest.php index 4319b799a..41b7e64ef 100644 --- a/test/Model/JsonModelTest.php +++ b/test/Model/JsonModelTest.php @@ -4,11 +4,17 @@ namespace LaminasTest\View\Model; -use Laminas\Json\Json; +use Laminas\View\Exception\DomainException; use Laminas\View\Model\JsonModel; use Laminas\View\Variables; use PHPUnit\Framework\TestCase; +use function json_encode; +use function sprintf; + +use const JSON_PRETTY_PRINT; +use const JSON_THROW_ON_ERROR; + class JsonModelTest extends TestCase { public function testAllowsEmptyConstructor(): void @@ -23,7 +29,7 @@ public function testCanSerializeVariablesToJson(): void $array = ['foo' => 'bar']; $model = new JsonModel($array); $this->assertEquals($array, $model->getVariables()); - $this->assertEquals(Json::encode($array), $model->serialize()); + $this->assertJsonStringEqualsJsonString(json_encode($array, JSON_THROW_ON_ERROR), $model->serialize()); } public function testCanSerializeWithJsonpCallback(): void @@ -31,12 +37,16 @@ public function testCanSerializeWithJsonpCallback(): void $array = ['foo' => 'bar']; $model = new JsonModel($array); $model->setJsonpCallback('callback'); - $this->assertEquals('callback(' . Json::encode($array) . ');', $model->serialize()); + $expect = sprintf( + 'callback(%s);', + json_encode($array, JSON_THROW_ON_ERROR) + ); + $this->assertEquals($expect, $model->serialize()); } public function testPrettyPrint(): void { - $array = [ + $array = [ 'simple' => 'simple test string', 'stringwithjsonchars' => '\"[1,2]', 'complex' => [ @@ -44,7 +54,18 @@ public function testPrettyPrint(): void 'far' => 'boo', ], ]; - $model = new JsonModel($array, ['prettyPrint' => true]); - $this->assertEquals(Json::encode($array, false, ['prettyPrint' => true]), $model->serialize()); + $model = new JsonModel($array, ['prettyPrint' => true]); + $expect = json_encode($array, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); + $this->assertEquals($expect, $model->serialize()); + } + + public function testThatAnExceptionIsThrownIfItIsNotPossibleToEncodeThePayload(): void + { + $malformedUtf8 = [ + 'string' => "\x92", + ]; + $this->expectException(DomainException::class); + $this->expectExceptionMessage('Failed to encode Json'); + (new JsonModel($malformedUtf8))->serialize(); } }