Unconventional PHP
🤮
<?php
// PHP 5.2
function is_palindrome(string $string)
{
return $string === strrev($string);
}
is_palindrome('PHP');
// Catchable fatal error: Argument 1 passed to is_palindrome()
// must be an instance of string, string given
<?php
class ScalarTypehintHandler
{
const TYPEHINT_PCRE = '/^Argument (\d)+ passed to (?:(\w+)::)?(\w+)\(\) must be an instance of (\w+), (\w+) given/';
private static $typehints = [
'bool' => 'is_bool',
'int' => 'is_int',
'float' => 'is_float',
'string' => 'is_string',
'resource' => 'is_resource',
];
private static function getTypehintedArgument($backtrace, $function, $argIndex, &$argValue)
{
foreach ($backtrace as $trace) {
if (isset($trace['function']) && $trace['function'] == $function) {
$argValue = $trace['args'][$argIndex - 1];
return true;
}
}
return false;
}
public static function handleTypehint($errorLevel, $errorMessage)
{
if ($errorLevel == E_RECOVERABLE_ERROR) {
if (preg_match(static::TYPEHINT_PCRE, $errorMessage, $errorMatches)) {
list($errorMatch, $argIndex, $class, $function, $hint, $type) = $errorMatches;
if (isset(static::$typehints[$hint])) {
$backtrace = debug_backtrace();
$argValue = null;
if (static::getTypehintedArgument($backtrace, $function, $argIndex, $argValue)) {
if (call_user_func(static::$typehints[$hint], $argValue)) {
return true;
}
}
}
}
}
return false;
}
}
set_error_handler('ScalarTypehintHandler::handleTypehint');
<?php
// PHP 5.2
require __DIR__ . '/ScalarTypehintHandler.php';
function is_palindrome(string $string)
{
return $string === strrev($string);
}
is_palindrome('PHP');
// true
📦
Typed Variables
<?php
$i = 0;
$i = 'foobar';
<?php
function int(int $i): int
{
return $i;
}
$i = int(0);
$i = 'foobar';
<?php
function int(int $i): int
{
$obj = new class {
public int $prop;
};
$obj->prop = $i;
return $obj->prop;
}
$i = int(0);
$i = 'foobar';
<?php
function &int(int $i): int
{
$obj = new class {
public int $prop;
};
$obj->prop = $i;
return $obj->prop;
}
$i =& int(0);
$i = 'foobar';
<?php
function &int(int $i): int
{
$obj = new class {
public int $prop;
};
$obj->prop = $i;
$GLOBALS['typed_variables'][] = $obj;
return $obj->prop;
}
$i =& int(0);
$i = 'foobar';
<?php
function &int(int $i): int
{
$obj = new class {
public int $prop;
};
$obj->prop = $i;
$GLOBALS['typed_variables'][] = $obj;
return $obj->prop;
}
$i =& int(0);
$i = 'foobar';
// Uncaught TypeError: Cannot assign string to reference
// held by property class@anonymous::$prop of type int
<?php
function &int(int $i): int
{
$obj = new class {
public int $prop;
};
$obj->prop = $i;
$GLOBALS['typed_variables'][] = $obj;
return $obj->prop;
}
$i =& int(0);
$i = 12;
// ✅
$i = 14;
// ✅
$i = false;
// ⛔️
Function Piping
<?php
$eventID = array_pop(explode('/', trim(parse_url('https://www.meetup.com/php-sw/events/272037512/', PHP_URL_PATH), '/')));
// "272037512"
<?php
$eventID = array_pop(
explode(
'/',
trim(
parse_url('https://www.meetup.com/php-sw/events/272037512/', PHP_URL_PATH),
'/'
)
)
);
// "272037512"
<?php
$url = 'https://www.meetup.com/php-sw/events/272037512/';
$path = parse_url($url, PHP_URL_PATH);
$pathWithTrimmedSlashes = rtrim($path, '/');
$pathSegments = explode('/', $pathWithTrimmedSlashes);
$eventID = array_pop($pathSegments);
// "272037512"
<?php
$eventID = 'https://www.meetup.com/php-sw/events/272037512/'
|> parse_url($$, PHP_URL_PATH)
|> rtrim($$, '/')
|> explode('/', $$)
|> array_pop($$);
// "272037512"
<?php
$eventID = pipe('https://www.meetup.com/php-sw/events/272037512/')
-> parse_url('$$', PHP_URL_PATH)
-> rtrim('$$', '/')
-> explode('/', '$$')
-> array_pop('$$')
-> get();
// "272037512"
<?php
function pipe($value)
{
return new class($value) {
protected $value;
public function __construct($value)
{
$this->value = $value;
}
public function __call($function, $arguments)
{
foreach ($arguments as &$argument) {
if ($argument === '$$') {
$argument = $this->value;
}
}
$newValue = call_user_func($function, ...$arguments);
return pipe($newValue);
}
public function get()
{
return $this->value;
}
};
}
<?php
$eventID = pipe('https://www.meetup.com/php-sw/events/272037512/')
-> parse_url('$$', PHP_URL_PATH)
-> rtrim('$$', '/')
-> explode('/', '$$')
-> array_pop('$$')
-> get();
// "272037512"
Constructors
Static
<?php
class Countries
{
public static function getValues()
{
return json_decode(file_get_contents(__DIR__ . '/countries.json'));
}
}
Countries::getValues();
<?php
class Countries
{
protected static array $values;
public static function getValues()
{
if (!isset($static::$values)) {
static::$values = json_decode(file_get_contents(__DIR__ . '/countries.json'));
}
return static::$values;
}
}
Countries::getValues();
A static constructor...
takes no parameters
should only be called once
is executed BEFORE any instances of the class are created
new Example();
is executed BEFORE any static properties are referenced
Example::$value;
is executed BEFORE any other static methods are called
Example::test();
<?php
class Countries
{
public static array $values = [];
public static function __constructStatic()
{
static::$values = json_decode(file_get_contents(__DIR__ . '/countries.json'));
}
}
<?php
class Countries
{
public static array $values = [];
public static function __constructStatic()
{
static::$values = json_decode(file_get_contents(__DIR__ . '/countries.json'));
}
}
Countries::__constructStatic();
Countries.php
<?php
class UserIndexController
{
public function __invoke()
{
// ...
}
}
UserIndexController.php
<?php
Route::get('/', HomepageController::class);
Route::get('/users', UserIndexController::class);
Route::get('/login', Auth\LoginController::class);
Route::post('/logout', Auth\LogoutController::class);
routes/web.php
<?php
class UserIndexController
{
public function __constructStatic()
{
Route::get('/users', UserIndexController::class);
}
public function __invoke()
{
// ...
}
}
UserIndexController::__constructStatic();
UserIndexController.php
Custom Autoloaders
<?php
$user = new User();
// Uncaught Error: Class 'User' not found
<?php
spl_autoload_register(function ($className) {
require_once "{$className}.php";
});
$user = new User();
// require_once 'User.php';
<?php
require_once __DIR__ . '/vendor/autoload.php';
spl_autoload_register(function ($className) {
if (class_exists($className, autoload: false)) {
if (method_exists($className, '__constructStatic')) {
$className::__constructStatic();
}
}
});
Class Attributes
<?php
class UserIndexController
{
public function __invoke()
{
// ...
}
}
<?php
@@Route('get', '/users')
class UserIndexController
{
public function __invoke()
{
// ...
}
}
<?php
@@Attribute(Attribute::TARGET_CLASS)
class Route
{
protected string $method;
protected string $route;
public function __construct(string $method, string $route) {
$this->method = $method;
$this->route = $route;
}
}
<?php
spl_autoload_register(function ($className) {
$reflectionClass = new ReflectionClass($className);
$attributes = $r->getAttributes();
foreach ($attributes as $attribute) {
if ($attribute->getName() === Route::class) {
$route = $attribute->newInstance();
}
}
});
<?php
@@Attribute(Attribute::TARGET_CLASS)
class Route
{
protected string $method;
protected string $route;
public function __construct(string $method, string $route) {
$this->method = $method;
$this->route = $route;
}
public function register(string $controller)
{
Route::{$this->method}($this->route, $controller);
}
}
<?php
spl_autoload_register(function ($className) {
$reflectionClass = new ReflectionClass($className);
$attributes = $r->getAttributes();
foreach ($attributes as $attribute) {
if ($attribute->getName() === Route::class) {
$route = $attribute->newInstance();
$route->register($className);
}
}
});
<?php
@@Route('get', '/users')
class UserIndexController
{
public function __invoke()
{
return view('users.index', [
'users' => User::all(),
]);
}
}
Realtime Classes
<?php
class Email
{
protected string $to;
protected string $subject = '(No subject)';
protected string $body = '';
public function to(string $to)
{
$this->to = $to;
return $this;
}
public function subject(string $subject)
{
$this->subject = $subject;
return $this;
}
public function body(string $body)
{
$this->body = $body;
return $this;
}
public function send()
{
mail($this->to, $this->subject, $this->body);
}
}
<?php
$email = new Email();
$email->to('info@phpsw.uk')
->body('Hi folks...')
->subject('Let me talk about Unconventional PHP!')
->send();
<?php
class EmailFacade
{
public function __callStatic($name, $arguments)
{
$email = new Email();
return $email->{$name}(...$arguments);
}
}
EmailFacade::body('Hi folks...')
->to('info@phpsw.uk')
->subject('Let me talk about Unconventional PHP!')
->send();
<?php
spl_autoload_register(
autoload_function: function($className) {
// ...
},
throw: true,
prepend: true
);
<?php
spl_autoload_register(
autoload_function: function($className) {
if (str_starts_with($className, 'Facades\\')) {
$path = __DIR__ . '/Facades/' . sha1($className) . '.php';
// ...
}
},
throw: true,
prepend: true
);
<?php
spl_autoload_register(
autoload_function: function($className) {
if (str_starts_with($className, 'Facades\\')) {
$path = __DIR__ . '/Facades/' . sha1($className) . '.php';
if (file_exists($path)) {
require_once $path;
return true;
}
// ...
}
},
throw: true,
prepend: true
);
<?php
spl_autoload_register(
autoload_function: function($className) {
if (str_starts_with($className, 'Facades\\')) {
$path = __DIR__ . '/Facades/' . sha1($className) . '.php';
if (file_exists($path)) {
require_once $path;
return true;
}
if (!is_dir(__DIR__ . '/Facades')) {
mkdir(__DIR__ . '/Facades');
}
touch($path);
// ...
}
},
throw: true,
prepend: true
);
<?php
spl_autoload_register(
autoload_function: function($className) {
if (str_starts_with($className, 'Facades\\')) {
$path = __DIR__ . '/Facades/' . sha1($className) . '.php';
if (file_exists($path)) {
require_once $path;
return true;
}
if (!is_dir(__DIR__ . '/Facades')) {
mkdir(__DIR__ . '/Facades');
}
touch($path);
$realClassName = substr($className, strlen('Facades\\'));
// ...
}
},
throw: true,
prepend: true
);
<?php
spl_autoload_register(
autoload_function: function($className) {
if (str_starts_with($className, 'Facades\\')) {
$path = __DIR__ . '/Facades/' . sha1($className) . '.php';
if (file_exists($path)) {
require_once $path;
return true;
}
if (!is_dir(__DIR__ . '/Facades')) {
mkdir(__DIR__ . '/Facades');
}
touch($path);
$realClassName = substr($className, strlen('Facades\\'));
file_put_contents($path, <<<EOL
<?php
namespace Facades;
class {$realClassName}
{
public static function __callStatic(\$name, \$arguments)
{
\$class = new \\{$realClassName};
return \$class->{\$name}(...\$arguments);
}
}
EOL);
require_once $path;
return true;
}
},
throw: true,
prepend: true
);
<?php
spl_autoload_register(
autoload_function: function($className) {
if (str_starts_with($className, 'Facades\\')) {
$path = __DIR__ . '/Facades/' . sha1($className) . '.php';
if (file_exists($path)) {
require_once $path;
return true;
}
if (!is_dir(__DIR__ . '/Facades')) {
mkdir(__DIR__ . '/Facades');
}
touch($path);
$realClassName = substr($className, strlen('Facades\\'));
file_put_contents($path, <<<EOL
<?php
namespace Facades;
class {$realClassName}
{
public static function __callStatic(\$name, \$arguments)
{
\$class = new \\{$realClassName};
return \$class->{\$name}(...\$arguments);
}
}
EOL);
require_once $path;
return true;
}
},
throw: true,
prepend: true
);
<?php
require_once __DIR__ . '/realtime_facade_autoload.php';
Facades\Email::to('info@phpsw.uk')
->body('Hi folks...')
->subject('Let me talk about Unconventional PHP!')
->send();
src/
├── Email.php
├── index.php
├── realtime_facade_autoload.php ├── Facades/ │ ├── 84add5b2952787581cb9a8851eef63d1ec75d22b.php
Pre-processing
class Fixture
{
private $name = ucwords("acme fixture");
private $thing = new \stdClass();
}
function example($one, $two = 2.2, $three = round(3.3), $four = new stdClass) { ... }
async function get_file(string $path)
{
await \Amp\File\get($path);
}
$users = { $user1, $user2, $user3 };
$admins = $users->filter(fn($user) => $user->isAdmin());
class Collection<T> {
protected $items = [];
public function __construct($items = [])
{
$this->items = $items;
}
public function add(T $item)
{
$this->items[] = $item;
}
}
$users = new Collection<User>;
$users->add($user1); // ✅
$users->add('Foo'); // ⛔︎
$file = fopen('file.txt');
defer fclose($file);
// Do stuff with $file ...
// The file will automatically be closed at the end
function MyForm($props) {
return (
<form>
{$props->showLabel ? <label htmlFor={"email"}>Email</label> : null}
<input type={"text"} name={"email"} id={"email"} />
</form>
);
}
Extensions
Runkit
https://github.com/runkit7/runkit7
Swoole
https://www.swoole.co.uk/
Scalar Objects
<?php
class StringHandler {
public static function length($self) {
return strlen($self);
}
public static function startsWith($self, $other) {
return strpos($self, $other) === 0;
}
}
register_primitive_type_handler('string', StringHandler::class);
$string = "abc";
var_dump($string->length()); // int(3)
var_dump($string->startsWith("a")); // bool(true)
Operator Overloading
+
-
>
&&
<=
*
<=>
<?php
$eighteenYearsAgo = new DateTime('-18 years');
$dateOfBirth = new DateTime('1995-07-15');
if ($dateOfBirth > $eighteenYearsAgo) {
throw new Exception('You are too young to buy this product');
}
<?php
class Money
{
public int $value;
public string $currency;
public function __construct(int $value, string $currency)
{
$this->value = $value;
$this->currency = $currency;
}
public function __add(Money $money)
{
$convertedValue = CurrencyConverter::from($money->currency, $money->value)
->to($this->currency);
return new Money($this->value + $money->value, $this->currency);
}
}
$bank = new Money(6350, 'GBP');
$wallet = new Money(200, 'USD');
$total = $bank + $wallet; // 6503 GBP
PHP 8
WeakMaps
(and WeakRefs)
@@Attributes
@LiamHammett
liamhammett.com
slides.com/liamhammett/unconventional-php
Unconventional PHP
By Liam Hammett
Unconventional PHP
You might think you know PHP, but if you dig below the surface you might find it can do more than you knew. Beyond the documented features, there lies a world of tricks, hacks and other techniques that can allow PHP to go a step further. In this talk we'll look outside the box at some things PHP provides like references, autoloading and magic methods and explore how using them in an unconventional way can benefit our own code.
- 1,428