Skip to content

Commit 2476ecb

Browse files
Added casts AsCollection for hyperf/database. (#7607)
Co-authored-by: 李铭昕 <715557344@qq.com>
1 parent 24dff99 commit 2476ecb

File tree

3 files changed

+227
-0
lines changed

3 files changed

+227
-0
lines changed

src/Model/Casts/AsCollection.php

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
13+
namespace Hyperf\Database\Model\Casts;
14+
15+
use Hyperf\Collection\Collection;
16+
use Hyperf\Contract\CastsAttributes;
17+
use Hyperf\Stringable\Str;
18+
use InvalidArgumentException;
19+
20+
class AsCollection implements CastsAttributes
21+
{
22+
public function __construct(protected ?string $collectionClass = null, protected ?string $parseCallback = null)
23+
{
24+
}
25+
26+
public function get($model, string $key, $value, array $attributes)
27+
{
28+
if (! isset($attributes[$key])) {
29+
return null;
30+
}
31+
32+
$data = Json::decode($attributes[$key]);
33+
34+
$collectionClass = empty($this->collectionClass) ? Collection::class : $this->collectionClass;
35+
36+
if (! is_a($collectionClass, Collection::class, true)) {
37+
throw new InvalidArgumentException('The provided class must extend [' . Collection::class . '].');
38+
}
39+
40+
if (! is_array($data)) {
41+
return null;
42+
}
43+
44+
$instance = new $collectionClass($data);
45+
46+
if (empty($this->parseCallback)) {
47+
return $instance;
48+
}
49+
50+
$parseCallback = Str::parseCallback($this->parseCallback);
51+
if (is_callable($parseCallback)) {
52+
return $instance->map($parseCallback);
53+
}
54+
55+
return $instance->mapInto($parseCallback[0]);
56+
}
57+
58+
public function set($model, $key, $value, $attributes)
59+
{
60+
return [$key => Json::encode($value)];
61+
}
62+
63+
/**
64+
* Specify the type of object each item in the collection should be mapped to.
65+
*
66+
* @param array{class-string, string}|class-string $map
67+
* @return string
68+
*/
69+
public static function of($map)
70+
{
71+
return static::using('', $map);
72+
}
73+
74+
/**
75+
* Specify the collection type for the cast.
76+
*
77+
* @param class-string $class
78+
* @param array{class-string, string}|class-string $map
79+
* @return string
80+
*/
81+
public static function using($class, $map = null)
82+
{
83+
if (
84+
is_array($map)
85+
&& count($map) === 2
86+
&& is_string($map[0])
87+
&& is_string($map[1])
88+
&& is_callable($map)
89+
) {
90+
$map = $map[0] . '@' . $map[1];
91+
}
92+
93+
return static::class . ':' . implode(',', [$class, $map]);
94+
}
95+
}

src/Model/Casts/Json.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
/**
5+
* This file is part of Hyperf.
6+
*
7+
* @link https://www.hyperf.io
8+
* @document https://hyperf.wiki
9+
* @contact group@hyperf.io
10+
* @license https://github.com/hyperf/hyperf/blob/master/LICENSE
11+
*/
12+
13+
namespace Hyperf\Database\Model\Casts;
14+
15+
use JsonException;
16+
17+
class Json
18+
{
19+
/**
20+
* The custom JSON encoder.
21+
*
22+
* @var null|callable
23+
*/
24+
protected static $encoder;
25+
26+
/**
27+
* The custom JSON decoder.
28+
*
29+
* @var null|callable
30+
*/
31+
protected static $decoder;
32+
33+
/**
34+
* Encode the given value.
35+
*/
36+
public static function encode(mixed $value, int $flags = 0): false|string
37+
{
38+
return isset(static::$encoder)
39+
? (static::$encoder)($value, $flags)
40+
: json_encode($value, $flags);
41+
}
42+
43+
/**
44+
* Decode the given value.
45+
*
46+
* @param mixed $value The JSON string to decode
47+
* @param null|bool $associative When true, JSON objects will be returned as associative arrays; when false, as objects
48+
* @return mixed The decoded value, or null if the JSON string is invalid or represents null
49+
* @throws JsonException When JSON decoding fails (if custom decoder is not set and JSON_THROW_ON_ERROR is used)
50+
*
51+
* @note This method returns null both when the JSON string is "null" (valid JSON null)
52+
* and when the JSON string is invalid/malformed (decode failure).
53+
* Use json_last_error() after calling this method to distinguish between these cases.
54+
*/
55+
public static function decode(mixed $value, ?bool $associative = true): mixed
56+
{
57+
if (isset(static::$decoder)) {
58+
return (static::$decoder)($value, $associative);
59+
}
60+
61+
$decoded = json_decode($value, $associative);
62+
63+
// Check for JSON decode errors
64+
// Note: json_decode() returns null both for valid JSON null and decode failures.
65+
// Use json_last_error() immediately after this call to distinguish between them.
66+
if (json_last_error() !== JSON_ERROR_NONE) {
67+
// Return null on decode failure, but error can be checked via json_last_error()
68+
return null;
69+
}
70+
71+
return $decoded;
72+
}
73+
74+
/**
75+
* Encode all values using the given callable.
76+
*/
77+
public static function encodeUsing(?callable $encoder): void
78+
{
79+
static::$encoder = $encoder;
80+
}
81+
82+
/**
83+
* Decode all values using the given callable.
84+
*/
85+
public static function decodeUsing(?callable $decoder): void
86+
{
87+
static::$decoder = $decoder;
88+
}
89+
}

tests/DatabaseModelCustomCastingTest.php

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
use Hyperf\Database\Exception\InvalidCastException;
2222
use Hyperf\Database\Model\Casts\ArrayObject;
2323
use Hyperf\Database\Model\Casts\AsArrayObject;
24+
use Hyperf\Database\Model\Casts\AsCollection;
2425
use Hyperf\Database\Model\CastsValue;
2526
use Hyperf\Database\Model\Model;
2627
use HyperfTest\Database\Stubs\ContainerStub;
@@ -391,6 +392,27 @@ public function testArrayObjectSerialization()
391392
$this->assertEquals($data, $unserialized->config->toArray());
392393
$this->assertEquals(json_encode($data), $unserialized->getAttributes()['config']);
393394
}
395+
396+
public function testCastsAsCollection()
397+
{
398+
$m = new TestModelWithCustomCast();
399+
$m->fill([
400+
'as_collection' => [['id' => 1, 'name' => 'hyperf'], ['id' => 2, 'name' => 'swoole']],
401+
'as_collection2' => [['id' => 3, 'name' => 'hyperf'], ['id' => 4, 'name' => 'swoole']],
402+
]);
403+
404+
/** @var Collection $collection */
405+
$collection = $m->as_collection;
406+
407+
$this->assertSame(1, $collection->first()['id']);
408+
$this->assertSame(2, $collection->last()['id']);
409+
410+
/** @var Collection $collection */
411+
$collection = $m->as_collection2;
412+
413+
$this->assertSame(3, $collection->first()->id);
414+
$this->assertSame(4, $collection->last()->id);
415+
}
394416
}
395417

396418
class TestModelWithCustomCast extends Model
@@ -415,7 +437,28 @@ class TestModelWithCustomCast extends Model
415437
'value_object_caster_with_caster_instance' => ValueObjectWithCasterInstance::class,
416438
'cast_using' => CastUsing::class,
417439
'invalid_caster' => InvalidCaster::class,
440+
'as_collection' => AsCollection::class,
441+
'as_collection2' => AsCollection::class,
418442
];
443+
444+
public function getCasts(): array
445+
{
446+
$casts = parent::getCasts();
447+
$casts['as_collection2'] = AsCollection::using(Collection::class, [TestModelValue::class, 'from']);
448+
return $casts;
449+
}
450+
}
451+
452+
class TestModelValue
453+
{
454+
public function __construct(public int $id, public string $name)
455+
{
456+
}
457+
458+
public static function from(array $item)
459+
{
460+
return new TestModelValue($item['id'], $item['name']);
461+
}
419462
}
420463

421464
class TestModelWithArrayObjectCast extends Model

0 commit comments

Comments
 (0)