修复foreach遍历动态表达式时的类型分发,正确处理IteratorAggregate嵌套及无效返回值检测pull/14/head
parent
6baaa9a221
commit
f22499bdf9
15 changed files with 627 additions and 20 deletions
@ -0,0 +1,22 @@ |
||||
--TEST-- |
||||
foreach by reference supports dynamically evaluated arrays |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function values(): array |
||||
{ |
||||
return [1, 2, 3]; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
foreach (values() as &$value) { |
||||
$value *= 2; |
||||
var_dump($value); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(2) |
||||
int(4) |
||||
int(6) |
||||
@ -0,0 +1,55 @@ |
||||
--TEST-- |
||||
foreach rejects invalid IteratorAggregate getIterator return values |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class ArrayReturningAggregate implements IteratorAggregate |
||||
{ |
||||
public function getIterator() |
||||
{ |
||||
return ['bad']; |
||||
} |
||||
} |
||||
|
||||
final class ObjectReturningAggregate implements IteratorAggregate |
||||
{ |
||||
public function getIterator() |
||||
{ |
||||
return (object) ['bad' => true]; |
||||
} |
||||
} |
||||
|
||||
final class NestedInvalidAggregate implements IteratorAggregate |
||||
{ |
||||
public function getIterator(): Traversable |
||||
{ |
||||
return new ObjectReturningAggregate(); |
||||
} |
||||
} |
||||
|
||||
function check(object $iterable): void |
||||
{ |
||||
try { |
||||
foreach ($iterable as $value) { |
||||
var_dump($value); |
||||
} |
||||
} catch (Throwable $e) { |
||||
var_dump($e instanceof Exception); |
||||
var_dump(str_contains($e->getMessage(), 'must be traversable or implement interface Iterator')); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
check(new ArrayReturningAggregate()); |
||||
check(new ObjectReturningAggregate()); |
||||
check(new NestedInvalidAggregate()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
bool(true) |
||||
bool(true) |
||||
bool(true) |
||||
bool(true) |
||||
bool(true) |
||||
bool(true) |
||||
@ -0,0 +1,102 @@ |
||||
--TEST-- |
||||
foreach supports PHP iterable object interfaces |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class NumberIterator implements Iterator |
||||
{ |
||||
private int $pos = 0; |
||||
|
||||
public function __construct(private array $items) |
||||
{ |
||||
} |
||||
|
||||
public function rewind(): void |
||||
{ |
||||
$this->pos = 0; |
||||
} |
||||
|
||||
public function current(): mixed |
||||
{ |
||||
return array_values($this->items)[$this->pos]; |
||||
} |
||||
|
||||
public function key(): mixed |
||||
{ |
||||
return array_keys($this->items)[$this->pos]; |
||||
} |
||||
|
||||
public function next(): void |
||||
{ |
||||
++$this->pos; |
||||
} |
||||
|
||||
public function valid(): bool |
||||
{ |
||||
return $this->pos < count($this->items); |
||||
} |
||||
} |
||||
|
||||
final class ArrayAggregate implements IteratorAggregate |
||||
{ |
||||
public function __construct(private array $items) |
||||
{ |
||||
} |
||||
|
||||
public function getIterator(): Traversable |
||||
{ |
||||
return new ArrayIterator($this->items); |
||||
} |
||||
} |
||||
|
||||
final class ArrayObjectAggregate implements IteratorAggregate |
||||
{ |
||||
public function __construct(private array $items) |
||||
{ |
||||
} |
||||
|
||||
public function getIterator(): Traversable |
||||
{ |
||||
return new ArrayObject($this->items); |
||||
} |
||||
} |
||||
|
||||
final class TraversableProvider |
||||
{ |
||||
public function getIterator(): Traversable |
||||
{ |
||||
return new ArrayIterator(['m' => 'method']); |
||||
} |
||||
} |
||||
|
||||
function dump_iterable(iterable $items): void |
||||
{ |
||||
foreach ($items as $key => $value) { |
||||
var_dump($key.':'.$value); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
dump_iterable(['a' => 'array']); |
||||
dump_iterable(new NumberIterator(['i' => 'iterator'])); |
||||
dump_iterable(new ArrayAggregate(['g' => 'aggregate'])); |
||||
dump_iterable(new ArrayObjectAggregate(['o' => 'arrayobject'])); |
||||
|
||||
$provider = new TraversableProvider(); |
||||
foreach ($provider->getIterator() as $key => $value) { |
||||
var_dump($key.':'.$value); |
||||
} |
||||
|
||||
foreach ((object) ['p' => 'property'] as $key => $value) { |
||||
var_dump($key.':'.$value); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(7) "a:array" |
||||
string(10) "i:iterator" |
||||
string(11) "g:aggregate" |
||||
string(13) "o:arrayobject" |
||||
string(8) "m:method" |
||||
string(10) "p:property" |
||||
@ -0,0 +1,25 @@ |
||||
--TEST-- |
||||
foreach supports dynamic method calls returning arrays |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class ArrayProvider |
||||
{ |
||||
public function values(): array |
||||
{ |
||||
return ['a' => 1, 'b' => 2]; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$provider = new ArrayProvider(); |
||||
|
||||
foreach ($provider->values() as $key => $value) { |
||||
var_dump($key.':'.$value); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(3) "a:1" |
||||
string(3) "b:2" |
||||
@ -0,0 +1,64 @@ |
||||
--TEST-- |
||||
count: Countable objects use Countable::count() |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class RouteBag implements Countable |
||||
{ |
||||
public function __construct(private array $routes) |
||||
{ |
||||
} |
||||
|
||||
public function count(): int |
||||
{ |
||||
echo "RouteBag::count\n"; |
||||
return count($this->routes); |
||||
} |
||||
} |
||||
|
||||
function count_mixed(mixed $value): int |
||||
{ |
||||
return count($value); |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$bag = new RouteBag(['home', 'about', 'contact']); |
||||
var_dump(count($bag)); |
||||
var_dump(count($bag, COUNT_RECURSIVE)); |
||||
var_dump(count_mixed($bag)); |
||||
|
||||
$anonymous = new class([1, 2, 3, 4]) implements Countable { |
||||
public function __construct(private array $items) |
||||
{ |
||||
} |
||||
|
||||
public function count(): int |
||||
{ |
||||
echo "anonymous::count\n"; |
||||
return count($this->items); |
||||
} |
||||
}; |
||||
var_dump(count($anonymous)); |
||||
|
||||
$arrayObject = new ArrayObject(['x', 'y']); |
||||
var_dump(count($arrayObject)); |
||||
|
||||
try { |
||||
count(new stdClass()); |
||||
} catch (TypeError $e) { |
||||
echo $e->getMessage(), "\n"; |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
RouteBag::count |
||||
int(3) |
||||
RouteBag::count |
||||
int(3) |
||||
RouteBag::count |
||||
int(3) |
||||
anonymous::count |
||||
int(4) |
||||
int(2) |
||||
count(): Argument #1 ($value) must be of type Countable|array |
||||
@ -0,0 +1,25 @@ |
||||
--TEST-- |
||||
Symfony pattern: count() dispatches to Countable object |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class CountableRoutes implements Countable |
||||
{ |
||||
public function __construct(private array $routes) |
||||
{ |
||||
} |
||||
|
||||
public function count(): int |
||||
{ |
||||
return count($this->routes); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$routes = new CountableRoutes(['home', 'about', 'contact']); |
||||
var_dump(count($routes)); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(3) |
||||
@ -0,0 +1,46 @@ |
||||
--TEST-- |
||||
Symfony pattern: set_error_handler with first-class static method and finally restore |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class BoxedErrorHandler |
||||
{ |
||||
private static ?string $lastError = null; |
||||
|
||||
public static function handleError(int $type, string $message): bool |
||||
{ |
||||
self::$lastError = $type.':'.$message; |
||||
return true; |
||||
} |
||||
|
||||
public static function call(callable $callback): mixed |
||||
{ |
||||
set_error_handler(self::handleError(...)); |
||||
|
||||
try { |
||||
return $callback(); |
||||
} finally { |
||||
restore_error_handler(); |
||||
} |
||||
} |
||||
|
||||
public static function lastError(): ?string |
||||
{ |
||||
return self::$lastError; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$result = BoxedErrorHandler::call(static function (): string { |
||||
trigger_error('boxed-warning', E_USER_WARNING); |
||||
return 'done'; |
||||
}); |
||||
|
||||
var_dump($result); |
||||
var_dump(BoxedErrorHandler::lastError()); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(4) "done" |
||||
string(17) "512:boxed-warning" |
||||
@ -0,0 +1,25 @@ |
||||
--TEST-- |
||||
Symfony pattern: foreach over Traversable returned from method |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class TraversableProvider |
||||
{ |
||||
public function getIterator(): ArrayIterator |
||||
{ |
||||
return new ArrayIterator(['first' => 1, 'second' => 2]); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$provider = new TraversableProvider(); |
||||
|
||||
foreach ($provider->getIterator() as $key => $value) { |
||||
var_dump($key.':'.$value); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(7) "first:1" |
||||
string(8) "second:2" |
||||
@ -0,0 +1,58 @@ |
||||
--TEST-- |
||||
Symfony pattern: parameter bag filter callback with coalesce throw |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class MiniParameterBag |
||||
{ |
||||
public function __construct(private array $parameters) |
||||
{ |
||||
} |
||||
|
||||
public function filter(string $key, mixed $default, int $filter, array|int $options = []): mixed |
||||
{ |
||||
$value = $this->parameters[$key] ?? $default; |
||||
|
||||
if (is_int($options)) { |
||||
$options = ['flags' => $options]; |
||||
} |
||||
|
||||
if ((FILTER_CALLBACK & $filter) && !(($options['options'] ?? null) instanceof Closure)) { |
||||
throw new InvalidArgumentException('callback filter requires a Closure'); |
||||
} |
||||
|
||||
$options['flags'] ??= 0; |
||||
|
||||
return filter_var($value, $filter, $options); |
||||
} |
||||
|
||||
public function getInt(string $key): int |
||||
{ |
||||
return $this->filter($key, null, FILTER_VALIDATE_INT, FILTER_NULL_ON_FAILURE) |
||||
?? throw new UnexpectedValueException('invalid int: '.$key); |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$bag = new MiniParameterBag([ |
||||
'port' => '9501', |
||||
'name' => 'Symfony', |
||||
]); |
||||
|
||||
var_dump($bag->getInt('port')); |
||||
var_dump($bag->filter('name', '', FILTER_CALLBACK, [ |
||||
'options' => static fn (string $value): string => strtolower($value), |
||||
])); |
||||
|
||||
try { |
||||
$bag->getInt('missing'); |
||||
} catch (UnexpectedValueException $e) { |
||||
var_dump($e->getMessage()); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
int(9501) |
||||
string(7) "symfony" |
||||
string(20) "invalid int: missing" |
||||
@ -0,0 +1,47 @@ |
||||
--TEST-- |
||||
Symfony pattern: recursive iterator over nested context attributes |
||||
--FILE-- |
||||
<?php |
||||
|
||||
function flattenLeaves(array $attributes): array |
||||
{ |
||||
$it = new RecursiveIteratorIterator( |
||||
new RecursiveArrayIterator($attributes), |
||||
RecursiveIteratorIterator::LEAVES_ONLY |
||||
); |
||||
|
||||
$result = []; |
||||
foreach ($it as $key => $value) { |
||||
$path = []; |
||||
for ($depth = 0; $depth <= $it->getDepth(); ++$depth) { |
||||
$path[] = $it->getSubIterator($depth)->key(); |
||||
} |
||||
$result[implode('.', $path)] = $value; |
||||
} |
||||
|
||||
ksort($result); |
||||
return $result; |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$flat = flattenLeaves([ |
||||
'groups' => ['Default', 'Extra'], |
||||
'options' => [ |
||||
'normalizer' => [ |
||||
'trim' => true, |
||||
'lower' => false, |
||||
], |
||||
], |
||||
]); |
||||
|
||||
foreach ($flat as $key => $value) { |
||||
var_dump($key.'='.json_encode($value)); |
||||
} |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(18) "groups.0="Default"" |
||||
string(16) "groups.1="Extra"" |
||||
string(30) "options.normalizer.lower=false" |
||||
string(28) "options.normalizer.trim=true" |
||||
@ -0,0 +1,84 @@ |
||||
--TEST-- |
||||
Symfony pattern: route collection deep clone with ArrayIterator |
||||
--FILE-- |
||||
<?php |
||||
|
||||
final class MiniRoute |
||||
{ |
||||
public function __construct(public string $path) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
final class MiniAlias |
||||
{ |
||||
public function __construct(public string $target) |
||||
{ |
||||
} |
||||
} |
||||
|
||||
final class MiniRouteCollection implements IteratorAggregate |
||||
{ |
||||
private array $routes = []; |
||||
private array $aliases = []; |
||||
|
||||
public function add(string $name, MiniRoute $route): void |
||||
{ |
||||
$this->routes[$name] = $route; |
||||
} |
||||
|
||||
public function addAlias(string $name, MiniAlias $alias): void |
||||
{ |
||||
$this->aliases[$name] = $alias; |
||||
} |
||||
|
||||
public function __clone() |
||||
{ |
||||
foreach ($this->routes as $name => $route) { |
||||
$this->routes[$name] = clone $route; |
||||
} |
||||
|
||||
foreach ($this->aliases as $name => $alias) { |
||||
$this->aliases[$name] = clone $alias; |
||||
} |
||||
} |
||||
|
||||
public function getIterator(): ArrayIterator |
||||
{ |
||||
return new ArrayIterator($this->routes); |
||||
} |
||||
|
||||
public function routeCount(): int |
||||
{ |
||||
return count($this->routes); |
||||
} |
||||
|
||||
public function aliasTarget(string $name): string |
||||
{ |
||||
return $this->aliases[$name]->target; |
||||
} |
||||
} |
||||
|
||||
function main(): void |
||||
{ |
||||
$collection = new MiniRouteCollection(); |
||||
$collection->add('home', new MiniRoute('/')); |
||||
$collection->add('about', new MiniRoute('/about')); |
||||
$collection->addAlias('root', new MiniAlias('home')); |
||||
|
||||
$copy = clone $collection; |
||||
|
||||
foreach (iterator_to_array($copy->getIterator()) as $name => $route) { |
||||
$route->path = strtoupper($route->path); |
||||
var_dump($name.':'.$route->path); |
||||
} |
||||
|
||||
var_dump($collection->routeCount()); |
||||
var_dump($collection->aliasTarget('root')); |
||||
} |
||||
?> |
||||
--EXPECT-- |
||||
string(6) "home:/" |
||||
string(12) "about:/ABOUT" |
||||
int(2) |
||||
string(4) "home" |
||||
Loading…
Reference in new issue