11.6 C
Pakistan
Wednesday, February 21, 2024

2024 PHP wish list

It’s never too late to add some fictitious features to the language to make it cooler.

These are some suggestions for improving PHP’s coolness, without making it harder to work with and while still maintaining its freshness and expressiveness.

unchangeable values

PHP was designed to be a template language that sat between a C backend and an HTML frontend, not as a “safe language.” Although being unsafe gives you some freedom and creativity when writing your code and using third-party libraries, there are situations when you need to enforce safety measures to guarantee fewer errors and occasionally better performance.

Enforcing safety in an immutable variable is a nice idea.

Essentially wrapping a variable in an anonymous object that sets it to “read only” and allowing access to the value through the object itself is the best way to make a variable immutable.

// Before
$immutable = new class() {
    public function __construct(public readonly $bool = true)
};

// After
readonly $immutable = true;

The readonly syntax for immutable class properties can be extended to variables using the same methodology. In this manner, altering the value will cause an exception to be raised and instruct the interpreter to allocate the value safely without worrying about subsequently changing its type or value.

An additional option is to use const, which is used to name constants in classes similarly to how JavaScript does.

const $immutable = true;

Making variables unchangeable seems like a good idea in terms of memory usage. I can see how the interpreter would allocate a string or array to the precise size needed rather than allowing room for expansion, which could contribute to memory fragmentation.

Naturally, there won’t be any errors returned when changing an object property or array item. It will only have an impact on the object’s reference in this case.

Return $this

PHP should finally implement the fluent method pattern by returning $this, which is another wish. In addition to making class methods more declarative, it also saves a few lines since the object’s return shouldn’t need to be declared explicitly in the method body.

// Before
class Cool
{
    /** @return $this */
    public function warm(): static
    {
        $this->temp = 20.0;

        return $this;
    }
}

// After
class Cool
{
    public function warm(): $this
    {
        $this->temp = 20.0;
    }
}

Additionally, this might aid in some memory optimizations for the interpreter. Instead of speculating and wasting cycles, I can see the interpreter copying the object pointer in memory beforehand.

Of course, this pattern won’t break an early exit. The only requisite is to declare return $this

public function warm(): $this
{
    if ($this->temp === null) {
        return $this;
    }

    $this->temp = 20.0;
}

Double returns

This pattern was extracted from the game Go. Functions in Go can return more than one value. An array that can hold any value type, which is then unpacked at the receiving end and (ideally) properly documented, is the closest PHP has to returning multiple values.

// Before
/** @return array{int:bool,int:int} */
function something(): array
{
    return [true, 10];
}

// After
function something(): (bool, int)
{
   return (true, 10);
}

// This works with both
[$bool, $int] = something();

Avoiding array construction not only saves memory usage (an array always has more space allocated to it so it can grow larger), but it also provides type hints to the interpreter, enabling it to make more efficient memory allocations.

We also make this approach backwards compatible by using the same unpacking syntax for variables. If the expected and returned values do not match, an error will obviously be returned; however, any proficient developer and IDE should be able to catch this easily.

Postponed return

Here’s another Go pattern. Deferred returns are a very helpful way to return values from a function without immediately leaving its context.

The tap() function found in the Laravel framework is the closest PHP function to a deferred return. In the absence of it, you will need to declare, modify, and return the value.

// Before
public function something(): Warm
{
    $warm = $this->heatUp();

    while ($warm->burns()) {
        $warm->lowerTemp();
    }

    return $warm;
}

// Laravel
public function something(): Warm
{
    return tap($this->heatUp(), function ($warm) {
        while ($warm->burns()) {
            $warm->lowerTemp();
        }
    );
}

// After
function something($cool): Warm
{
    defer $warm = $this->heatUp();

    while ($warm->burns()) {
        $warm->lowerTemp();
    }
}

The defer keyword

would be a fantastic way to tell the interpreter to hold that value rather than leaving the context while the next code is being executed. This is more helpful when we have to call a non-fluent method or one that sets values after the object is constructed.

PHP only executes the first return that is allowed within a function. If there is already a defer in the function, it will overwrite the previous one. We can apply the same pattern for deferring a return. Any deferred value would just be superseded by a plain return.

public function multiple(int $number): Number|Number
{
    // Defer returning the instance
    defer $instance = new Number($number);
    
    // Forcefully return a new instance
    if ($instance->isNegative()) {
        return new Number(0);
    }

    // Overwrite the previous deferred return
    if ($instance->isTooBig()) {
        defer new BigNumber($number);
    }

    if ($instance->isFloat()) {
        $instance->ceil();
    }

    // Return the deferred `$instance` value.
}

Using a loop as an expression

The rigidity of PHP loops is one of its shortcomings. Finding situations where you must initialize a variable into an array, create a loop to add values to the preceding variable, and then return the previous variable is not difficult.

// Until this day...
function uncoolCode($items): array
{
    $array = []

    foreach ($items as $item) {
        if ($array->isValid()) {
            $array[] = $item
        }
    }

    return $array;
}

The closest I’ve been to make a initialize-with-loop is by using iterator_to_array() and a function that returns a Generator, which itself is an iterator. I’ve been using it some years ago.

// Until this day...function uncoolCode($items): array{    return iterator_to_array((function ($items) {        foreach ($items as $item) {            if ($item->valid) {                yield $item;            }        }    })($items));}

PHP 8.0 released the each keyword, which allows us to create loops that loop over another iterable value, such as another array or even a traversable object, and return an array.

// After
$accepted = each ($items as $item) {
    if ($item->valid) {
        yield $item;
    }
};

Although there is a case to be made for using foreach as an expression, doing so may cause some issues and edge cases, particularly when utilizing the return keyword to end the loop.

Returning to the individual concepts, we can use the range() method, for instance, to create an array of items that we can easily iterate through.

// After
$items = each(range(0, 10) as $key => $item) {
    yield $key => $item * 100 / 3
}

We are still using the same variable context, so you might be wondering why I’m using yield instead of return. This does not preclude us from using return; in fact, doing so would cause us to leave the context, much like a foreach loop does.

In other words, we can use the continue keyword to skip the item, the break keyword to end the loop, the throw keyword to raise an exception, or the return keyword to end the function.

/**
 * Return all the billable items from the cart.
 *
 * @return \App\Item[]
 */
public function getBillableItems(Cart $cart): array
{
    return each($cart->items as $item) {
        // Discard the loop and return another value
        if ($item->isCompensationCoupon()) {
            return [];
        }

        // Discard the loop and throw an exception.
        if ($item->isNotForSale()) {
            throw new Exception("The $item->name cannot be sold");
        }

        // Continue with the next item.
        if ($item->isFreeGift()) {
            continue;
        }

        // Stop the loop and return it.
        if ($item->isBackordered()) {
            break;
        }

        yield $part;
    }
}

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles