Skip to content

Commit 75ab812

Browse files
committed
Merge remote-tracking branch 'upstream/develop' into 4.8
# Conflicts: # utils/phpstan-baseline/loader.neon
2 parents c82e68e + d109a1a commit 75ab812

File tree

15 files changed

+343
-23
lines changed

15 files changed

+343
-23
lines changed

.github/workflows/deploy-userguide-latest.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
5858
# Create an artifact of the html output
5959
- name: Upload artifact
60-
uses: actions/upload-artifact@v6
60+
uses: actions/upload-artifact@v7
6161
with:
6262
name: HTML Documentation
6363
path: user_guide_src/build/html/

.github/workflows/reusable-coveralls.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ jobs:
3030
coverage: xdebug
3131

3232
- name: Download coverage files
33-
uses: actions/download-artifact@v7
33+
uses: actions/download-artifact@v8
3434
with:
3535
path: build/cov
3636

.github/workflows/reusable-phpunit-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ jobs:
235235

236236
- name: Upload coverage results as artifact
237237
if: ${{ inputs.enable-artifact-upload }}
238-
uses: actions/upload-artifact@v6
238+
uses: actions/upload-artifact@v7
239239
with:
240240
name: ${{ steps.setup-env.outputs.ARTIFACT_NAME }}
241241
path: build/cov/coverage-${{ steps.setup-env.outputs.ARTIFACT_NAME }}.cov

.github/workflows/reusable-serviceless-phpunit-test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ jobs:
127127

128128
- name: Upload coverage results as artifact
129129
if: ${{ inputs.enable-artifact-upload }}
130-
uses: actions/upload-artifact@v6
130+
uses: actions/upload-artifact@v7
131131
with:
132132
name: ${{ steps.setup-env.outputs.ARTIFACT_NAME }}
133133
path: build/cov/coverage-${{ steps.setup-env.outputs.ARTIFACT_NAME }}.cov

system/CLI/CLI.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1106,7 +1106,7 @@ public static function reset(): void
11061106
static::$initialized = false;
11071107
static::$segments = [];
11081108
static::$options = [];
1109-
static::$lastWrite = 'write';
1109+
static::$lastWrite = null;
11101110
static::$height = null;
11111111
static::$width = null;
11121112
static::$isColored = static::hasColorSupport(STDOUT);

system/Language/en/Validation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@
6565
'valid_json' => 'The {field} field must contain a valid json.',
6666

6767
// Credit Cards
68-
'valid_cc_num' => '{field} does not appear to be a valid credit card number.',
68+
'valid_cc_number' => '{field} does not appear to be a valid credit card number.',
6969

7070
// Files
7171
'uploaded' => '{field} is not a valid uploaded file.',

system/Validation/Validation.php

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,15 @@ public function run(?array $data = null, ?string $group = null, $dbGroup = null)
178178
ARRAY_FILTER_USE_KEY,
179179
);
180180

181+
// Emit null for every leaf path that is structurally reachable
182+
// but whose key is absent from the data. This mirrors the
183+
// non-wildcard behaviour where a missing key is treated as null,
184+
// so that all rules behave consistently regardless of whether
185+
// the field uses a wildcard or not.
186+
foreach ($this->walkForAllPossiblePaths(explode('.', $field), $data, '') as $path) {
187+
$values[$path] = null;
188+
}
189+
181190
// if keys not found
182191
$values = $values !== [] ? $values : [$field => null];
183192
} else {
@@ -987,6 +996,86 @@ protected function splitRules(string $rules): array
987996
return array_unique($rules);
988997
}
989998

