Skip to content

Commit 07557e0

Browse files
mjansenDatabaylscharmer
authored andcommitted
[FEATURE] Refinery: Add Decode group and Json transformation
This commit suggests adding a `Decode` group and a `Json` transformation.
1 parent 0b69130 commit 07557e0

7 files changed

Lines changed: 477 additions & 0 deletions

File tree

components/ILIAS/Refinery/README.md

Lines changed: 27 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,31 @@ 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 an `InvalidArgumentException` instead, as announced by the
344+
`Transformation` interface, so `applyTo` can be used to reify the problem into a `Result`.
345+
346+
JSON objects are decoded into associative arrays rather than `stdClass`, so the result can be
347+
processed further with the `container`, `to` and `kindlyTo` groups.
348+
349+
```php
350+
$settings = $refinery->decode()->json()->transform('{"limit":10,"tags":["a","b"]}');
351+
// $settings => ['limit' => 10, 'tags' => ['a', 'b']]
352+
353+
$refinery->decode()->json()->transform('{'); // throws an InvalidArgumentException
354+
355+
$result = $refinery->decode()->json()->applyTo(new ILIAS\Data\Result\Ok('{'));
356+
// $result->isError() => true
357+
```
358+
332359
##### Custom
333360

334361
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: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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\DeriveApplyToFromTransform;
24+
use ILIAS\Refinery\DeriveInvokeFromTransform;
25+
use ILIAS\Refinery\Transformation;
26+
use InvalidArgumentException;
27+
use JsonException;
28+
29+
/**
30+
* This class is a wrapper around `json_decode` which rejects undecodable input with an
31+
* `InvalidArgumentException` instead of returning `null`, which cannot be told apart from the
32+
* successfully decoded JSON literal `null`.
33+
*
34+
* JSON objects are decoded into associative arrays instead of `stdClass`, so that results can be
35+
* processed further with the `container`, `to` and `kindlyTo` groups and round-trip with the
36+
* `encode` group.
37+
*
38+
* Please see https://www.php.net/manual/en/function.json-decode.php for more information.
39+
*/
40+
final class Json implements Transformation
41+
{
42+
use DeriveInvokeFromTransform;
43+
use DeriveApplyToFromTransform;
44+
45+
/**
46+
* PHP does not expose the default of its JSON parser (PHP_JSON_PARSER_DEFAULT_DEPTH) to userland.
47+
*/
48+
public const int DEFAULT_MAX_DEPTH = 512;
49+
public const int MAX_DEPTH_LOWER_BOUND = 1;
50+
public const int MAX_DEPTH_UPPER_BOUND = 2147483647;
51+
52+
public function __construct(private readonly int $max_depth = self::DEFAULT_MAX_DEPTH)
53+
{
54+
if ($max_depth < self::MAX_DEPTH_LOWER_BOUND || $max_depth > self::MAX_DEPTH_UPPER_BOUND) {
55+
throw new InvalidArgumentException(
56+
\sprintf(
57+
'Maximum depth must be between %d and %d, got %d.',
58+
self::MAX_DEPTH_LOWER_BOUND,
59+
self::MAX_DEPTH_UPPER_BOUND,
60+
$max_depth
61+
)
62+
);
63+
}
64+
}
65+
66+
public function transform($from): mixed
67+
{
68+
if (!\is_string($from)) {
69+
throw new InvalidArgumentException(
70+
\sprintf(
71+
'The value of type "%s" is not a string and cannot be decoded as JSON.',
72+
get_debug_type($from)
73+
)
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 InvalidArgumentException(
82+
\sprintf('The value cannot be decoded as JSON: %s.', $exception->getMessage())
83+
);
84+
}
85+
}
86+
}

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)