Skip to content

Commit 063d73f

Browse files
authored
PHPORM-100 Support query on numerical field names (#2642)
1 parent f80501d commit 063d73f

File tree

4 files changed

+79
-12
lines changed

4 files changed

+79
-12
lines changed

src/Eloquent/Model.php

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,8 @@ public function getAttribute($key)
169169
return null;
170170
}
171171

172+
$key = (string) $key;
173+
172174
// An unset attribute is null or throw an exception.
173175
if (isset($this->unset[$key])) {
174176
return $this->throwMissingAttributeExceptionIfApplicable($key);
@@ -194,6 +196,8 @@ public function getAttribute($key)
194196
/** @inheritdoc */
195197
protected function getAttributeFromArray($key)
196198
{
199+
$key = (string) $key;
200+
197201
// Support keys in dot notation.
198202
if (str_contains($key, '.')) {
199203
return Arr::get($this->attributes, $key);
@@ -205,6 +209,8 @@ protected function getAttributeFromArray($key)
205209
/** @inheritdoc */
206210
public function setAttribute($key, $value)
207211
{
212+
$key = (string) $key;
213+
208214
// Convert _id to ObjectID.
209215
if ($key === '_id' && is_string($value)) {
210216
$builder = $this->newBaseQueryBuilder();
@@ -314,6 +320,8 @@ public function originalIsEquivalent($key)
314320
/** @inheritdoc */
315321
public function offsetUnset($offset): void
316322
{
323+
$offset = (string) $offset;
324+
317325
if (str_contains($offset, '.')) {
318326
// Update the field in the subdocument
319327
Arr::forget($this->attributes, $offset);

src/Query/Builder.php

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,12 @@
2323
use MongoDB\BSON\UTCDateTime;
2424
use MongoDB\Driver\Cursor;
2525
use RuntimeException;
26-
use Stringable;
2726

2827
use function array_fill_keys;
2928
use function array_is_list;
3029
use function array_key_exists;
30+
use function array_map;
3131
use function array_merge;
32-
use function array_merge_recursive;
3332
use function array_values;
3433
use function array_walk_recursive;
3534
use function assert;
@@ -46,7 +45,11 @@
4645
use function implode;
4746
use function in_array;
4847
use function is_array;
48+
use function is_bool;
49+
use function is_callable;
50+
use function is_float;
4951
use function is_int;
52+
use function is_null;
5053
use function is_string;
5154
use function md5;
5255
use function preg_match;
@@ -60,6 +63,7 @@
6063
use function strlen;
6164
use function strtolower;
6265
use function substr;
66+
use function var_export;
6367

6468
class Builder extends BaseBuilder
6569
{
@@ -665,7 +669,7 @@ public function update(array $values, array $options = [])
665669
{
666670
// Use $set as default operator for field names that are not in an operator
667671
foreach ($values as $key => $value) {
668-
if (str_starts_with($key, '$')) {
672+
if (is_string($key) && str_starts_with($key, '$')) {
669673
continue;
670674
}
671675

@@ -952,7 +956,20 @@ public function convertKey($id)
952956
return $id;
953957
}
954958

955-
/** @inheritdoc */
959+
/**
960+
* Add a basic where clause to the query.
961+
*
962+
* If 1 argument, the signature is: where(array|Closure $where)
963+
* If 2 arguments, the signature is: where(string $column, mixed $value)
964+
* If 3 arguments, the signature is: where(string $colum, string $operator, mixed $value)
965+
*
966+
* @param Closure|string|array $column
967+
* @param mixed $operator
968+
* @param mixed $value
969+
* @param string $boolean
970+
*
971+
* @return $this
972+
*/
956973
public function where($column, $operator = null, $value = null, $boolean = 'and')
957974
{
958975
$params = func_get_args();
@@ -966,8 +983,12 @@ public function where($column, $operator = null, $value = null, $boolean = 'and'
966983
}
967984
}
968985

969-
if (func_num_args() === 1 && is_string($column)) {
970-
throw new ArgumentCountError(sprintf('Too few arguments to function %s("%s"), 1 passed and at least 2 expected when the 1st is a string.', __METHOD__, $column));
986+
if (func_num_args() === 1 && ! is_array($column) && ! is_callable($column)) {
987+
throw new ArgumentCountError(sprintf('Too few arguments to function %s(%s), 1 passed and at least 2 expected when the 1st is not an array or a callable', __METHOD__, var_export($column, true)));
988+
}
989+
990+
if (is_float($column) || is_bool($column) || is_null($column)) {
991+
throw new InvalidArgumentException(sprintf('First argument of %s must be a field path as "string". Got "%s"', __METHOD__, get_debug_type($column)));
971992
}
972993

973994
return parent::where(...$params);
@@ -998,17 +1019,15 @@ protected function compileWheres(): array
9981019
}
9991020

10001021
// Convert column name to string to use as array key
1001-
if (isset($where['column']) && $where['column'] instanceof Stringable) {
1022+
if (isset($where['column'])) {
10021023
$where['column'] = (string) $where['column'];
10031024
}
10041025

10051026
// Convert id's.
10061027
if (isset($where['column']) && ($where['column'] === '_id' || str_ends_with($where['column'], '._id'))) {
10071028
if (isset($where['values'])) {
10081029
// Multiple values.
1009-
foreach ($where['values'] as &$value) {
1010-
$value = $this->convertKey($value);
1011-
}
1030+
$where['values'] = array_map($this->convertKey(...), $where['values']);
10121031
} elseif (isset($where['value'])) {
10131032
// Single value.
10141033
$where['value'] = $this->convertKey($where['value']);
@@ -1076,7 +1095,14 @@ protected function compileWheres(): array
10761095
}
10771096

10781097
// Merge the compiled where with the others.
1079-
$compiled = array_merge_recursive($compiled, $result);
1098+
// array_merge_recursive can't be used here because it converts int keys to sequential int.
1099+
foreach ($result as $key => $value) {
1100+
if (in_array($key, ['$and', '$or', '$nor'])) {
1101+
$compiled[$key] = array_merge($compiled[$key] ?? [], $value);
1102+
} else {
1103+
$compiled[$key] = $value;
1104+
}
1105+
}
10801106
}
10811107

10821108
return $compiled;

tests/ModelTest.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -971,4 +971,20 @@ public function testEnumCast(): void
971971
$this->assertSame(MemberStatus::Member->value, $check->getRawOriginal('member_status'));
972972
$this->assertSame(MemberStatus::Member, $check->member_status);
973973
}
974+
975+
public function testNumericFieldName(): void
976+
{
977+
$user = new User();
978+
$user->{1} = 'one';
979+
$user->{2} = ['3' => 'two.three'];
980+
$user->save();
981+
982+
$found = User::where(1, 'one')->first();
983+
$this->assertInstanceOf(User::class, $found);
984+
$this->assertEquals('one', $found[1]);
985+
986+
$found = User::where('2.3', 'two.three')->first();
987+
$this->assertInstanceOf(User::class, $found);
988+
$this->assertEquals([3 => 'two.three'], $found[2]);
989+
}
974990
}

tests/Query/BuilderTest.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,11 @@ public static function provideQueryBuilderToMql(): iterable
9090
fn (Builder $builder) => $builder->where('foo', 'bar'),
9191
];
9292

93+
yield 'find with numeric field name' => [
94+
['find' => [['123' => 'bar'], []]],
95+
fn (Builder $builder) => $builder->where(123, 'bar'),
96+
];
97+
9398
yield 'where with single array of conditions' => [
9499
[
95100
'find' => [
@@ -1175,10 +1180,16 @@ public static function provideExceptions(): iterable
11751180

11761181
yield 'find with single string argument' => [
11771182
ArgumentCountError::class,
1178-
'Too few arguments to function MongoDB\Laravel\Query\Builder::where("foo"), 1 passed and at least 2 expected when the 1st is a string',
1183+
'Too few arguments to function MongoDB\Laravel\Query\Builder::where(\'foo\'), 1 passed and at least 2 expected when the 1st is not an array',
11791184
fn (Builder $builder) => $builder->where('foo'),
11801185
];
11811186

1187+
yield 'find with single numeric argument' => [
1188+
ArgumentCountError::class,
1189+
'Too few arguments to function MongoDB\Laravel\Query\Builder::where(123), 1 passed and at least 2 expected when the 1st is not an array',
1190+
fn (Builder $builder) => $builder->where(123),
1191+
];
1192+
11821193
yield 'where regex not starting with /' => [
11831194
LogicException::class,
11841195
'Missing expected starting delimiter in regular expression "^ac/me$", supported delimiters are: / # ~',
@@ -1208,6 +1219,12 @@ public static function provideExceptions(): iterable
12081219
'Invalid time format, expected HH:MM:SS, HH:MM or HH, got "stdClass"',
12091220
fn (Builder $builder) => $builder->whereTime('created_at', new stdClass()),
12101221
];
1222+
1223+
yield 'where invalid column type' => [
1224+
InvalidArgumentException::class,
1225+
'First argument of MongoDB\Laravel\Query\Builder::where must be a field path as "string". Got "float"',
1226+
fn (Builder $builder) => $builder->where(2.3, '>', 1),
1227+
];
12111228
}
12121229

12131230
/** @dataProvider getEloquentMethodsNotSupported */

0 commit comments

Comments
 (0)