Writing Readable PHP - being expressive

Being expressive when writing code makes it easier for people to understand your code and shows you care about the next developer who comes along.


To make code as readable as possible, you want names to convey as much meaning as possible.

Convert comments to functions with a descriptive name

When a function grows, you'll often end up with a lengthy piece of code that has comments describing various steps. Chances are that you can break up the code block and extract each step to its own function using each comments as an appropriate function name.


// get and sanitize data
$data = Http::get('https://some-api.com')->json();

foreach ($data['items'] as $item) {
 // do stuff
}

// create pdf
$directory = $user->getStorageDirectory();
$template = $user->getPdfTemplate();
(new Pdf())
    ->create($template)
    ->setData($data)
    ->save($directory);

// mail pdf
$mail = (new Mail($user->email))
        ->setSubject('your pdf')
        ->send();


$data = $this->getSanitizedPdfData();

$pathToPdf = $this->createPdf($data, $user);

$this->mailPdf($user, $mail);

As an added bonus, these new methods make excellent cases for unit tests!

If you're using PhpStorm, you'll be happy to know that it allows you to easily refactor lines of code to a new function. It'll take care of passing the right arguments and returning the right result. To perform the refactor:

  • select a piece of code
  • right click, and select "Refactor" > "Extract method"
  • optionally edit the proposed function name and press ok to close the dialog.
Use method name prefixes

You can use prefixes to make your code sound more natural.


$status = $user->pending();


$userIsPending = $user->isPending();

In most projects, suffixes like make, create, get or fetch also have a dedicated meaning.


$user = $this->makeUser();
$user = $this->createUser();

Even without looking at the implementation, we can already guess what these methods do.

make signifies that a new object will be created in memory. If we don't explicitly save it, it won't be persisted. create signifies that a new object will be created and persisted in the database.


$invoice = $this->getInvoice();


$invoice = $this->fetchInvoice();

Of course, you can decide on your own prefixes to use, but make sure you use them consistently.

Adding metrics to name

Whenever you work with something that can be measured, consider adding the unit to the name.


$averageTime = 100;


$averageTimeInMs = 100;

Another way of dealing with this is to create dedicated objects.


$percentage = 0.5;
$percentage = 50;

You can't really tell what the system you're working in expects.


class Percentage
{    
    public static fromInt(int $percentage): self
    {
        new self($percentage);
    }

    public static fromFloat(float $percentage): self
    {
        new self($percentage * 100);
    }

    private function __construct(
        public int $value
    ) {};
}

By using a Percentage class, it's clear that an int is expected.


$percentage = Percentage::fromInt(50);

Add prefixes to hint what is going to be returned

By naming a method right, you can hint want is going to get returned.


$status = $user->status('pending');


// Better because "is" hints that we are going to get back a boolean
$isUserPending = $user->isStatus('pending');

// Best (because shorter and easier to change)
$isUserPending = $user->isPending();

By adding "is" to the names we make our intentions more clear. Also, the new variable name lets us assume it will return a boolean. Of course, you can also use a different word depending on the context, for example has.


$user->hasReplied();

Name what you get

Try to make your names unambiguous: the term "class", for example, can be interpreted as a file name, a namespace name, a reflection class, and maybe even more. By using clear names, you can avoid confusion.


return $factory->getTargetClass();


return $factory->getTargetClassName();
return $factory->getTargetClassFile();
return $factory->getTargetClassReflection();

Don't be afraid of names that become too long. It's better to have readable code than to spare a couple of characters here and there.

Use descriptive table names for many-to-many relationships

When creating a table for a many-to-many relationship, like between users and videos, it is common to name the table with both the model names. This makes it obvious which tables are referred to. Still, this approach often lacks meaning and purpose.

In this example, the table would be named "user_video." We see that this table combines users and videos, but what we store is which user has watched which videos. So a much better name would be just "watched_videos." This gives this table meaning and purpose.


user_video
product_user


watched_videos
purchased_products