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