Welcome to the first ever talk on PHP 8.6!
Let's talk about PHP 5.3
It's been a long road...
... getting from there to here
PHP 5.3: So old it doesn't have its own logo
$c = 5;
$fn = function ($a, $b) use ($c) {
return ($a * $b) * $c;
};
print $fn(2, 3); // 25
$c = 5;
$fn = function ($a, $b) use (&$c) {
return ($a * $b) * $c;
};
$c = 2;
print $fn(2, 3); // 10
$c = 5;
class Closure_abc123 extends Closure {
public function __construct(private $c) {}
public function __invoke(int $a, int $b) {
$c = $this->c;
return ($a * $b) * $c;
}
}
$fn = new Closure_abc123($c);
print $fn(2, 3); // 25
$c = 5;
class Computation {
public function __construct(private $c) {}
public function __invoke(int $a, int $b) {
$c = $this->c;
return ($a * $b) * $c;
}
}
$fn = new Computation($c);
print $fn(2, 3); // 25
And then the world was silent for many a year...
PHP 7.4: Doesn't have its own logo, but PHP 7 has one?
$c = 5;
$fn = function (int $a, int $b) use ($c): int {
return ($a * $b) * $c;
};
print $fn(2, 3); // 25
$c = 5;
$fn = fn (int $a, int $b): int => ($a * $b) * $c;
print $fn(2, 3); // 25
$arr = [1, 2, 3, 4, 5];
// Doing this with long-closures was just fugly.
$odds = array_filter($arr, fn(int $v): bool => (bool) $v % 2);
PHP 8.1: Now with per-release logos!
function is_odd(int $v): bool {
return (bool) $v % 2;
}
$arr = [1, 2, 3, 4, 5];
$odds = array_filter($arr, fn(int $v): bool => is_odd($v));
function is_odd(int $v): bool {
return (bool) $v % 2;
}
$arr = [1, 2, 3, 4, 5];
$odds = array_filter($arr, is_odd(...));
function is_odd(int $v): bool {
return (bool) $v % 2;
}
$arr = [1, 2, 3, 4, 5];
$fn = class() extends Closure {
public function __invoke(int $v): bool {
return is_odd($v);
}
}
$odds = array_filter($arr, $fn);
This was the consolation prize
We tried to get pipes and Parital Function Application,
but failed
Wait, tried to get what...?
PHP 8.5: Now that we're all caught up...
$arr = ['A', 'B', 'C', 'D', 'E'];
$result = array_values(
array_filter(
is_odd(...),
array_map(
fn(string $x): bool => ord($x),
$arr
)
)
);
$arr = ['A', 'B', 'C', 'D', 'E'];
$temp = array_map(fn(string $x): bool => ord($x), $arr));
$temp = array_filter(is_odd(...), $temp);
$temp = array_values($temp);
$result = $temp;
$arr = ['A', 'B', 'C', 'D', 'E'];
$result = $arr
|> (fn($x) => array_map(ord(...), $x))
|> (fn($x) => array_filter(is_odd(...), $x))
|> array_values(...)
;
|>
// Please don't use these.
$foo |> 'bar';
$foo |> [$obj, 'bar'];
// Do use these.
$foo |> bar(...);
$foo |> $obj->bar(...);
$foo |> $invokableObject;
$foo |> function($x) { ... };
$foo |> (fn($X) => /* */);
$ ls -l | grep "page" | awk '{print $5 " " $3 " " $9}' | sort -nr | head -5
$result = scandir('.', SCANDIR_SORT_NONE)
;
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($f) => array_filter(fn($x) => str_contains($x, 'page')))
;
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($v) => array_filter($v, fn($x) => str_contains($x, 'page')))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
;
function sorted(array $a, \Closure $sorter): array {
usort($a, $sorter);
return $a;
}
function size_sort(SplFileInfo $a, SplFileInfo $b): int {
return $a->getSize() <=> $b->getSize();
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($v) => array_filter($v, fn($x) => str_contains($x, 'page')))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
|> (fn($v) => sorted($v, size_sort(...)))
|> array_reverse(...)
;
function sorted(array $a, \Closure $sorter): array {
usort($a, $sorter);
return $a;
}
function size_sort(SplFileInfo $a, SplFileInfo $b): int {
return $a->getSize() <=> $b->getSize();
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($v) => array_filter($v, fn($x) => str_contains($x, 'page')))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
|> (fn($v) => sorted($v, size_sort(...)))
|> array_reverse(...)
|> (fn($v) => array_slice($v, 0, 5))
;
function sorted(array $a, \Closure $sorter): array {
usort($a, $sorter);
return $a;
}
function size_sort(SplFileInfo $a, SplFileInfo $b): int {
return $a->getSize() <=> $b->getSize();
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($v) => array_filter($v, fn($x) => str_contains($x, 'page')))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
|> (fn($v) => sorted($v, size_sort(...)))
|> array_reverse(...)
|> (fn($v) => array_slice($v, 0, 5))
|> (fn($v) => array_map(fn(SplFileInfo $x) => sprintf('%d %s %s', $x->getSize(), $x->getOwner(), $x->getFilename()))
;

function sorted(\Closure $sorter): \Closure {
return function(array $a) use ($sorter) {
usort($a, $sorter);
return $a;
}
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($v) => array_filter($v, fn($x) => str_contains($x, 'page')))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> (fn($v) => array_slice($v, 0, 5))
|> (fn($v) => array_map(fn(SplFileInfo $x) => sprintf('%d %s %s', $x->getSize(), $x->getOwner(), $x->getFilename()))
;
function take(int $count): \Closure {
return fn(array $v) => array_slice($v, 0, 5);
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> (fn($v) => array_filter($v, fn($x) => str_contains($x, 'page')))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> (fn($v) => array_map(fn(SplFileInfo $x) => sprintf('%d %s %s', $x->getSize(), $x->getOwner(), $x->getFilename()))
;
function afilter(\Closure $filter): \Closure {
fn($v) => array_filter($v, $filter);
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> afilter(fn($x) => str_contains($x, 'page'))
|> (fn($v) => array_map(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> (fn($v) => array_map(fn(SplFileInfo $x) => sprintf('%d %s %s', $x->getSize(), $x->getOwner(), $x->getFilename()))
;
function amap(\Closure $mapper): \Closure {
return fn(array $v) => array_map($mapper, $v);
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> afilter(fn($x) => str_contains($x, 'page'))
|> amap(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> amap(fn(SplFileInfo $x) => sprintf('%d %s %s', $x->getSize(), $x->getOwner(), $x->getFilename())
;
function format_file(\SplFileInfo $f): string {
return sprintf('%d %s %s', $x->getSize(), $x->getOwner(), $x->getFilename());
}
$result = scandir('.', SCANDIR_SORT_NONE)
|> afilter(fn($x) => str_contains($x, 'page'))
|> amap(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> amap(format_file(...))
;
// General purpose tools
function sorted(\Closure $sorter): \Closure {}
function take(int $count): \Closure {}
function afilter(\Closure $filter): \Closure {}
function amap(\Closure $mapper): \Closure {}
// Task specific
function size_sort(SplFileInfo $a, SplFileInfo $b): int { }
function format_file(\SplFileInfo $f): string {}
$result = scandir('.', SCANDIR_SORT_NONE)
|> afilter(fn($x) => str_contains($x, 'page'))
|> amap(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> amap(format_file(...))
;
See Crell/fp
PHP 8.6: So new it doesn't even have a logo yet
(AKA, yet another way to make closures)
$hasPage = fn($x) => str_contains($x, 'page');
//
$hasPage = fn($x) => str_contains($x, 'page');
$hasPage = str_contains(?, 'page');
function f(int $a, float $b, Point $c, string $d, int $e, int $opt = 0): string {}
f(?, 3.14, $p1, 'narf', e: ?, ...)
⇓
function (int $a, int $e, int $opt = 0) use ($p1): string {
return f($a, 3.14, $p1, 'narf', $e, $opt);
};
?: Copy this parameter...: Copy "the rest of the parameters"
function f(int $a, float $b, Point $c, string $d, int $e, int $opt = 0): string {}
f(b: ?, a: ?, d: 'narf', opt: ?, c: $p1, ...)
⇓
function (float $b, int $a, int $opt, int $e) use ($p1): string {
return f($a, $b, $p1, 'narf', $e, $opt);
};
function f(int $a, float $b, Point $c, string $d, int $e, int $opt = 0): string {}
f(1, 3.14, new Point(1, 2), 'narf', 5, 6, ...)
⇓
$__p1 = new Point(1, 2);
function () use ($__p1): string {
return f(1, 3.14, $__p1, 'narf', 5, 6);
};
// Provide all but one argument
$c = array_map(strtoupper(...), ?);
$c = array_filter(?, is_numeric(...));
// Provide one or two values to start with, and "the rest".
$c = stuff(1, 'two', ...);
// Fill in a few parameters by name, and leave the rest as is.
$c = stuff(f: 3.14, s: 'two', ...);
// Just reference a function/method, aka first-class-callables.
$c = stuff(...);
// Thunks
$id = 'someId';
$value = getSetting($id, default: getDefaultFromDb($id, ...));
// getSetting() calls $default() only if necessary.
$result = scandir('.', SCANDIR_SORT_NONE)
|> afilter(fn($x) => str_contains($x, 'page'))
|> amap(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> amap(format_file(...))
;
$result = scandir('.', SCANDIR_SORT_NONE)
|> afilter(str_contains(?, 'page'))
|> amap(fn($x) => new SplFileInfo($x))
|> sorted(size_sort(...))
|> array_reverse(...)
|> take(5)
|> amap(format_file(...))
;
$result = scandir('.', SCANDIR_SORT_NONE)
|> array_filter(?, str_contains(?, 'page'))
|> array_map((fn($x) => new SplFileInfo($x), ?)
|> sorted(?, size_sort(...))
|> array_reverse(...)
|> array_slice(?, 0, 5)
|> array_map(format_file(...), ?)
;
Tacit programming, also called point-free style, is a programming paradigm in which function definitions do not identify the arguments (or "points") on which they operate.
"Skipping needless temp variables
to make code more readable"
enum Cases {
private tokenize(string $input): array {
return preg_split('/(^[^A-Z]+|[A-Z][^A-Z]+)/', $input, -1, PREG_SPLIT_NO_EMPTY|PREG_SPLIT_DELIM_CAPTURE);
}
private function splitString(string $input): array {
return $this->tokenize(input)
|> amap(replace('_', ' '))
|> amap(explode(' '))
|> flatten(...)
|> amap(trim(...))
|> afilter()
}
}
enum Cases {
// ...
public function convert(string $name): string {
return match ($this) {
self::Unchanged => $name,
self::lowercase => strtolower($name),
self::snake_case => $name,
|> $this->splitString(...)
|> implode('_')
|> strtolower(...)
,
self::kebab_case => $name,
|> $this->splitString(...)
|> implode('-')
|> strtolower(...)
,
self::CamelCase => $name,
|> $this->splitString(...)
|> amap(ucfirst(...))
|> implode('')
,
};
}
}
class PageData {
private array $files = [
new File(tags: ['a', 'b', 'c']),
new File(tags: ['c', 'd', 'e']),
new File(tags: ['x', 'y', 'a']),
];
public array $tags {
get => $this->arr
|> array_column(?, 'tags') // Gets an array of arrays
|> flatten(...) // Flatten that array
|> array_unique(...) // Remove duplicates
|> array_values(...) // Reindex the array.
}
// ...
}
array_values(array_unique(array_merge(...array_column($arr, 'tags'))));
Blech
function decode_rot13($fp): \Generator {
while ($c = fgetc($fp)) yield str_rot13($c);
}
// Takes an iterable of strings and returns a line-buffered version of it.
function lines_from_charstream(iterable $it): \Closure {
$buffer = '';
return static function () use ($it, &$buffer) {
foreach ($it as $c) {
$buffer .= $c;
while (($pos = strpos($buffer, PHP_EOL)) !== false) {
yield substr($buffer, 0, $pos);
$buffer = substr($buffer, $pos);
}
}
};
}
$result = fopen('pipes.csv', 'rb') // No variable, so closes automatically at GC
|> decode_rot13(...)
|> lines_from_charstream(...)
|> map(str_getcsv(...))
|> map(Product::create(...))
|> map($repo->save(...))
;
function getUserEntity(int $id): UserEntity { }
function serializeUserDto(UserDto $user): string { }
// Type error fail!
print $id |> getUserEntity(...) |> serializeUserDto(...);
Don't send internal entities on the wire
function getUserEntity(int $id): UserEntity { }
function userEntityToDto(UserEntity $user): UserDto { }
function serializeUserDto(UserDto $user): string { }
// Type match win!
print $id |> getUserEntity(...) |> userEntityToDto(...) |> serializeUserDto(...);
Nice that the function names are so self-documenting...
function getUserEntity(int $id): ?UserEntity { }
function userEntityToDto(UserEntity $user): ?UserDto { }
function serializeUserDto(UserDto $user): string { }
// Type match fail (maybe).
print $id |> getUserEntity(...) |> userEntityToDto(...) |> serializeUserDto(...);
Now what...?
function getUserEntity(int $id): ?UserEntity { }
function userEntityToDto(UserEntity $user): ?UserDto { }
function serializeUserDto(UserDto $user): string { }
$user = getUserEntity($id);
if ($user !== null) { // Maybe there's a user?
$dto = userEntityToDto($user);
if ($dto !== null) { // Maybe there's a dto?
print serializeUserDto($dto);
}
}
function getUserEntity(int $id): ?UserEntity { }
function userEntityToDto(UserEntity $user): ?UserDto { }
function serializeUserDto(UserDto $user): string { }
function maybe(\Closure $fn): \Closure {
return fn($val) => $val === null ? null : $fn($val);
}
$user = getUserEntity($id);
if ($user !== null) { // Maybe there's a user?
$dto = userEntityToDto($user);
if ($dto !== null) { // Maybe there's a dto?
print serializeUserDto($dto);
}
}
"Lifts" a function from accepting TYPE to ?TYPE
function getUserEntity(int $id): ?UserEntity { }
function serializeUserDto(UserDto $user): string { }
function userEntityToDto(UserEntity $user): ?UserDto { }
function maybe(\Closure $fn): \Closure {
return static fn($val) => $val === null ? null : $fn($val);
}
print $id
|> getUserEntity(...)
|> maybe(userEntityToDto(...))
|> maybe(serializeUserDto(...))
?? '';
|> binds higher than ??
But what if we want more details than just null?
enum ValidationError {
case StringTooShort;
case UsernameExists;
case InvalidCharacters;
case DoesNotStartWithLetter;
}
function lengthCheck(string $s, int $len): string|ValidationError {
return strlen($s) >= $len ? $s : ValidationError::StringTooShort;
}
function existsCheck(string $s): string|ValidationError {
// Use real code here, obviously...
return in_array($s, [...]) ? ValidationError::UsernameExists : $s;
}
function characterCheck(string $regex, string $s): string|ValidationError {
return preg_match($regex, $s) ? $s : ValidationError::InvalidCharacters;
}
function startsWithLetter(string $s): string|ValidationError {
return ctype_alpha($s[0]) ? $s : ValidationError::DoesNotStartWithLetter;
}
function lengthCheck(string $s, int $len): string|ValidationError {}
function existsCheck(string $s): string|ValidationError {}
function characterCheck(string $regex, string $s): string|ValidationError {}
function startsWithLetter(string $s): string|ValidationError {}
$result = 'ABC123'
|> strtolower(...)
|> lengthCheck(? , 3)
|> existsCheck(...)
|> characterCheck('/^[a-zA-Z0-9]*$/', ?)
|> startsWithLetter(...)
;
Works great on the happy path; type errors on unhappy path
function either(\Closure $fn): \Closure
{
return static fn(mixed $val) => $val instanceof Err ? $val : $fn($val);
}
$result = 'ABC123'
|> strtolower(...)
|> either(lengthCheck(? , 3))
|> either(existsCheck(...))
|> either(characterCheck('/^[a-zA-Z0-9]*$/', ?)))
|> either(startsWithLetter(...))
;
Works fine on all paths
function either(\Closure $fn): \Closure
{
return static fn(mixed $val) => $val instanceof Err ? $val : $fn($val);
}
$result = 'ABC123'
|> strtolower(...)
|> either(lengthCheck(? , 3))
|> either(existsCheck(...))
|> either(characterCheck('/^[a-zA-Z0-9]*$/', ?)))
|> either(startsWithLetter(...))
;
match ($result) {
ValidationError::StringTooShort => output('String must be 3 chars'),
ValidationError::UsernameExists => output('That username already exists'),
ValidationError::InvalidCharacters => output('Only alphanumeric chars are allowed'),
ValidationError::DoesNotStartWithLetter => output('Name must start with a letter'),
default => saveToDb($result),
};
Or whatever type-aware handling you want
$result = 'ABC123'
|> strtolower(...)
|> either(lengthCheck(? , 3))
|> either(existsCheck(...))
|> either(characterCheck('/^[a-zA-Z0-9]*$/', ?)))
|> either(startsWithLetter(...))
;
But what if I want to collect all errors, not just one?
class ValidationLog {
public bool $valid { get => $this->errors === []; }
public function __construct(
readonly public string $string,
readonly public array $errors = []
) {}
}
function logError(\Closure $fn): \Closure {
return static function (string|ValidationLog $val) use ($fn) {
if (! $val instanceof ValidationLog) {
$val = new ValidationLog($val);
}
$result = $fn($val->string);
if ($result instanceof ValidationError) {
return new ValidationLog($val->string, [...$val->errors, $result]);
}
return $val;
};
}
function logError(\Closure $fn): \Closure {
return static function (string|ValidationLog $val) use ($fn) {
if (! $val instanceof ValidationLog) {
$val = new ValidationLog($val);
}
$result = $fn($val->string);
if ($result instanceof ValidationError) {
return new ValidationLog($val->string, [...$val->errors, $result]);
}
return $val;
};
}
$result = 'ABC123'
|> strtolower(...)
|> logError(lengthCheck(? , 3))
|> logError(existsCheck(...))
|> logError(characterCheck('/^[a-zA-Z0-9]*$/', ?)))
|> logError(startsWithLetter(...))
;
if (! $result->valid) {
output($result->errors);
return;
}
// ...
A to A-and-stuffWhat shall we call this pattern?