999+
/**
1000+
* Entry point: allocates a single accumulator and delegates to the
1001+
* recursive collector, so no intermediate arrays are built or unpacked.
1002+
*
1003+
* @param list<string> $segments
1004+
* @param array<array-key, mixed>|mixed $current
1005+
*
1006+
* @return list<string>
1007+
*/
1008+
private function walkForAllPossiblePaths(array $segments, mixed $current, string $prefix): array
1009+
{
1010+
$result = [];
1011+
$this->collectMissingPaths($segments, 0, count($segments), $current, $prefix, $result);
1012+
1013+
return $result;
1014+
}
1015+
1016+
/**
1017+
* Recursively walks the data structure, expanding wildcard segments over
1018+
* all array keys, and appends to $result by reference. Only concrete leaf
1019+
* paths where the key is genuinely absent are recorded - intermediate
1020+
* missing segments are silently skipped so `*` never appears in a result.
1021+
*
1022+
* @param list<string> $segments
1023+
* @param int<0, max> $segmentCount
1024+
* @param array<array-key, mixed>|mixed $current
1025+
* @param list<string> $result
1026+
*/
1027+
private function collectMissingPaths(
1028+
array $segments,
1029+
int $index,
1030+
int $segmentCount,
1031+
mixed $current,
1032+
string $prefix,
1033+
array &$result,
1034+
): void {
1035+
if ($index >= $segmentCount) {
1036+
// Successfully navigated every segment - the path exists in the data.
1037+
return;
1038+
}
1039+
1040+
$segment = $segments[$index];
1041+
$nextIndex = $index + 1;
1042+
1043+
if ($segment === '*') {
1044+
if (! is_array($current)) {
1045+
return;
1046+
}
1047+
1048+
foreach ($current as $key => $value) {
1049+
$keyPrefix = $prefix !== '' ? $prefix . '.' . $key : (string) $key;
1050+
1051+
// Non-array elements with remaining segments are a structural
1052+
// mismatch (e.g. the DBGroup sentinel, scalar siblings) - skip.
1053+
if (! is_array($value) && $nextIndex < $segmentCount) {
1054+
continue;
1055+
}
1056+
1057+
$this->collectMissingPaths($segments, $nextIndex, $segmentCount, $value, $keyPrefix, $result);
1058+
}
1059+
1060+
return;
1061+
}
1062+
1063+
$newPrefix = $prefix !== '' ? $prefix . '.' . $segment : $segment;
1064+
1065+
if (! is_array($current) || ! array_key_exists($segment, $current)) {
1066+
// Only record a missing path for the leaf key. When an intermediate
1067+
// segment is absent there is nothing to validate in that branch,
1068+
// so skip it to avoid false-positive errors.
1069+
if ($nextIndex === $segmentCount) {
1070+
$result[] = $newPrefix;
1071+
}
1072+
1073+
return;
1074+
}
1075+
1076+
$this->collectMissingPaths($segments, $nextIndex, $segmentCount, $current[$segment], $newPrefix, $result);
1077+
}
1078+
9901079
/**
9911080
* Resets the class to a blank slate. Should be called whenever
9921081
* you need to process more than one array.

tests/system/CLI/CLITest.php

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -347,23 +347,23 @@ public function testWriteForeground(): void
347347
{
348348
CLI::write('test', 'red');
349349

350-
$expected = "\033[0;31mtest\033[0m" . PHP_EOL;
350+
$expected = PHP_EOL . "\033[0;31mtest\033[0m" . PHP_EOL;
351351
$this->assertSame($expected, $this->getStreamFilterBuffer());
352352
}
353353

354354
public function testWriteForegroundWithColorBefore(): void
355355
{
356356
CLI::write(CLI::color('green', 'green') . ' red', 'red');
357357

358-
$expected = "\033[0;32mgreen\033[0m\033[0;31m red\033[0m" . PHP_EOL;
358+
$expected = PHP_EOL . "\033[0;32mgreen\033[0m\033[0;31m red\033[0m" . PHP_EOL;
359359
$this->assertSame($expected, $this->getStreamFilterBuffer());
360360
}
361361

362362
public function testWriteForegroundWithColorAfter(): void
363363
{
364364
CLI::write('red ' . CLI::color('green', 'green'), 'red');
365365

366-
$expected = "\033[0;31mred \033[0m\033[0;32mgreen\033[0m" . PHP_EOL;
366+
$expected = PHP_EOL . "\033[0;31mred \033[0m\033[0;32mgreen\033[0m" . PHP_EOL;
367367
$this->assertSame($expected, $this->getStreamFilterBuffer());
368368
}
369369

@@ -377,15 +377,15 @@ public function testWriteForegroundWithColorTwice(): void
377377
'red',
378378
);
379379

380-
$expected = "\033[0;32mgreen\033[0m\033[0;31m red \033[0m\033[0;32mgreen\033[0m" . PHP_EOL;
380+
$expected = PHP_EOL . "\033[0;32mgreen\033[0m\033[0;31m red \033[0m\033[0;32mgreen\033[0m" . PHP_EOL;
381381
$this->assertSame($expected, $this->getStreamFilterBuffer());
382382
}
383383

384384
public function testWriteBackground(): void
385385
{
386386
CLI::write('test', 'red', 'green');
387387

388-
$expected = "\033[0;31m\033[42mtest\033[0m" . PHP_EOL;
388+
$expected = PHP_EOL . "\033[0;31m\033[42mtest\033[0m" . PHP_EOL;
389389
$this->assertSame($expected, $this->getStreamFilterBuffer());
390390
}
391391

@@ -427,7 +427,7 @@ public function testShowProgress(): void
427427
CLI::write('third.');
428428
CLI::showProgress(1, 20);
429429

430-
$expected = 'first.' . PHP_EOL .
430+
$expected = PHP_EOL . 'first.' . PHP_EOL .
431431
"[\033[32m#.........\033[0m] 5% Complete" . PHP_EOL .
432432
"\033[1A[\033[32m#####.....\033[0m] 50% Complete" . PHP_EOL .
433433
"\033[1A[\033[32m##########\033[0m] 100% Complete" . PHP_EOL .
@@ -447,7 +447,7 @@ public function testShowProgressWithoutBar(): void
447447
CLI::showProgress(false, 20);
448448
CLI::showProgress(false, 20);
449449

450-
$expected = 'first.' . PHP_EOL . "\007\007\007";
450+
$expected = PHP_EOL . 'first.' . PHP_EOL . "\007\007\007";
451451
$this->assertSame($expected, $this->getStreamFilterBuffer());
452452
}
453453

@@ -620,13 +620,15 @@ public static function provideTable(): iterable
620620
[
621621
$oneRow,
622622
[],
623+
PHP_EOL .
623624
'+---+-----+' . PHP_EOL .
624625
'| 1 | bar |' . PHP_EOL .
625626
'+---+-----+' . PHP_EOL . PHP_EOL,
626627
],
627628
[
628629
$oneRow,
629630
$head,
631+
PHP_EOL .
630632
'+----+-------+' . PHP_EOL .
631633
'| ID | Title |' . PHP_EOL .
632634
'+----+-------+' . PHP_EOL .
@@ -636,6 +638,7 @@ public static function provideTable(): iterable
636638
[
637639
$manyRows,
638640
[],
641+
PHP_EOL .
639642
'+---+-----------------+' . PHP_EOL .
640643
'| 1 | bar |' . PHP_EOL .
641644
'| 2 | bar * 2 |' . PHP_EOL .
@@ -645,6 +648,7 @@ public static function provideTable(): iterable
645648
[
646649
$manyRows,
647650
$head,
651+
PHP_EOL .
648652
'+----+-----------------+' . PHP_EOL .
649653
'| ID | Title |' . PHP_EOL .
650654
'+----+-----------------+' . PHP_EOL .
@@ -665,6 +669,7 @@ public static function provideTable(): iterable
665669
'ID',
666670
'タイトル',
667671
],
672+
PHP_EOL .
668673
'+------+----------+' . PHP_EOL .
669674
'| ID | タイトル |' . PHP_EOL .
670675
'+------+----------+' . PHP_EOL .

tests/system/CLI/SignalTest.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ protected function setUp(): void
5353
$this->command = new SignalCommand($this->logger, service('commands'));
5454
}
5555

56+
protected function tearDown(): void
57+
{
58+
CLI::reset();
59+
60+
parent::tearDown();
61+
}
62+
5663
public function testSignalRegistration(): void
5764
{
5865
$this->command->testRegisterSignals([SIGTERM, SIGINT], [SIGTERM => 'customTermHandler']);

tests/system/Database/Live/InsertTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -275,4 +275,18 @@ public function testInsertBatchWithQueryAndRawSqlAndManualColumns(): void
275275

276276
$this->forge->dropTable('user2', true);
277277
}
278+
279+
public function testInsertWithTooLongCharactersThrowsError(): void
280+
{
281+
if ($this->db->DBDriver === 'SQLite3') {
282+
$this->markTestSkipped('SQLite does not enforce VARCHAR length constraints.');
283+
}
284+
285+
$this->expectException(DatabaseException::class);
286+
287+
$this->db->table('misc')->insert([
288+
'key' => 'too_long',
289+
'value' => str_repeat('a', 401), // 'value' is VARCHAR(400), so this should throw an error
290+
]);
291+
}
278292
}

0 commit comments

Comments
 (0)