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.

  • 127
Loading comments...

More from Liam Hammett