upscale/stdlib-overloading

PHP7 function/method arguments overloading

1.0.3 2020-02-23 05:02 UTC

This package is auto-updated.

Last update: 2024-04-18 05:57:30 UTC


README

This library introduces function/method overloading – varying implementation depending on input arguments.

Features:

  • Overloading by argument types
  • Overloading by number of arguments
  • Efficient native type checks of PHP7
  • Informative native error messages of TypeError
  • Lightweight: no OOP, no Reflection

Installation

The library is to be installed via Composer as a dependency:

composer require upscale/stdlib-overloading

Usage

Syntax

Overload a custom function/method:

<?php
declare(strict_types=1);

use function Upscale\Stdlib\Overloading\overload;

function func(...$args)
{
    return overload(
        function (int $num1, int $num2) {
            // ...
        },
        function (string $str1, string $str2) {
            // ...
        }
        // ...
    )(...$args);
}

Any number of valid callable implementations can be declared. Order defines evaluation priority.

Call the overloaded function:

func(1, 2);
func('a', 'b');

Example

Arithmetic calculator that works with ordinary integers, arbitrary-length GMP integers, and Money objects.

<?php
declare(strict_types=1);

use function Upscale\Stdlib\Overloading\overload;

class Money
{
    private $amount;
    private $currency;

    public function __construct(int $amount, string $currency)
    {
        $this->amount = $amount;
        $this->currency = $currency;
    }

    public function add(self $sum): self
    {
        if ($sum->currency != $this->currency) {
            throw new \InvalidArgumentException('Money currency mismatch');
        }
        return new self($this->amount + $sum->amount, $this->currency);
    }
}

class Calculator
{
    public function add(...$args)
    {
        return overload(
            function (int $num1, int $num2): int {
                return $num1 + $num2;
            },
            function (\GMP $num1, \GMP $num2): \GMP {
                return gmp_add($num1, $num2);
            },
            function (Money $sum1, Money $sum2): Money {
                return $sum1->add($sum2);
            }
        )(...$args);
    }
}


$calc = new Calculator();

$one = gmp_init(1);
$two = gmp_init(2);

$oneUsd = new Money(1, 'USD');
$twoUsd = new Money(2, 'USD');

print_r($calc->add(1, 2));
// 3

print_r($calc->add($one, $two));
// GMP Object([num] => 3)

print_r($calc->add($oneUsd, $twoUsd));
// Money Object([amount:Money:private] => 3 [currency:Money:private] => USD)

print_r($calc->add(1.25, 2));
// TypeError: Argument 1 passed to Calculator::{closure}() must be an instance of Money, float given

Architecture

The overloading mechanism leverages the native type system of PHP7 and relies on declaration of strict type annotations. It traverses implementation callbacks in the declared order and attempts to invoke each of them with provided arguments. Result of the first compatible invocation is returned and the subsequent callbacks discarded.

Limitations

PHP engine allows to pass more runtime arguments to a function/method than accounted for in its signature declaration. Excess arguments are being silently discarded without triggering any catchable errors, not even ArgumentCountError. The solution is to declare more specific longer signatures before less specific ones with matching subset of arguments.

Optional arguments are problematic as they are call-time compatible with a shorter signature of required arguments only. Consider the following ambiguous declaration that cannot be resolved by reordering:

overload(
    function (int $num1, int $num2) {
        // ... 
    },
    function (int $num1) {
        // ... 
    },
    function (int $num1, string $str2 = 'default') {
        // Unreachable because preceding declaration matches first and swallows excess arguments
        // ...
    }
)

The workaround is to validate the number of arguments to not exceed the declaration:

overload(
    function (int $num1, int $num2) {
        // ... 
    },
    function (int $num1) {
        if (func_num_args() > 1) {
            throw new \ArgumentCountError('Too many arguments provided');
        }
        // ... 
    },
    function (int $num1, string $str2 = 'default') {
        // Reachable with the optional argument passed, but still unreacable without it
        // ... 
    }
)

Contributing

Pull Requests with fixes and improvements are welcome!

License

Copyright © Upscale Software. All rights reserved.

Licensed under the Apache License, Version 2.0.