Skip to content

Commit 471fb91

Browse files
committed
Merge branch 'master' into 3.2-merge
# Conflicts: # .github/workflows/test.yml # src/grpc-client/src/BaseClient.php
2 parents 10fe1ff + 2476ecb commit 471fb91

17 files changed

+600
-29
lines changed

src/Concerns/BuildsQueries.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -187,7 +187,7 @@ public function first($columns = ['*'])
187187
*/
188188
public function eachById(callable $callback, int $count = 1000, ?string $column = null, ?string $alias = null): bool
189189
{
190-
return $this->chunkById($count, function (Collection $results) use ($callback) {
190+
return $this->chunkById($count, function (BaseCollection $results) use ($callback) {
191191
foreach ($results as $value) {
192192
if ($callback($value) === false) {
193193
return false;

src/Concerns/CompilesJsonPaths.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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\Concerns;
14+
15+
use Hyperf\Stringable\Str;
16+
17+
use function Hyperf\Collection\collect;
18+
19+
trait CompilesJsonPaths
20+
{
21+
/**
22+
* Split the given JSON selector into the field and the optional path and wrap them separately.
23+
*
24+
* @param string $column
25+
* @return array
26+
*/
27+
protected function wrapJsonFieldAndPath($column)
28+
{
29+
$parts = explode('->', $column, 2);
30+
31+
$field = $this->wrap($parts[0]);
32+
33+
$path = count($parts) > 1 ? ', ' . $this->wrapJsonPath($parts[1], '->') : '';
34+
35+
return [$field, $path];
36+
}
37+
38+
/**
39+
* Wrap the given JSON path.
40+
*
41+
* @param string $value
42+
* @param string $delimiter
43+
* @return string
44+
*/
45+
protected function wrapJsonPath($value, $delimiter = '->')
46+
{
47+
$value = preg_replace("/([\\\\]+)?\\'/", "''", $value);
48+
49+
$jsonPath = collect(explode($delimiter, $value))
50+
->map(fn ($segment) => $this->wrapJsonPathSegment($segment))
51+
->join('.');
52+
53+
return "'$" . (str_starts_with($jsonPath, '[') ? '' : '.') . $jsonPath . "'";
54+
}
55+
56+
/**
57+
* Wrap the given JSON path segment.
58+
*
59+
* @param string $segment
60+
* @return string
61+
*/
62+
protected function wrapJsonPathSegment($segment)
63+
{
64+
if (preg_match('/(\[[^\]]+\])+$/', $segment, $parts)) {
65+
$key = Str::beforeLast($segment, $parts[0]);
66+
67+
if (! empty($key)) {
68+
return '"' . $key . '"' . $parts[0];
69+
}
70+
71+
return $parts[0];
72+
}
73+
74+
return '"' . $segment . '"';
75+
}
76+
}

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+
}

src/Query/Builder.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1559,6 +1559,42 @@ public function orWhereJsonDoesntOverlap(string $column, mixed $value): static
15591559
return $this->whereJsonDoesntOverlap($column, $value, 'or');
15601560
}
15611561

1562+
/**
1563+
* Add a clause that determines if a JSON path exists to the query.
1564+
*/
1565+
public function whereJsonContainsKey(string $column, string $boolean = 'and', bool $not = false): static
1566+
{
1567+
$type = 'JsonContainsKey';
1568+
1569+
$this->wheres[] = compact('type', 'column', 'boolean', 'not');
1570+
1571+
return $this;
1572+
}
1573+
1574+
/**
1575+
* Add an "or" clause that determines if a JSON path exists to the query.
1576+
*/
1577+
public function orWhereJsonContainsKey(string $column): static
1578+
{
1579+
return $this->whereJsonContainsKey($column, 'or');
1580+
}
1581+
1582+
/**
1583+
* Add a clause that determines if a JSON path does not exist to the query.
1584+
*/
1585+
public function whereJsonDoesntContainKey(string $column, string $boolean = 'and'): static
1586+
{
1587+
return $this->whereJsonContainsKey($column, $boolean, true);
1588+
}
1589+
1590+
/**
1591+
* Add an "or" clause that determines if a JSON path does not exist to the query.
1592+
*/
1593+
public function orWhereJsonDoesntContainKey(string $column): static
1594+
{
1595+
return $this->whereJsonDoesntContainKey($column, 'or');
1596+
}
1597+
15621598
/**
15631599
* Add an "where Bit Functions and Operators" clause to the query.
15641600
*/

src/Query/Grammars/Grammar.php

Lines changed: 21 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
namespace Hyperf\Database\Query\Grammars;
1414

1515
use Hyperf\Collection\Arr;
16+
use Hyperf\Database\Concerns\CompilesJsonPaths;
1617
use Hyperf\Database\Grammar as BaseGrammar;
1718
use Hyperf\Database\Query\Builder;
1819
use Hyperf\Database\Query\Expression;
@@ -27,6 +28,8 @@
2728

2829
class Grammar extends BaseGrammar
2930
{
31+
use CompilesJsonPaths;
32+
3033
/**
3134
* The grammar specific operators.
3235
*/
@@ -398,6 +401,24 @@ protected function compileJsonOverlaps(string $column, string $value): string
398401
throw new RuntimeException('This database engine does not support JSON overlaps operations.');
399402
}
400403

404+
/**
405+
* Compile a "where JSON contains key" clause.
406+
*/
407+
protected function whereJsonContainsKey(Builder $query, array $where): string
408+
{
409+
$not = $where['not'] ? 'not ' : '';
410+
411+
return $not . $this->compileJsonContainsKey($where['column']);
412+
}
413+
414+
/**
415+
* Compile a "JSON contains key" statement into SQL.
416+
*/
417+
protected function compileJsonContainsKey(string $column): string
418+
{
419+
throw new RuntimeException('This database engine does not support JSON contains key operations.');
420+
}
421+
401422
/**
402423
* Compile the components necessary for a select clause.
403424
*/
@@ -1108,33 +1129,6 @@ protected function wrapJsonSelector($value): string
11081129
throw new RuntimeException('This database engine does not support JSON operations.');
11091130
}
11101131

1111-
/**
1112-
* Split the given JSON selector into the field and the optional path and wrap them separately.
1113-
*
1114-
* @param string $column
1115-
*/
1116-
protected function wrapJsonFieldAndPath($column): array
1117-
{
1118-
$parts = explode('->', $column, 2);
1119-
1120-
$field = $this->wrap($parts[0]);
1121-
1122-
$path = count($parts) > 1 ? ', ' . $this->wrapJsonPath($parts[1], '->') : '';
1123-
1124-
return [$field, $path];
1125-
}
1126-
1127-
/**
1128-
* Wrap the given JSON path.
1129-
*
1130-
* @param string $value
1131-
* @param string $delimiter
1132-
*/
1133-
protected function wrapJsonPath($value, $delimiter = '->'): string
1134-
{
1135-
return '\'$."' . str_replace($delimiter, '"."', $value) . '"\'';
1136-
}
1137-
11381132
/**
11391133
* Determine if the given string is a JSON selector.
11401134
*

0 commit comments

Comments
 (0)