parent
035b1b09d7
commit
f0e025dad4
21 changed files with 1038 additions and 40 deletions
@ -0,0 +1,69 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Serializer pattern: match true used for metadata side effects |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class GroupsAttribute |
||||||
|
{ |
||||||
|
public function __construct(public array $groups) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class IgnoreAttribute |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
class MaxDepthAttribute |
||||||
|
{ |
||||||
|
public function __construct(public int $maxDepth) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Metadata |
||||||
|
{ |
||||||
|
public array $groups = []; |
||||||
|
public bool $ignored = false; |
||||||
|
public ?int $maxDepth = null; |
||||||
|
|
||||||
|
public function addGroup(string $group): void |
||||||
|
{ |
||||||
|
$this->groups[] = $group; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function applyAttribute(object $attribute, Metadata $metadata): void |
||||||
|
{ |
||||||
|
match (true) { |
||||||
|
$attribute instanceof MaxDepthAttribute => $metadata->maxDepth = $attribute->maxDepth, |
||||||
|
$attribute instanceof IgnoreAttribute => $metadata->ignored = true, |
||||||
|
$attribute instanceof GroupsAttribute => array_map($metadata->addGroup(...), $attribute->groups), |
||||||
|
default => null, |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$metadata = new Metadata(); |
||||||
|
applyAttribute(new GroupsAttribute(['read', 'write']), $metadata); |
||||||
|
applyAttribute(new MaxDepthAttribute(3), $metadata); |
||||||
|
applyAttribute(new IgnoreAttribute(), $metadata); |
||||||
|
|
||||||
|
var_dump($metadata); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
object(Metadata)#1 (3) { |
||||||
|
["groups"]=> |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(4) "read" |
||||||
|
[1]=> |
||||||
|
string(5) "write" |
||||||
|
} |
||||||
|
["ignored"]=> |
||||||
|
bool(true) |
||||||
|
["maxDepth"]=> |
||||||
|
int(3) |
||||||
|
} |
||||||
@ -0,0 +1,68 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony DI pattern: filtered attribute nullsafe property with coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class TargetAttribute |
||||||
|
{ |
||||||
|
public function __construct(public ?string $name) |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class ServiceReference |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
private string $name, |
||||||
|
private array $attributes, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public function getAttributes(): array |
||||||
|
{ |
||||||
|
return $this->attributes; |
||||||
|
} |
||||||
|
|
||||||
|
public function getName(): ?string |
||||||
|
{ |
||||||
|
return $this->name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function resolveTarget(ServiceReference $reference): array |
||||||
|
{ |
||||||
|
$name = $target = (array_filter($reference->getAttributes(), static fn ($a) => $a instanceof TargetAttribute)[0] ?? null)?->name; |
||||||
|
|
||||||
|
if (null !== $name ??= $reference->getName()) { |
||||||
|
return [$name, $target]; |
||||||
|
} |
||||||
|
|
||||||
|
return ['missing', $target]; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(resolveTarget(new ServiceReference('fallback', [new TargetAttribute('explicit')]))); |
||||||
|
var_dump(resolveTarget(new ServiceReference('fallback', []))); |
||||||
|
var_dump(resolveTarget(new ServiceReference('fallback', [new stdClass(), new TargetAttribute(null)]))); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(8) "explicit" |
||||||
|
[1]=> |
||||||
|
string(8) "explicit" |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(8) "fallback" |
||||||
|
[1]=> |
||||||
|
NULL |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(8) "fallback" |
||||||
|
[1]=> |
||||||
|
NULL |
||||||
|
} |
||||||
@ -0,0 +1,78 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Console pattern: nullsafe attribute values with coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class CommandAttribute |
||||||
|
{ |
||||||
|
public function __construct( |
||||||
|
public ?string $description = null, |
||||||
|
public ?string $help = null, |
||||||
|
public ?array $usages = null, |
||||||
|
) { |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function normalizeCommandMetadata(?CommandAttribute $attribute, array $tag): array |
||||||
|
{ |
||||||
|
$description = null; |
||||||
|
$help = null; |
||||||
|
$usages = null; |
||||||
|
|
||||||
|
$description ??= $tag['description'] ?? null; |
||||||
|
$help ??= $tag['help'] ?? null; |
||||||
|
$usages ??= $tag['usages'] ?? null; |
||||||
|
|
||||||
|
if ($help ??= $attribute?->help) { |
||||||
|
$help = trim($help); |
||||||
|
} |
||||||
|
if ($usages ??= $attribute?->usages) { |
||||||
|
$usages = array_values($usages); |
||||||
|
} |
||||||
|
if ($description ??= $attribute?->description) { |
||||||
|
$description = strtoupper($description); |
||||||
|
} |
||||||
|
|
||||||
|
return [$description, $help, $usages]; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(normalizeCommandMetadata(new CommandAttribute('from attribute', ' help ', ['a', 'b']), [])); |
||||||
|
var_dump(normalizeCommandMetadata(new CommandAttribute('ignored', 'ignored', ['x']), ['description' => 'from tag', 'help' => 'tag help'])); |
||||||
|
var_dump(normalizeCommandMetadata(null, [])); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(3) { |
||||||
|
[0]=> |
||||||
|
string(14) "FROM ATTRIBUTE" |
||||||
|
[1]=> |
||||||
|
string(4) "help" |
||||||
|
[2]=> |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(1) "a" |
||||||
|
[1]=> |
||||||
|
string(1) "b" |
||||||
|
} |
||||||
|
} |
||||||
|
array(3) { |
||||||
|
[0]=> |
||||||
|
string(8) "FROM TAG" |
||||||
|
[1]=> |
||||||
|
string(8) "tag help" |
||||||
|
[2]=> |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(1) "x" |
||||||
|
} |
||||||
|
} |
||||||
|
array(3) { |
||||||
|
[0]=> |
||||||
|
NULL |
||||||
|
[1]=> |
||||||
|
NULL |
||||||
|
[2]=> |
||||||
|
NULL |
||||||
|
} |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Console pattern: closure caches iterable values with coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function valueCallback(iterable $values): Closure |
||||||
|
{ |
||||||
|
$valueCache = null; |
||||||
|
|
||||||
|
return static function () use (&$valueCache, $values): array { |
||||||
|
return $valueCache ??= iterator_to_array($values, false); |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$callback = valueCallback(new ArrayIterator(['a' => 'A', 'b' => 'B'])); |
||||||
|
|
||||||
|
var_dump($callback()); |
||||||
|
var_dump($callback()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(1) "A" |
||||||
|
[1]=> |
||||||
|
string(1) "B" |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(1) "A" |
||||||
|
[1]=> |
||||||
|
string(1) "B" |
||||||
|
} |
||||||
@ -0,0 +1,31 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony DI pattern: normalize named arguments with array_combine and preg_replace |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function normalizeArguments(array $arguments): array |
||||||
|
{ |
||||||
|
return array_combine( |
||||||
|
array_map(static fn ($key) => preg_replace('/^.*\$/', '', $key), array_keys($arguments)), |
||||||
|
$arguments, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(normalizeArguments([ |
||||||
|
'App\Service $mailer' => 'smtp', |
||||||
|
'$logger' => 'file', |
||||||
|
'plain' => 'value', |
||||||
|
])); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(3) { |
||||||
|
["mailer"]=> |
||||||
|
string(4) "smtp" |
||||||
|
["logger"]=> |
||||||
|
string(4) "file" |
||||||
|
["plain"]=> |
||||||
|
string(5) "value" |
||||||
|
} |
||||||
@ -0,0 +1,38 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony HttpKernel pattern: controller reflector match with unpacked callable |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class DemoController |
||||||
|
{ |
||||||
|
public function action(): void |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public static function staticAction(): void |
||||||
|
{ |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function reflectorOf(callable|string $controller): ReflectionFunctionAbstract |
||||||
|
{ |
||||||
|
return match (true) { |
||||||
|
is_array($controller) && method_exists(...$controller) => new ReflectionMethod(...$controller), |
||||||
|
is_string($controller) && str_contains($controller, '::') => new ReflectionMethod(...explode('::', $controller, 2)), |
||||||
|
default => new ReflectionFunction($controller(...)), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$object = new DemoController(); |
||||||
|
|
||||||
|
var_dump(reflectorOf([$object, 'action'])->getName()); |
||||||
|
var_dump(reflectorOf(DemoController::class.'::staticAction')->getName()); |
||||||
|
var_dump(reflectorOf(static fn () => null)->isClosure()); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(6) "action" |
||||||
|
string(12) "staticAction" |
||||||
|
bool(true) |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony HttpFoundation pattern: accept header specificity with explode defaults and match |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function specificity(string $rangeValue, int $paramCount, bool $isQueryMedia, bool $isRangeMedia): int |
||||||
|
{ |
||||||
|
if (!$isQueryMedia && !$isRangeMedia) { |
||||||
|
return ('*' !== $rangeValue ? 2000 : 1000) + $paramCount; |
||||||
|
} |
||||||
|
|
||||||
|
[$rangeType, $rangeSubtype] = explode('/', $rangeValue, 2) + [1 => '*']; |
||||||
|
|
||||||
|
$specificity = match (true) { |
||||||
|
'*' !== $rangeSubtype => 3000, |
||||||
|
'*' !== $rangeType => 2000, |
||||||
|
default => 1000, |
||||||
|
}; |
||||||
|
|
||||||
|
return $specificity + $paramCount; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(specificity('text/plain', 2, true, true)); |
||||||
|
var_dump(specificity('text/*', 1, true, true)); |
||||||
|
var_dump(specificity('*', 0, true, true)); |
||||||
|
var_dump(specificity('application/json', 3, false, false)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(3002) |
||||||
|
int(2001) |
||||||
|
int(1000) |
||||||
|
int(2003) |
||||||
@ -0,0 +1,49 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony HttpFoundation pattern: null default new ArrayObject and new static JSON factory |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class JsonResponseLike |
||||||
|
{ |
||||||
|
public mixed $data; |
||||||
|
public bool $json; |
||||||
|
|
||||||
|
public function __construct(mixed $data = null, bool $json = false) |
||||||
|
{ |
||||||
|
if ($json && !is_string($data) && !is_numeric($data) && !$data instanceof Stringable) { |
||||||
|
throw new TypeError(sprintf('bad data "%s"', get_debug_type($data))); |
||||||
|
} |
||||||
|
|
||||||
|
$data ??= new ArrayObject(); |
||||||
|
$this->data = $data; |
||||||
|
$this->json = $json; |
||||||
|
} |
||||||
|
|
||||||
|
public static function fromJsonString(string $data): static |
||||||
|
{ |
||||||
|
return new static($data, true); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class CustomJsonResponseLike extends JsonResponseLike |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$response = new JsonResponseLike(); |
||||||
|
var_dump($response->data instanceof ArrayObject); |
||||||
|
var_dump($response->json); |
||||||
|
|
||||||
|
$custom = CustomJsonResponseLike::fromJsonString('{"ok":true}'); |
||||||
|
var_dump($custom::class); |
||||||
|
var_dump($custom->data); |
||||||
|
var_dump($custom->json); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
bool(true) |
||||||
|
bool(false) |
||||||
|
string(22) "CustomJsonResponseLike" |
||||||
|
string(11) "{"ok":true}" |
||||||
|
bool(true) |
||||||
@ -0,0 +1,70 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Mailer style self::method(...) callback in array_map |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class Address |
||||||
|
{ |
||||||
|
public function __construct(private string $address, private string $name = '') {} |
||||||
|
|
||||||
|
public function getAddress(): string |
||||||
|
{ |
||||||
|
return $this->address; |
||||||
|
} |
||||||
|
|
||||||
|
public function getName(): string |
||||||
|
{ |
||||||
|
return $this->name; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class PayloadBuilder |
||||||
|
{ |
||||||
|
public static function build(array $to, array $cc, array $bcc): array |
||||||
|
{ |
||||||
|
return [ |
||||||
|
'to' => array_map(self::encodeEmail(...), $to), |
||||||
|
'cc' => array_map(self::encodeEmail(...), $cc), |
||||||
|
'bcc' => array_map(self::encodeEmail(...), $bcc), |
||||||
|
]; |
||||||
|
} |
||||||
|
|
||||||
|
private static function encodeEmail(Address $address): array |
||||||
|
{ |
||||||
|
return array_filter(['email' => $address->getAddress(), 'name' => $address->getName()]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$payload = PayloadBuilder::build( |
||||||
|
[new Address('a@example.com', 'Alice')], |
||||||
|
[new Address('b@example.com')], |
||||||
|
[] |
||||||
|
); |
||||||
|
var_dump($payload); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(3) { |
||||||
|
["to"]=> |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
array(2) { |
||||||
|
["email"]=> |
||||||
|
string(13) "a@example.com" |
||||||
|
["name"]=> |
||||||
|
string(5) "Alice" |
||||||
|
} |
||||||
|
} |
||||||
|
["cc"]=> |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
array(1) { |
||||||
|
["email"]=> |
||||||
|
string(13) "b@example.com" |
||||||
|
} |
||||||
|
} |
||||||
|
["bcc"]=> |
||||||
|
array(0) { |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony HttpKernel pattern: match true with assignment inside condition |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class Metadata |
||||||
|
{ |
||||||
|
public function __construct(private string $value) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function evaluate(): string |
||||||
|
{ |
||||||
|
return 'metadata:'.$this->value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class EventWithMetadata |
||||||
|
{ |
||||||
|
public ?Metadata $controllerMetadata = null; |
||||||
|
} |
||||||
|
|
||||||
|
function evaluate(EventWithMetadata $event): string |
||||||
|
{ |
||||||
|
return match (true) { |
||||||
|
($m = $event->controllerMetadata ?? null) instanceof Metadata => $m->evaluate(), |
||||||
|
default => 'none', |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$event = new EventWithMetadata(); |
||||||
|
var_dump(evaluate($event)); |
||||||
|
|
||||||
|
$event->controllerMetadata = new Metadata('ok'); |
||||||
|
var_dump(evaluate($event)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(4) "none" |
||||||
|
string(11) "metadata:ok" |
||||||
@ -0,0 +1,33 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Config pattern: match true formats scalar values or throws by debug type |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function displayValue(mixed $value): string|int |
||||||
|
{ |
||||||
|
return match (true) { |
||||||
|
is_int($value) => $value, |
||||||
|
is_string($value) => sprintf('"%s"', $value), |
||||||
|
is_bool($value) => throw new InvalidArgumentException(sprintf('unsupported "%s"', get_debug_type($value))), |
||||||
|
default => sprintf('of type "%s"', get_debug_type($value)), |
||||||
|
}; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(displayValue(7)); |
||||||
|
var_dump(displayValue('name')); |
||||||
|
var_dump(displayValue([])); |
||||||
|
|
||||||
|
try { |
||||||
|
displayValue(false); |
||||||
|
} catch (InvalidArgumentException $e) { |
||||||
|
echo $e->getMessage(), "\n"; |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
int(7) |
||||||
|
string(6) ""name"" |
||||||
|
string(15) "of type "array"" |
||||||
|
unsupported "bool" |
||||||
@ -0,0 +1,91 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony style Envelope clone with variadic stamps and dynamic stamp removal |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
interface StampInterface {} |
||||||
|
interface RemovableStampInterface extends StampInterface {} |
||||||
|
|
||||||
|
class FirstStamp implements RemovableStampInterface |
||||||
|
{ |
||||||
|
public function __construct(public string $name) {} |
||||||
|
} |
||||||
|
|
||||||
|
class SecondStamp implements StampInterface |
||||||
|
{ |
||||||
|
public function __construct(public string $name) {} |
||||||
|
} |
||||||
|
|
||||||
|
final class Envelope |
||||||
|
{ |
||||||
|
private array $stamps = []; |
||||||
|
|
||||||
|
public function __construct(private object $message, array $stamps = []) |
||||||
|
{ |
||||||
|
foreach ($stamps as $stamp) { |
||||||
|
$this->stamps[$stamp::class][] = $stamp; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
public static function wrap(object $message, array $stamps = []): self |
||||||
|
{ |
||||||
|
$envelope = $message instanceof self ? $message : new self($message); |
||||||
|
|
||||||
|
return $envelope->with(...$stamps); |
||||||
|
} |
||||||
|
|
||||||
|
public function with(StampInterface ...$stamps): static |
||||||
|
{ |
||||||
|
$cloned = clone $this; |
||||||
|
|
||||||
|
foreach ($stamps as $stamp) { |
||||||
|
$cloned->stamps[$stamp::class][] = $stamp; |
||||||
|
} |
||||||
|
|
||||||
|
return $cloned; |
||||||
|
} |
||||||
|
|
||||||
|
public function withoutStampsOfType(string $type): self |
||||||
|
{ |
||||||
|
$cloned = clone $this; |
||||||
|
|
||||||
|
foreach ($cloned->stamps as $class => $stamps) { |
||||||
|
if ($class === $type || is_subclass_of($class, $type)) { |
||||||
|
unset($cloned->stamps[$class]); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return $cloned; |
||||||
|
} |
||||||
|
|
||||||
|
public function all(?string $stampFqcn = null): array |
||||||
|
{ |
||||||
|
if (null !== $stampFqcn) { |
||||||
|
return $this->stamps[$stampFqcn] ?? []; |
||||||
|
} |
||||||
|
|
||||||
|
return $this->stamps; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$envelope = Envelope::wrap(new stdClass(), [new FirstStamp('a'), new SecondStamp('b')]); |
||||||
|
$filtered = $envelope->withoutStampsOfType(RemovableStampInterface::class); |
||||||
|
var_dump(array_keys($envelope->all())); |
||||||
|
var_dump(array_keys($filtered->all())); |
||||||
|
var_dump($filtered->all(FirstStamp::class)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(10) "FirstStamp" |
||||||
|
[1]=> |
||||||
|
string(11) "SecondStamp" |
||||||
|
} |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(11) "SecondStamp" |
||||||
|
} |
||||||
|
array(0) { |
||||||
|
} |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Messenger style nullsafe chain with coalesce throw |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class MissingStampException extends Exception {} |
||||||
|
|
||||||
|
class TransportStamp |
||||||
|
{ |
||||||
|
public function __construct(private string $id) {} |
||||||
|
|
||||||
|
public function getId(): string |
||||||
|
{ |
||||||
|
return $this->id; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
class Envelope |
||||||
|
{ |
||||||
|
public function __construct(private ?TransportStamp $stamp) {} |
||||||
|
|
||||||
|
public function last(string $class): ?object |
||||||
|
{ |
||||||
|
return $this->stamp instanceof $class ? $this->stamp : null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function stampId(Envelope $envelope): string |
||||||
|
{ |
||||||
|
return $envelope->last(TransportStamp::class)?->getId() ?? throw new MissingStampException('No stamp found.'); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(stampId(new Envelope(new TransportStamp('abc')))); |
||||||
|
try { |
||||||
|
stampId(new Envelope(null)); |
||||||
|
} catch (MissingStampException $e) { |
||||||
|
var_dump($e->getMessage()); |
||||||
|
} |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(3) "abc" |
||||||
|
string(15) "No stamp found." |
||||||
@ -0,0 +1,19 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony DI pattern: indent generated code lines with explode array_map implode |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
function indentCode(string $code): string |
||||||
|
{ |
||||||
|
return implode("\n", array_map(static fn ($line) => $line ? ' '.$line : $line, explode("\n", $code))); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(indentCode("first\n\nsecond")); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(21) " first |
||||||
|
|
||||||
|
second" |
||||||
@ -0,0 +1,48 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony style RememberMeDetails new static(...array after unset) |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
class AuthenticationException extends Exception {} |
||||||
|
|
||||||
|
class RememberMeDetails |
||||||
|
{ |
||||||
|
public const COOKIE_DELIMITER = ':'; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
public string $userIdentifier, |
||||||
|
public int $expires, |
||||||
|
public string $value, |
||||||
|
) { |
||||||
|
var_dump(get_debug_type($userIdentifier), get_debug_type($expires), get_debug_type($value)); |
||||||
|
} |
||||||
|
|
||||||
|
public static function fromRawCookie(string $rawCookie): self |
||||||
|
{ |
||||||
|
if (!str_contains($rawCookie, self::COOKIE_DELIMITER)) { |
||||||
|
$rawCookie = 'prefix'.self::COOKIE_DELIMITER.$rawCookie; |
||||||
|
} |
||||||
|
|
||||||
|
$cookieParts = explode(self::COOKIE_DELIMITER, $rawCookie, 4); |
||||||
|
|
||||||
|
if (4 !== count($cookieParts)) { |
||||||
|
throw new AuthenticationException('The cookie contains invalid data.'); |
||||||
|
} |
||||||
|
|
||||||
|
unset($cookieParts[0]); |
||||||
|
|
||||||
|
return new static(...$cookieParts); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$raw = 'prefix:user-name:12345:series-token'; |
||||||
|
$details = RememberMeDetails::fromRawCookie($raw); |
||||||
|
var_dump($details::class); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(6) "string" |
||||||
|
string(3) "int" |
||||||
|
string(6) "string" |
||||||
|
string(17) "RememberMeDetails" |
||||||
@ -0,0 +1,36 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Routing pattern: provider returns callable invoked with unpacked args |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class FunctionProvider |
||||||
|
{ |
||||||
|
/** @var array<string, callable> */ |
||||||
|
private array $functions = []; |
||||||
|
|
||||||
|
public function set(string $name, callable $function): void |
||||||
|
{ |
||||||
|
$this->functions[$name] = $function; |
||||||
|
} |
||||||
|
|
||||||
|
public function get(string $name): callable |
||||||
|
{ |
||||||
|
return $this->functions[$name]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function evaluate(FunctionProvider $provider, string $function, array $args): string |
||||||
|
{ |
||||||
|
return $provider->get($function)(...$args); |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$provider = new FunctionProvider(); |
||||||
|
$provider->set('format', static fn (string $prefix, string $value): string => $prefix.':'.strtoupper($value)); |
||||||
|
|
||||||
|
var_dump(evaluate($provider, 'format', ['name', 'symfony'])); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
string(12) "name:SYMFONY" |
||||||
@ -0,0 +1,59 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Routing pattern: extra parameters diff and recursive object caster |
||||||
|
--SKIPIF-- |
||||||
|
<?php exit('skip closures do not support reference parameters'); ?> |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class Slug |
||||||
|
{ |
||||||
|
public function __construct(private string $value) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function __toString(): string |
||||||
|
{ |
||||||
|
return $this->value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function normalizeExtra(array $parameters, array $variables, array $defaults, array $queryParameters): array |
||||||
|
{ |
||||||
|
$extra = array_udiff_assoc(array_diff_key($parameters, $variables), $defaults, static fn ($a, $b) => $a == $b ? 0 : 1); |
||||||
|
$extra = array_replace($extra, $queryParameters); |
||||||
|
|
||||||
|
array_walk_recursive($extra, $caster = static function (&$value) use (&$caster): void { |
||||||
|
if (is_object($value)) { |
||||||
|
$value = (string) $value; |
||||||
|
} elseif (is_array($value)) { |
||||||
|
array_walk_recursive($value, $caster); |
||||||
|
} |
||||||
|
}); |
||||||
|
|
||||||
|
return $extra; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(normalizeExtra( |
||||||
|
['id' => 10, 'page' => 1, 'slug' => new Slug('hello'), 'tags' => [new Slug('a'), new Slug('b')]], |
||||||
|
['id' => true], |
||||||
|
['page' => 1, 'slug' => 'old'], |
||||||
|
['q' => new Slug('search')] |
||||||
|
)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(3) { |
||||||
|
["slug"]=> |
||||||
|
string(5) "hello" |
||||||
|
["tags"]=> |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(1) "a" |
||||||
|
[1]=> |
||||||
|
string(1) "b" |
||||||
|
} |
||||||
|
["q"]=> |
||||||
|
string(6) "search" |
||||||
|
} |
||||||
@ -0,0 +1,42 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Routing pattern: null comparison with route coalesce assignment |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class RouteStore |
||||||
|
{ |
||||||
|
public function __construct(private array $routes) |
||||||
|
{ |
||||||
|
} |
||||||
|
|
||||||
|
public function get(string $name): ?string |
||||||
|
{ |
||||||
|
echo "lookup:$name\n"; |
||||||
|
return $this->routes[$name] ?? null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function generate(RouteStore $routes, string $name): string |
||||||
|
{ |
||||||
|
$route = null; |
||||||
|
|
||||||
|
if (null === $route ??= $routes->get($name)) { |
||||||
|
return 'missing'; |
||||||
|
} |
||||||
|
|
||||||
|
return 'route:'.$route; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
$routes = new RouteStore(['home' => '/']); |
||||||
|
|
||||||
|
var_dump(generate($routes, 'home')); |
||||||
|
var_dump(generate($routes, 'missing')); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
lookup:home |
||||||
|
string(7) "route:/" |
||||||
|
lookup:missing |
||||||
|
string(7) "missing" |
||||||
@ -0,0 +1,44 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony style dynamic instanceof against pipe-separated type string |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
interface UserInterface {} |
||||||
|
class AdminUser implements UserInterface {} |
||||||
|
class GuestUser {} |
||||||
|
|
||||||
|
function resolveUser(object $user, ?string $type): array |
||||||
|
{ |
||||||
|
if (null === $type || $user instanceof ($type)) { |
||||||
|
return [$user::class]; |
||||||
|
} |
||||||
|
|
||||||
|
$types = explode('|', $type); |
||||||
|
foreach ($types as $candidate) { |
||||||
|
if ($user instanceof $candidate) { |
||||||
|
return [$user::class, $candidate]; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
return []; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(resolveUser(new AdminUser(), UserInterface::class)); |
||||||
|
var_dump(resolveUser(new AdminUser(), GuestUser::class.'|'.UserInterface::class)); |
||||||
|
var_dump(resolveUser(new GuestUser(), UserInterface::class)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
array(1) { |
||||||
|
[0]=> |
||||||
|
string(9) "AdminUser" |
||||||
|
} |
||||||
|
array(2) { |
||||||
|
[0]=> |
||||||
|
string(9) "AdminUser" |
||||||
|
[1]=> |
||||||
|
string(13) "UserInterface" |
||||||
|
} |
||||||
|
array(0) { |
||||||
|
} |
||||||
@ -0,0 +1,35 @@ |
|||||||
|
--TEST-- |
||||||
|
Symfony Serializer pattern: static local coalesce assignment with ternary object cache |
||||||
|
--FILE-- |
||||||
|
<?php |
||||||
|
|
||||||
|
class LocalResolver |
||||||
|
{ |
||||||
|
public function resolve(string $value): string |
||||||
|
{ |
||||||
|
return 'resolved:'.$value; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
function resolveWhenAvailable(?string $value, bool $enabled): ?string |
||||||
|
{ |
||||||
|
static $resolver; |
||||||
|
|
||||||
|
if (null !== $value && $resolver ??= $enabled && class_exists(LocalResolver::class) ? new LocalResolver() : false) { |
||||||
|
return $resolver->resolve($value); |
||||||
|
} |
||||||
|
|
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
function main(): void |
||||||
|
{ |
||||||
|
var_dump(resolveWhenAvailable(null, true)); |
||||||
|
var_dump(resolveWhenAvailable('first', true)); |
||||||
|
var_dump(resolveWhenAvailable('second', false)); |
||||||
|
} |
||||||
|
?> |
||||||
|
--EXPECT-- |
||||||
|
NULL |
||||||
|
string(14) "resolved:first" |
||||||
|
string(15) "resolved:second" |
||||||
Loading…
Reference in new issue