Three language features I wish PHP had

Good languages copy, great languages steal. What could PHP nick from Python, C#, TypeScript...?

I've been having some fun refactoring a PHP application recently, and finding it somewhat of a cathartic experience. There's a satisfaction to taking something that's become a bit of a mess over the past couple of years and bringing some order to the chaos. Modern PHP's language constructs can be incredibly useful for refactoring (type-hint all the things!), but there are a handful of features that I'm finding myself missing, especially after working with C#, that could reduce a lot of overhead when refactoring.

3. Getters and Setters

Coming in hot at number 3, I'm stealing Auto-Implemented Properties from C#. I like using getters and setters to protect against my domain entities from becoming invalid, or to make certain properties write- or read-only. Here's a class that reasonably accurately represents an entity in the legacy application I've been working with:

<?php declare(strict_types=1);
final class Order
{
    private string $postcode;
    private string $orderNumber;

    public function getOrderNumber(): string
    {
        // Legacy logic
        if (substr($this->orderNumber, 0, 1) === '2') {
            return sprintf('US:%s', $this->orderNumber);
        }

        return $this->orderNumber;
    }

    public function getPostcode(): string
    {
        return $this->postcode;
    }
}

In this example, we have getter methods for a couple of properties (and deliberately no setters, as we don't want this Order to be modified after its creation). However, with C#-style getters and setters, we could dramatically improve this:

<?php declare(strict_types=1);
final class Order
{
    private string $orderNumber {
        get {
            if (substr($orderNumber, 0, 1) === '2') {
                return sprintf('US:%s', $orderNumber);
            }

            return $orderNumber;
        }
    };
    private string $postcode { get; };
}

By reducing the amount of code to just the business logic, we're better communicating intent rather than it being lost amongst a bunch of anemic getters and setters.

2. Annotations (for Decorators)

This is probably a controversial one. Consider a repository:

<?php declare(strict_types=1);
class OrderRepository implements OrderRepositoryInterface
{
    private SomeORMInterface $db;

    public function getOrderByOrderNumber(string $orderNumber): Order
    {
        return $this->db->selectOne(Order::class, [ 'order_nr' => $orderNumber ]);
    }

    public function getOrdersByCustomerId(int $customerId): iterable
    {
        return $this->db->select(Order::class, [ 'customer_id' => $customerId ]);
    }
}

Now consider wanting to add a caching layer over the repository:

<?php declare(strict_types=1);
class CachingOrderRepository implements OrderRepositoryInterface
{
    private OrderRepositoryInterface $orderRepository;
    private RedisCacheInterface $cache;

    public function getOrderByOrderNumber(string $orderNumber): Order
    {
        try {
            return $this->cache->get(sprintf('%s::%s', __METHOD__, $orderNumber));
        } catch (CacheMissException $e) {
            $order = $this->orderRepository->getOrderByOrderNumber($orderNumber);
            $this->cache->set(sprintf('%s::%s', __METHOD__, $orderNumber), $order);
            return $order;
        }
    }

    public function getOrdersByCustomerId(int $customerId): iterable
    {
        try {
            return $this->cache->get(sprintf('%s::%s', __METHOD__, $customerId));
        } catch (CacheMissException $e) {
            $orders = $this->orderRepository->getOrdersByCustomerId($customerId);
            $this->cache->set(sprintf('%s::%s', __METHOD__, $customerId), $orders);
            return $orders;
        }
    }
}

Yikes, that's a whole lot of lifetime off my Cherry Blues.

Stealing something like Python's annotation-style decorators would allow us to write a generic caching decorator that we could reuse everywhere with much less boilerplate. Something like:

<?php declare(strict_types=1);
class OrderRepository implements OrderRepositoryInterface
{
    private SomeORMInterface $db;

    @CachingDecorator(__METHOD__, $orderNumber)
    public function getOrderByOrderNumber(string $orderNumber): Order
    {
        return $this->db->selectOne(Order::class, [ 'order_nr' => $orderNumber ]);
    }

    @CachingDecorator(__METHOD__, $orderNumber)
    public function getOrdersByCustomerId(int $customerId): iterable
    {
        return $this->db->select(Order::class, [ 'customer_id' => $customerId ]);
    }
}

class CachingDecorator
{
    private RedisCacheInterface $cache;

    public function __invoke(callable $method, string $key, $identifier)
    {
        try {
            return $this->cache->get(sprintf('%s::%s', $key, $identifier));
        } catch (CacheMissException $e) {
            $contents = $method(); // Call the decorated method
            $this->cache->set(sprintf('%s::%s', $key, $identifier), $contents);
            return $contents;
        }
    }
}

One huge advantage here, besides saving me from buying a new keyboard anytime soon, is again expressing intent in the repository class itself. Here, we can instantly see that query results are cachedā€”it's not hidden in the DI containerā€”and yet the class still has a single responsibility, as we don't pollute the method bodies with caching details.

There's actually an Annotations RFC in progress, but I'm not sure how it will go when it comes to the voting stage.

1. Generics!! My kingdom for generics!

If you've ever spoken to me on this topic, you knew it was coming.

Quite often, I'll use a Collections library instead of raw arrays (php-ds is by my current favourite), but no matter which one I choose there's an underpinning flaw when it comes to typing. Therefore, I end up using composition heavily, adding type checking to be able to mimic typed collections - again, that's quite a lot of typing, though.

<?php declare(strict_types=1);
class OrderCollection
{
    private Vector $collection;

    public function push(Order $order): void
    {
        $this->collection->push($order);
    }
}

// Now, we must explicitly not use raw collections, always the typed ones.
$orders = new OrderCollection();
$orders->push('notanorder'); // TypeError šŸ‘Œ

Generics would allow for truly typed collections without any boilerplate, and would make me a very happy man. Well, a bit happier, anyway.

<?php declare(strict_types=1);
// No extraneous class required! Plus, we literally can't use an untyped collection!
$orders = new Vector<Order>();
$orders->push('notanorder'); // TypeError šŸ‘Œ

Bonus round: Class visibility

All the above are actually possible to work around (at the expense of my poor, middle-class fingers), but there's one thing that is very difficult, if not impossible, to achieve in the current state of PHP: class visibility. When writing libraries especially, we may want to hide an internal implementation from userland, in order to guard against weird shit happening.

Being able to restrict class instantiation to within a particular namespace (private class) or namespace tree (protected class) would go a long way towards this.

<?php declare(strict_types=1);
namespace My\Namespace\Tree {
    private class PrivateClass { /* ... */ }
    protected class ProtectedClass { /* ... */ }
}

namespace My\Namespace {
    new Tree\PrivateClass(); // Error
    new Tree\ProtectedClass(); // OK
}

namespace Something\Else\Entirely {
    new Tree\ProtectedClass(); // Error
}

I actually don't know a good way of achieving this in PHP today. It's very minor (and I do prefer to trust the users of my libraries somewhat!), but there have been one or two cases I've run into where this feature would be useful.

I'll be waiting with bated breath for any one of these features to come in PHP 8.x. Until then, I'll keep on refactoring the long way round, and buying new keyboards.