|
15 | 15 | /** |
16 | 16 | * SmartArrayBase - Base implementation for SmartArray and SmartArrayHtml. |
17 | 17 | * |
18 | | - * Uses wide return types that child classes narrow via covariance. |
| 18 | + * Uses wide return types that SmartArray and SmartArrayHtml narrow per mode. |
19 | 19 | * Do not instantiate directly - use SmartArray or SmartArrayHtml. |
20 | 20 | * |
21 | 21 | * Extends stdClass to enable clean IDE property autocomplete. Without this, |
@@ -503,25 +503,6 @@ public static function getRawValue(mixed $value): mixed |
503 | 503 | throw new InvalidArgumentException("Unsupported value type: " . get_debug_type($value)); |
504 | 504 | } |
505 | 505 |
|
506 | | - /** |
507 | | - * How where(), whereNot(), and contains() decide two values match: the same |
508 | | - * answers a SQL WHERE gives, except strings stay case-sensitive and a |
509 | | - * non-numeric string never equals 0. Numbers match numeric strings (5 matches |
510 | | - * '5'), two strings must match exactly, null only matches null, and true/false |
511 | | - * mean 1/0 (so, like MySQL, '01' and ' 1' match true and '00' matches false). |
512 | | - * Callers unwrap Smart values with getRawValue() first. |
513 | | - */ |
514 | | - private static function valueMatches(mixed $rowValue, mixed $value): bool |
515 | | - { |
516 | | - $value = is_bool($value) ? (int)$value : $value; |
517 | | - $rowValue = is_bool($rowValue) ? (int)$rowValue : $rowValue; |
518 | | - return match (true) { |
519 | | - $value === null || $rowValue === null => $value === $rowValue, |
520 | | - is_string($value) && is_string($rowValue) => $value === $rowValue, |
521 | | - default => $value == $rowValue, // PHP 8 numeric comparison, e.g. 1 == '1.00' |
522 | | - }; |
523 | | - } |
524 | | - |
525 | 506 | //endregion |
526 | 507 | //region Array Information |
527 | 508 |
|
@@ -558,9 +539,19 @@ public function isNotEmpty(): bool |
558 | 539 | */ |
559 | 540 | public function contains(mixed $value): bool |
560 | 541 | { |
561 | | - $value = self::getRawValue($value); |
| 542 | + $value = is_scalar($value) || $value === null ? $value : self::getRawValue($value); // fast path: skip getRawValue() for plain values |
| 543 | + $value = is_bool($value) ? (int)$value : $value; |
| 544 | + // This comparison repeats 3x across contains()/where()/whereNot() on purpose: a |
| 545 | + // shared helper would need a per-row call, which costs more than the comparison |
| 546 | + // itself. ValueMatchParityTest keeps the copies identical. |
562 | 547 | foreach ($this->toArray() as $element) { |
563 | | - if (self::valueMatches($element, $value)) { |
| 548 | + $element = is_bool($element) ? (int)$element : $element; |
| 549 | + $isMatch = match (true) { |
| 550 | + is_string($value) && is_string($element) => $value === $element, |
| 551 | + $value === null || $element === null => $value === $element, |
| 552 | + default => $value == $element, // PHP 8 numeric comparison, e.g. 1 == '1.00' |
| 553 | + }; |
| 554 | + if ($isMatch) { |
564 | 555 | return true; |
565 | 556 | } |
566 | 557 | } |
@@ -726,11 +717,22 @@ public function where(array|string $field, mixed $value = null): static |
726 | 717 | // Two-argument syntax: where('field', value) |
727 | 718 | if (is_string($field) && func_num_args() === 2) { |
728 | 719 | $this->warnIfMissing($field); |
729 | | - $value = self::getRawValue($value); |
730 | | - // repeated 4x, see the first where() loop for why |
| 720 | + $value = is_scalar($value) || $value === null ? $value : self::getRawValue($value); // fast path: skip getRawValue() for plain values |
| 721 | + $value = is_bool($value) ? (int)$value : $value; |
| 722 | + // loop repeated 4x, comparison repeated 3x - see the first where() loop and contains() for why |
731 | 723 | $matches = []; |
732 | 724 | foreach ($this->toArray() as $key => $row) { |
733 | | - if (array_key_exists($field, $row) && self::valueMatches($row[$field], $value)) { |
| 725 | + if (!array_key_exists($field, $row)) { |
| 726 | + continue; |
| 727 | + } |
| 728 | + $rowValue = $row[$field]; |
| 729 | + $rowValue = is_bool($rowValue) ? (int)$rowValue : $rowValue; |
| 730 | + $isMatch = match (true) { |
| 731 | + is_string($value) && is_string($rowValue) => $value === $rowValue, |
| 732 | + $value === null || $rowValue === null => $value === $rowValue, |
| 733 | + default => $value == $rowValue, // PHP 8 numeric comparison, e.g. 1 == '1.00' |
| 734 | + }; |
| 735 | + if ($isMatch) { |
734 | 736 | $matches[$key] = $row; |
735 | 737 | } |
736 | 738 | } |
@@ -781,11 +783,23 @@ public function whereNot(string $field, mixed $value = null): static |
781 | 783 | return $result; |
782 | 784 | } |
783 | 785 |
|
784 | | - $value = self::getRawValue($value); |
785 | | - // repeated 4x, see the first where() loop for why |
| 786 | + $value = is_scalar($value) || $value === null ? $value : self::getRawValue($value); // fast path: skip getRawValue() for plain values |
| 787 | + $value = is_bool($value) ? (int)$value : $value; |
| 788 | + // loop repeated 4x, comparison repeated 3x - see the first where() loop and contains() for why |
786 | 789 | $matches = []; |
787 | 790 | foreach ($this->toArray() as $key => $row) { |
788 | | - if (!array_key_exists($field, $row) || !self::valueMatches($row[$field], $value)) { |
| 791 | + if (!array_key_exists($field, $row)) { |
| 792 | + $matches[$key] = $row; |
| 793 | + continue; |
| 794 | + } |
| 795 | + $rowValue = $row[$field]; |
| 796 | + $rowValue = is_bool($rowValue) ? (int)$rowValue : $rowValue; |
| 797 | + $isMatch = match (true) { |
| 798 | + is_string($value) && is_string($rowValue) => $value === $rowValue, |
| 799 | + $value === null || $rowValue === null => $value === $rowValue, |
| 800 | + default => $value == $rowValue, // PHP 8 numeric comparison, e.g. 1 == '1.00' |
| 801 | + }; |
| 802 | + if (!$isMatch) { |
789 | 803 | $matches[$key] = $row; |
790 | 804 | } |
791 | 805 | } |
@@ -816,9 +830,11 @@ public function whereInList(string $field, mixed $value): static |
816 | 830 | { |
817 | 831 | $this->assertNestedArray(); |
818 | 832 | $this->warnIfMissing($field); |
819 | | - $value = self::getRawValue($value); |
820 | | - if (!is_scalar($value) && $value !== null) { |
821 | | - throw new InvalidArgumentException("whereInList(): expected a single value to match, got " . get_debug_type($value)); |
| 833 | + if (!is_scalar($value) && $value !== null) { // fast path: skip getRawValue() for plain values |
| 834 | + $value = self::getRawValue($value); |
| 835 | + if (!is_scalar($value) && $value !== null) { |
| 836 | + throw new InvalidArgumentException("whereInList(): expected a single value to match, got " . get_debug_type($value)); |
| 837 | + } |
822 | 838 | } |
823 | 839 | $value = (string) $value; |
824 | 840 | $matches = []; |
@@ -1281,7 +1297,7 @@ public function load(string $field): static|SmartNull |
1281 | 1297 | * |
1282 | 1298 | * @internal |
1283 | 1299 | */ |
1284 | | - public function root(): self |
| 1300 | + public function root(): SmartArrayBase |
1285 | 1301 | { |
1286 | 1302 | return $this->root; |
1287 | 1303 | } |
|
0 commit comments