修复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