Skip to content

Commit 6cb011a

Browse files
[FEATURE] Refinery: Add Decode group and Json transformation
This commit suggests adding a `Decode` group and a `Json` transformation.
1 parent 687da96 commit 6cb011a

7 files changed

Lines changed: 481 additions & 0 deletions

File tree

components/ILIAS/Refinery/README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ processed by the ILIAS project.
2020
* [htmlAttributeValue](#htmlAttributeValue)
2121
* [json](#json)
2222
* [url](#url)
23+
- [decode](#decode)
24+
* [json](#json-1)
2325
* [Custom Transformation](#custom-transformation)
2426
+ [DeriveApplyToFromTransform](#deriveapplytofromtransform)
2527
- [Error Handling](#error-handling)
@@ -329,6 +331,32 @@ The transformation prevents a value to change other URL parameters & values or t
329331
$link = $ctrl->setParameterByClass(FooGUI::class, 'bar', $refinery->encode()->url()->transform($foobar));
330332
```
331333

334+
##### decode
335+
336+
The `decode` group is the counterpart of the [encode](#encode) group and turns encoded strings back
337+
into native PHP values.
338+
339+
###### json
340+
341+
This transformation is a wrapper around `json_decode`. In contrast to `json_decode` it never returns
342+
`null` to signal a problem, because `null` cannot be told apart from the successfully decoded JSON
343+
literal `null`. Undecodable input raises a `ConstraintViolationException` instead, just like the
344+
other transformations of this library, so `applyTo` can be used to reify the problem into a
345+
`Result`.
346+
347+
JSON objects are decoded into associative arrays rather than `stdClass`, so the result can be
348+
processed further with the `container`, `to` and `kindlyTo` groups.
349+
350+
```php
351+
$settings = $refinery->decode()->json()->transform('{"limit":10,"tags":["a","b"]}');
352+
// $settings => ['limit' => 10, 'tags' => ['a', 'b']]
353+
354+
$refinery->decode()->json()->transform('{'); // throws a ConstraintViolationException
355+
356+
$result = $refinery->decode()->json()->applyTo(new ILIAS\Data\Result\Ok('{'));
357+
// $result->isError() => true
358+
```
359+
332360
##### Custom
333361

334362
The `Custom` group contains `Transformations` and `Constraints`
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*
17+
*********************************************************************/
18+
19+
declare(strict_types=1);
20+
21+
namespace ILIAS\Refinery\Decode;
22+
23+
use ILIAS\Refinery\Decode\Transformation\Json;
24+
use ILIAS\Refinery\Transformation;
25+
26+
final class Group
27+
{
28+
/**
29+
* Decodes a JSON string into native PHP values, JSON objects become associative arrays.
30+
*
31+
* @param int $max_depth Maximum nesting depth of the structure being decoded, counting the
32+
* scalars at the very bottom as one level. Defaults to the depth PHP
33+
* itself uses for `json_decode`.
34+
*/
35+
public function json(int $max_depth = Json::DEFAULT_MAX_DEPTH): Transformation
36+
{
37+
return new Json($max_depth);
38+
}
39+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*
17+
*********************************************************************/
18+
19+
declare(strict_types=1);
20+
21+
namespace ILIAS\Refinery\Decode\Transformation;
22+
23+
use ILIAS\Refinery\ConstraintViolationException;
24+
use ILIAS\Refinery\DeriveApplyToFromTransform;
25+
use ILIAS\Refinery\DeriveInvokeFromTransform;
26+
use ILIAS\Refinery\Transformation;
27+
use InvalidArgumentException;
28+
use JsonException;
29+
30+
/**
31+
* This class is a wrapper around `json_decode` which reports undecodable input as a violation
32+
* instead of returning `null`, which cannot be told apart from the successfully decoded JSON
33+
* literal `null`.
34+
*
35+
* JSON objects are decoded into associative arrays instead of `stdClass`, so that results can be
36+
* processed further with the `container`, `to` and `kindlyTo` groups and round-trip with the
37+
* `encode` group.
38+
*
39+
* Please see https://www.php.net/manual/en/function.json-decode.php for more information.
40+
*/
41+
final class Json implements Transformation
42+
{
43+
use DeriveInvokeFromTransform;
44+
use DeriveApplyToFromTransform;
45+
46+
/**
47+
* PHP does not expose the default of its JSON parser (PHP_JSON_PARSER_DEFAULT_DEPTH) to userland.
48+
*/
49+
public const int DEFAULT_MAX_DEPTH = 512;
50+
public const int MAX_DEPTH_LOWER_BOUND = 1;
51+
public const int MAX_DEPTH_UPPER_BOUND = 2147483647;
52+
53+
public function __construct(private readonly int $max_depth = self::DEFAULT_MAX_DEPTH)
54+
{
55+
if ($max_depth < self::MAX_DEPTH_LOWER_BOUND || $max_depth > self::MAX_DEPTH_UPPER_BOUND) {
56+
throw new InvalidArgumentException(
57+
\sprintf(
58+
'Maximum depth must be between %d and %d, got %d.',
59+
self::MAX_DEPTH_LOWER_BOUND,
60+
self::MAX_DEPTH_UPPER_BOUND,
61+
$max_depth
62+
)
63+
);
64+
}
65+
}
66+
67+
public function transform($from): mixed
68+
{
69+
if (!\is_string($from)) {
70+
throw new ConstraintViolationException(
71+
\sprintf('The value of type "%s" is not a string and cannot be decoded as JSON.', get_debug_type($from)),
72+
'not_a_string',
73+
get_debug_type($from)
74+
);
75+
}
76+
77+
try {
78+
return json_decode($from, true, $this->max_depth, JSON_THROW_ON_ERROR);
79+
} catch (JsonException $exception) {
80+
// The value itself is left out of the message, it may be large and carry sensitive data.
81+
throw new ConstraintViolationException(
82+
\sprintf('The value cannot be decoded as JSON: %s.', $exception->getMessage()),
83+
'not_json',
84+
$exception->getMessage()
85+
);
86+
}
87+
}
88+
}

components/ILIAS/Refinery/src/Factory.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,11 @@ public function encode(): Encode\Group
161161
return new Encode\Group();
162162
}
163163

164+
public function decode(): Decode\Group
165+
{
166+
return new Decode\Group();
167+
}
168+
164169
/**
165170
* Accepts Transformations and uses first successful one.
166171
* @param Transformation[] $transformations
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<?php
2+
3+
/**
4+
* This file is part of ILIAS, a powerful learning management system
5+
* published by ILIAS open source e-Learning e.V.
6+
*
7+
* ILIAS is licensed with the GPL-3.0,
8+
* see https://www.gnu.org/licenses/gpl-3.0.en.html
9+
* You should have received a copy of said license along with the
10+
* source code, too.
11+
*
12+
* If this is not the case or you just want to try ILIAS, you'll find
13+
* us at:
14+
* https://www.ilias.de
15+
* https://github.com/ILIAS-eLearning
16+
*
17+
*********************************************************************/
18+
19+
declare(strict_types=1);
20+
21+
namespace ILIAS\Tests\Refinery\Decode;
22+
23+
use ILIAS\Refinery\Decode\Group;
24+
use ILIAS\Refinery\Decode\Transformation\Json;
25+
use PHPUnit\Framework\TestCase;
26+
27+
class GroupTest extends TestCase
28+
{
29+
public function testConstruct(): void
30+
{
31+
self::assertInstanceOf(Group::class, new Group());
32+
}
33+
34+
public function testJson(): void
35+
{
36+
$group = new Group();
37+
38+
self::assertInstanceOf(Json::class, $group->json());
39+
}
40+
}

0 commit comments

Comments
 (0)