Development Blog
Writing Readable PHP - being expressive
To make code as readable as possible, you want names to convey as much meaning as possible.
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.
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.
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);
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();
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.
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