- Added clock timezone coalesce assignment and clone constructor test - Added CSS selector unicode escape preg_replace_callback test - Added dotenv escaped dollars protection test - Added filesystem dynamic call error handler test - Added finder gitignore preg_replace_callback test - Added finder directory normalization with array_merge unpack test - Added intl environment variable filtering with static cache test - Added LDAP collection iterator caching test - Added process environment union and command line string conversion test - Added semaphore negated coalesce assignment test - Added serializer context group merging with unpack test - Added validator constraint serialization with private key handling test - Added word count trimming and filtering test - Added weblink header parser with repeated attributes testpull/15/head
parent
75bf9f4113
commit
addbf897f9
14 changed files with 642 additions and 0 deletions
@ -0,0 +1,56 @@ |
||||
--TEST-- |
||||
Symfony Clock style timezone coalesce assignment and clone in constructor |
||||
--XFAIL-- |
||||
Known AOT bug: DateTimeZone assigned through clone during constructor can become an uninitialized internal object. |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyNativeClockCase |
||||
{ |
||||
private DateTimeZone $timezone; |
||||
|
||||
public function __construct(DateTimeZone|string|null $timezone = null) |
||||
{ |
||||
$this->timezone = is_string($timezone ??= date_default_timezone_get()) ? $this->withTimeZone($timezone)->timezone : $timezone; |
||||
} |
||||
|
||||
public function withTimeZone(DateTimeZone|string $timezone): static |
||||
{ |
||||
if (is_string($timezone)) { |
||||
$timezone = new DateTimeZone($timezone); |
||||
} |
||||
|
||||
$clone = clone $this; |
||||
$clone->timezone = $timezone; |
||||
|
||||
return $clone; |
||||
} |
||||
|
||||
public function name(): string |
||||
{ |
||||
return $this->timezone->getName(); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$previous = date_default_timezone_get(); |
||||
date_default_timezone_set('UTC'); |
||||
|
||||
$clock = new SymfonyNativeClockCase(); |
||||
var_dump($clock->name()); |
||||
|
||||
$tokyo = $clock->withTimeZone('Asia/Tokyo'); |
||||
var_dump($clock->name()); |
||||
var_dump($tokyo->name()); |
||||
|
||||
$custom = new SymfonyNativeClockCase(new DateTimeZone('Europe/Paris')); |
||||
var_dump($custom->name()); |
||||
|
||||
date_default_timezone_set($previous); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(3) "UTC" |
||||
string(3) "UTC" |
||||
string(10) "Asia/Tokyo" |
||||
string(12) "Europe/Paris" |
||||
@ -0,0 +1,32 @@ |
||||
--TEST-- |
||||
Symfony CssSelector pattern: unicode escape preg_replace_callback with bit operations |
||||
--FILE-- |
||||
<?php |
||||
function cssUnicodeUnescape(string $value): string |
||||
{ |
||||
return preg_replace_callback('/\\\\([0-9a-fA-F]{1,6})\s?/', static function ($match) { |
||||
$c = hexdec($match[1]); |
||||
|
||||
if (0x80 > $c %= 0x200000) { |
||||
return chr($c); |
||||
} |
||||
if (0x800 > $c) { |
||||
return chr(0xC0 | $c >> 6).chr(0x80 | $c & 0x3F); |
||||
} |
||||
if (0x10000 > $c) { |
||||
return chr(0xE0 | $c >> 12).chr(0x80 | $c >> 6 & 0x3F).chr(0x80 | $c & 0x3F); |
||||
} |
||||
|
||||
return ''; |
||||
}, $value); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(cssUnicodeUnescape('\\41 \\26')); |
||||
var_dump(bin2hex(cssUnicodeUnescape('\\20ac'))); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(2) "A&" |
||||
string(6) "e282ac" |
||||
@ -0,0 +1,31 @@ |
||||
--TEST-- |
||||
Symfony Dotenv pattern: preg_replace_callback preserves escaped dollars by backslash parity |
||||
--FILE-- |
||||
<?php |
||||
function protectEscapedDollars(string $value): string |
||||
{ |
||||
if (!str_contains($value, '$')) { |
||||
return $value; |
||||
} |
||||
|
||||
return preg_replace_callback('/\\\\+\$/', static function ($m) { |
||||
$bs = substr($m[0], 0, -1); |
||||
if (1 === strlen($bs) % 2) { |
||||
return substr($bs, 0, -1)."\x00"; |
||||
} |
||||
|
||||
return $m[0]; |
||||
}, $value); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(bin2hex(protectEscapedDollars('A\\$B'))); |
||||
var_dump(protectEscapedDollars('A\\\\$B')); |
||||
var_dump(protectEscapedDollars('plain')); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(6) "410042" |
||||
string(5) "A\\$B" |
||||
string(5) "plain" |
||||
@ -0,0 +1,57 @@ |
||||
--TEST-- |
||||
Symfony Filesystem style dynamic function call boxed by first-class error handler |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyFilesystemBoxCase |
||||
{ |
||||
private static ?array $lastError = null; |
||||
|
||||
private static function assertFunctionExists(string $func): void |
||||
{ |
||||
if (!function_exists($func)) { |
||||
throw new RuntimeException(sprintf('Unable to perform filesystem operation because the "%s()" function has been disabled.', $func)); |
||||
} |
||||
} |
||||
|
||||
private static function handleError(int $type, string $message): bool |
||||
{ |
||||
self::$lastError = [$type, $message]; |
||||
|
||||
return true; |
||||
} |
||||
|
||||
public static function box(string $func, mixed ...$args): mixed |
||||
{ |
||||
self::assertFunctionExists($func); |
||||
|
||||
self::$lastError = null; |
||||
set_error_handler(self::handleError(...)); |
||||
try { |
||||
return $func(...$args); |
||||
} finally { |
||||
restore_error_handler(); |
||||
} |
||||
} |
||||
|
||||
public static function getLastError(): ?array |
||||
{ |
||||
return self::$lastError; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(SymfonyFilesystemBoxCase::box('strtoupper', 'abc')); |
||||
var_dump(SymfonyFilesystemBoxCase::getLastError()); |
||||
|
||||
try { |
||||
SymfonyFilesystemBoxCase::box('definitely_missing_function'); |
||||
} catch (RuntimeException $e) { |
||||
echo $e->getMessage(), "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(3) "ABC" |
||||
NULL |
||||
Unable to perform filesystem operation because the "definitely_missing_function()" function has been disabled. |
||||
@ -0,0 +1,24 @@ |
||||
--TEST-- |
||||
Symfony Finder Gitignore pattern: preg_replace_callback with static arrow function |
||||
--FILE-- |
||||
<?php |
||||
function normalizeGitignoreCharacterClass(string $regex): string |
||||
{ |
||||
return preg_replace_callback( |
||||
'~\\\\\[((?:\\\\!)?)([^\[\]]*)\\\\\]~', |
||||
static fn (array $matches): string => '['.('' !== $matches[1] ? '^' : '').str_replace('\\-', '-', $matches[2]).']', |
||||
$regex |
||||
); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(normalizeGitignoreCharacterClass('\\[a\\-z\\]')); |
||||
var_dump(normalizeGitignoreCharacterClass('\\[\\!0\\-9\\]')); |
||||
var_dump(normalizeGitignoreCharacterClass('plain')); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(5) "[a-z]" |
||||
string(6) "[^0-9]" |
||||
string(5) "plain" |
||||
@ -0,0 +1,52 @@ |
||||
--TEST-- |
||||
Symfony Finder pattern: object method first-class callable with array_merge unpack |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyFinderDirNormalizer |
||||
{ |
||||
private array $dirs = []; |
||||
|
||||
public function in(array|string $dirs): self |
||||
{ |
||||
$resolvedDirs = []; |
||||
foreach ((array) $dirs as $dir) { |
||||
$glob = str_contains($dir, '*') ? ['src/', 'tests/'] : [$dir]; |
||||
$resolvedDirs[] = array_map($this->normalizeDir(...), $glob); |
||||
} |
||||
|
||||
$this->dirs = array_merge($this->dirs, ...$resolvedDirs); |
||||
|
||||
return $this; |
||||
} |
||||
|
||||
private function normalizeDir(string $dir): string |
||||
{ |
||||
return rtrim(str_replace('\\', '/', $dir), '/').'/'; |
||||
} |
||||
|
||||
public function getDirs(): array |
||||
{ |
||||
return $this->dirs; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$finder = (new SymfonyFinderDirNormalizer()) |
||||
->in('var/cache') |
||||
->in(['app/*', 'vendor/package']); |
||||
|
||||
var_dump($finder->getDirs()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(4) { |
||||
[0]=> |
||||
string(10) "var/cache/" |
||||
[1]=> |
||||
string(4) "src/" |
||||
[2]=> |
||||
string(6) "tests/" |
||||
[3]=> |
||||
string(15) "vendor/package/" |
||||
} |
||||
@ -0,0 +1,48 @@ |
||||
--TEST-- |
||||
Symfony Intl pattern: env fallback with filter_var and static coalesce cache |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyIntlEnvFlag |
||||
{ |
||||
private static ?bool $withUserAssigned = null; |
||||
|
||||
public static function reset(): void |
||||
{ |
||||
self::$withUserAssigned = null; |
||||
} |
||||
|
||||
public static function withUserAssigned(): bool |
||||
{ |
||||
return self::$withUserAssigned ??= filter_var( |
||||
$_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'] ?? $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED'] ?? getenv('SYMFONY_INTL_WITH_USER_ASSIGNED'), |
||||
FILTER_VALIDATE_BOOLEAN |
||||
); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
unset($_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'], $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED']); |
||||
putenv('SYMFONY_INTL_WITH_USER_ASSIGNED=1'); |
||||
var_dump(SymfonyIntlEnvFlag::withUserAssigned()); |
||||
|
||||
putenv('SYMFONY_INTL_WITH_USER_ASSIGNED=0'); |
||||
var_dump(SymfonyIntlEnvFlag::withUserAssigned()); |
||||
|
||||
SymfonyIntlEnvFlag::reset(); |
||||
$_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED'] = 'false'; |
||||
var_dump(SymfonyIntlEnvFlag::withUserAssigned()); |
||||
|
||||
SymfonyIntlEnvFlag::reset(); |
||||
$_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'] = 'true'; |
||||
var_dump(SymfonyIntlEnvFlag::withUserAssigned()); |
||||
|
||||
unset($_ENV['SYMFONY_INTL_WITH_USER_ASSIGNED'], $_SERVER['SYMFONY_INTL_WITH_USER_ASSIGNED']); |
||||
putenv('SYMFONY_INTL_WITH_USER_ASSIGNED'); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
bool(true) |
||||
bool(false) |
||||
bool(true) |
||||
@ -0,0 +1,64 @@ |
||||
--TEST-- |
||||
Symfony Ldap pattern: iterator_to_array without keys cached by coalesce assignment |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyLdapEntryCollection implements IteratorAggregate |
||||
{ |
||||
private ?array $entries = null; |
||||
public int $iterations = 0; |
||||
|
||||
public function __construct(private array $source) |
||||
{ |
||||
} |
||||
|
||||
public function getIterator(): Traversable |
||||
{ |
||||
++$this->iterations; |
||||
|
||||
return new ArrayIterator($this->source); |
||||
} |
||||
|
||||
public function toArray(): array |
||||
{ |
||||
return $this->entries ??= iterator_to_array($this->getIterator(), false); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$collection = new SymfonyLdapEntryCollection([ |
||||
'uid' => ['alice'], |
||||
'mail' => ['alice@example.com'], |
||||
]); |
||||
|
||||
var_dump($collection->toArray()); |
||||
var_dump($collection->toArray()); |
||||
var_dump($collection->iterations); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(5) "alice" |
||||
} |
||||
[1]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(17) "alice@example.com" |
||||
} |
||||
} |
||||
array(2) { |
||||
[0]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(5) "alice" |
||||
} |
||||
[1]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(17) "alice@example.com" |
||||
} |
||||
} |
||||
int(1) |
||||
@ -0,0 +1,61 @@ |
||||
--TEST-- |
||||
Symfony Process style env array union and commandline array_map(strval(...)) |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyProcessCommandCase |
||||
{ |
||||
private static array $executables = []; |
||||
|
||||
public function __construct( |
||||
private array|string $commandline, |
||||
private array $env = [], |
||||
) { |
||||
} |
||||
|
||||
public function normalize(array $runtimeEnv, array $defaultEnv): array |
||||
{ |
||||
$env = $runtimeEnv; |
||||
if ($this->env) { |
||||
$env += '\\' === DIRECTORY_SEPARATOR ? array_diff_ukey($this->env, $env, 'strcasecmp') : $this->env; |
||||
} |
||||
|
||||
$env += '\\' === DIRECTORY_SEPARATOR ? array_diff_ukey($defaultEnv, $env, 'strcasecmp') : $defaultEnv; |
||||
|
||||
if (is_array($commandline = $this->commandline)) { |
||||
$commandline = array_values(array_map(strval(...), $commandline)); |
||||
} |
||||
|
||||
if ('\\' === DIRECTORY_SEPARATOR && isset($commandline[0][0]) && strlen($commandline[0]) === strcspn($commandline[0], ':/\\')) { |
||||
$commandline[0] = (self::$executables[$commandline[0]] ??= $commandline[0]) ?? $commandline[0]; |
||||
} |
||||
|
||||
return [$env, $commandline]; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$process = new SymfonyProcessCommandCase(['php', '-r', 123], ['APP_ENV' => 'local', 'NEW_VAR' => 'yes']); |
||||
[$env, $commandline] = $process->normalize(['APP_ENV' => 'runtime'], ['PATH' => '/usr/bin', 'APP_ENV' => 'default']); |
||||
|
||||
var_dump($env); |
||||
var_dump($commandline); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
["APP_ENV"]=> |
||||
string(7) "runtime" |
||||
["NEW_VAR"]=> |
||||
string(3) "yes" |
||||
["PATH"]=> |
||||
string(8) "/usr/bin" |
||||
} |
||||
array(3) { |
||||
[0]=> |
||||
string(3) "php" |
||||
[1]=> |
||||
string(2) "-r" |
||||
[2]=> |
||||
string(3) "123" |
||||
} |
||||
@ -0,0 +1,33 @@ |
||||
--TEST-- |
||||
Symfony Semaphore pattern: negated coalesce assignment in condition |
||||
--FILE-- |
||||
<?php |
||||
class SymfonySemaphoreLike |
||||
{ |
||||
public function __construct(private ?float $ttlInSecond = null) |
||||
{ |
||||
} |
||||
|
||||
public function refresh(?float $ttlInSecond = null): string |
||||
{ |
||||
if (!$ttlInSecond ??= $this->ttlInSecond) { |
||||
return 'missing'; |
||||
} |
||||
|
||||
return 'ttl='.$ttlInSecond; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump((new SymfonySemaphoreLike())->refresh()); |
||||
var_dump((new SymfonySemaphoreLike(2.5))->refresh()); |
||||
var_dump((new SymfonySemaphoreLike(2.5))->refresh(1.25)); |
||||
var_dump((new SymfonySemaphoreLike(2.5))->refresh(0.0)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(7) "missing" |
||||
string(7) "ttl=2.5" |
||||
string(8) "ttl=1.25" |
||||
string(7) "missing" |
||||
@ -0,0 +1,57 @@ |
||||
--TEST-- |
||||
Symfony Serializer pattern: group contexts merged with unpack |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyAttributeContextMetadata |
||||
{ |
||||
private array $normalizationContexts = []; |
||||
|
||||
public function setNormalizationContextForGroups(array $context, array $groups = []): void |
||||
{ |
||||
if (!$groups) { |
||||
$this->normalizationContexts['*'] = $context; |
||||
} |
||||
|
||||
foreach ($groups as $group) { |
||||
$this->normalizationContexts[$group] = $context; |
||||
} |
||||
} |
||||
|
||||
public function getNormalizationContextForGroups(array $groups): array |
||||
{ |
||||
$contexts = []; |
||||
foreach ($groups as $group) { |
||||
$contexts[] = $this->normalizationContexts[$group] ?? []; |
||||
} |
||||
|
||||
return array_merge($this->normalizationContexts['*'] ?? [], ...$contexts); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$metadata = new SymfonyAttributeContextMetadata(); |
||||
$metadata->setNormalizationContextForGroups(['skip_null_values' => true, 'groups' => ['default']]); |
||||
$metadata->setNormalizationContextForGroups(['groups' => ['admin'], 'max_depth' => 2], ['admin']); |
||||
$metadata->setNormalizationContextForGroups(['ignored_attributes' => ['secret']], ['public']); |
||||
|
||||
var_dump($metadata->getNormalizationContextForGroups(['missing', 'admin', 'public'])); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(4) { |
||||
["skip_null_values"]=> |
||||
bool(true) |
||||
["groups"]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(5) "admin" |
||||
} |
||||
["max_depth"]=> |
||||
int(2) |
||||
["ignored_attributes"]=> |
||||
array(1) { |
||||
[0]=> |
||||
string(6) "secret" |
||||
} |
||||
} |
||||
@ -0,0 +1,43 @@ |
||||
--TEST-- |
||||
Symfony Validator Constraint pattern: serialize object vars with match(true) private key normalization |
||||
--XFAIL-- |
||||
AOT does not yet match private object array keys with NUL-prefixed class names in str_starts_with(). |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyConstraintLike |
||||
{ |
||||
public string $publicName = 'public'; |
||||
protected string $protectedName = 'protected'; |
||||
private string $privateName = 'private'; |
||||
|
||||
public function __serialize(): array |
||||
{ |
||||
$data = []; |
||||
$class = $this::class; |
||||
foreach ((array) $this as $k => $v) { |
||||
$data[match (true) { |
||||
'' === $k || "\0" !== $k[0] => $k, |
||||
str_starts_with($k, "\0*\0") => substr($k, 3), |
||||
str_starts_with($k, "\0{$class}\0") => substr($k, 2 + strlen($class)), |
||||
default => $k, |
||||
}] = $v; |
||||
} |
||||
|
||||
return $data; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump((new SymfonyConstraintLike())->__serialize()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(3) { |
||||
["publicName"]=> |
||||
string(6) "public" |
||||
["protectedName"]=> |
||||
string(9) "protected" |
||||
["privateName"]=> |
||||
string(7) "private" |
||||
} |
||||
@ -0,0 +1,21 @@ |
||||
--TEST-- |
||||
Symfony Validator pattern: array_map(trim(...)) filtered by static arrow callback |
||||
--FILE-- |
||||
<?php |
||||
class SymfonyWordCounter |
||||
{ |
||||
public static function countWords(array $words): int |
||||
{ |
||||
return count(array_filter(array_map(trim(...), $words), static fn ($word) => '' !== $word)); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
var_dump(SymfonyWordCounter::countWords([' one ', '', " \t ", 'two', ' three'])); |
||||
var_dump(SymfonyWordCounter::countWords([])); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(3) |
||||
int(0) |
||||
@ -0,0 +1,63 @@ |
||||
--TEST-- |
||||
Symfony WebLink style header parser with match(true), rel ??= and repeated attributes |
||||
--FILE-- |
||||
<?php |
||||
function symfony_parse_link_attributes(string $attributesString): array |
||||
{ |
||||
$attributes = []; |
||||
$rels = null; |
||||
|
||||
if (preg_match_all('/;\s*([a-zA-Z0-9_-]+)(?:=(?:"((?:\\\"|[^"])*)"|([^;,\s]+)))?/', $attributesString, $attributeMatches, PREG_SET_ORDER)) { |
||||
foreach ($attributeMatches as $pm) { |
||||
$key = $pm[1]; |
||||
$value = match (true) { |
||||
($pm[2] ?? '') !== '' => stripcslashes($pm[2]), |
||||
($pm[3] ?? '') !== '' => $pm[3], |
||||
default => true, |
||||
}; |
||||
|
||||
if ('rel' === $key) { |
||||
$rels ??= true === $value ? [] : preg_split('/\s+/', $value, 0, PREG_SPLIT_NO_EMPTY); |
||||
} elseif (is_array($attributes[$key] ?? null)) { |
||||
$attributes[$key][] = $value; |
||||
} elseif (isset($attributes[$key])) { |
||||
$attributes[$key] = [$attributes[$key], $value]; |
||||
} else { |
||||
$attributes[$key] = $value; |
||||
} |
||||
} |
||||
} |
||||
|
||||
return [$rels, $attributes]; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
[$rels, $attributes] = symfony_parse_link_attributes('; rel="preload module"; rel=ignored; as=script; title="a \"quoted\" title"; hreflang=en; hreflang=fr; disabled'); |
||||
|
||||
var_dump($rels); |
||||
var_dump($attributes); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
array(2) { |
||||
[0]=> |
||||
string(7) "preload" |
||||
[1]=> |
||||
string(6) "module" |
||||
} |
||||
array(4) { |
||||
["as"]=> |
||||
string(6) "script" |
||||
["title"]=> |
||||
string(16) "a "quoted" title" |
||||
["hreflang"]=> |
||||
array(2) { |
||||
[0]=> |
||||
string(2) "en" |
||||
[1]=> |
||||
string(2) "fr" |
||||
} |
||||
["disabled"]=> |
||||
bool(true) |
||||
} |
||||
Loading…
Reference in new issue