Object Calisthenics with PHP
Tadeu Barbosa
Posted on December 27, 2020
Image by: Vishnu R Nair @vishnurnair
I want to share in this post my recent studies with PHP, more specifically with Object Calisthenics. It's nine rules that was created to help us to code better. There's no secrets with that nine rules, you will see it when you'll be reading. These rules was introduced by Jeff Bay.
One level of indentation per method.
It means that you don't must create a class or a method with more than one level of indentation. For example:
function addDescriptionToPhoto(string $description, array $photos)
{
foreach ($photos as $photo) {
if ($photo->isEditable()) {
if ($photo->belongsToAnGroup()) {
$description += "\n-----\n";
$groups = $photo->getGroups();
foreach ($groups as $group) {
$description += $group->description;
}
$photo->description = $description;
} else {
$photo->description = $description;
$phpto->save();
}
} else {
continue;
}
}
}
Can you see how this code can be confuse for someone else that read it? Of course, maybe that code is not so confuse or complex that codes that we see on the real life. On the past I saw codes that look like "Hadouken".
If your code looks like this one, there's something wrong!
An way to start writing something better, is separating your method in an another. The following code is more legible to me.
function addDescriptionToPhoto(string $description, array $photos)
{
foreach ($photos as $photo) {
if ($photo->isEditable()) {
$this->storePhotoDescription($description, $photos);
} else {
continue;
}
}
}
function storePhotoDescription(string $description, Photo $photo)
{
if ($photo->belongsToAnGroup()) {
$description += "\n-----\n";
$groups = $photo->getGroups();
foreach ($groups as $group) {
$description += $group->description;
}
$photo->description = $description;
$phpto->save();
} else {
$photo->description = $description;
$photo->save();
}
}
Don't use the ELSE keyword.
I love this rule! I don't know when or where... but some years ago, I heard and I follow this one. It's something related about functional programming, maybe I write about it after.
We don't need to ELSEs on our code! If you're using Object Oriented Programming, it means that you can write codes without any ELSEs. The code gets more clean and readable. On the previous code, for example, if we rewrite removing the first else:
function addDescriptionToPhoto(string $description, array $photos)
{
/** $photo Photo */
foreach ($photos as $photo) {
if ($photo->isEditable() === false) continue;
$this->storePhotoDescription($description, $photos);
}
}
function storePhotoDescription(string $description, Photo $photo)
{
if ($photo->belongsToAnGroup() === false) {
$photo->description = $description;
$photo->save();
return;
}
$description += "\n-----\n";
$groups = $photo->getGroups();
foreach ($groups as $group) {
$description += $group->description;
}
$photo->description = $description;
$phpto->save();
}
May you can prevent to use a lot of IFs and ELSEs on your code. Two another rules related with this one that is good too is: fail first and early return. It means that you must return your methods soon as possible, even fails.
function getUserType(User $user, DateTimeInterface $date)
{
$type = 0;
if ($user->registration_date < $date->twoMonthsAgo) {
$type = 1;
} elseif ($user->registration_date < $date->oneMonthAgo) {
$type = 2;
} elseif ($user->registration_date < $date->twoWeeksAgo) {
$type = 3;
} else {
$type = 4;
}
return $type;
}
// An better way
function getUserType(User $user, DateTimeInterface $date)
{
if ($user->registration_date < $date->twoMonthsAgo) {
return 1;
}
if ($user->registration_date < $date->oneMonthAgo) {
return 2;
}
if ($user->registration_date < $date->twoWeeksAgo) {
return 3;
}
return 4;
}
In case of an throw:
function getAssistentToUser(User $user)
{
if ($user->hasAccess()) {
$assistents = Assistents::getAssistentsToSector($user->sector_id);
if (sizeof($assistents) > 0) {
$assistentIndex = rand(0, sizeof($assistentsIndex) - 1);
$assistent = $assistents[$assistentIndex];
return $assistent->id;
} else {
throw new AssistentException("There's no asistents for sector {$user->sector_id}");
}
}
throw new UserException("The actual user has no access to an assistent!");
}
// a better solution
function getAssistentToUser(User $user)
{
if ($user->hasAccess() === false) {
throw new UserException("The actual user has no access to an assistent!");
}
$assistents = Assistents::getAssistentsToSector($user->sector_id);
if (sizeof($assistents) === 0) {
throw new AssistentException("There's no asistents for sector {$user->sector_id}");
}
$assistentIndex = rand(0, sizeof($assistentsIndex) - 1);
$assistent = $assistents[$assistentIndex];
return $assistent->id;
}
The code gets more legible! Can you see?
Wrap all primitives and strings in classes.
Whenever you can, you may replace primitives and strings of a class to an another. For example, we have a User class with phone number and email properties.
class User
{
private $name;
private $email;
private $phone;
public function __construct(string $name, string $email, string $phone) {
$this->name = $name;
$this->email = $email;
$this->phone = $phone;
if (fiter_var($email, FILTER_VALIDATE_EMAIL) === false) {
throw new \InvalidArgumentException(
"You must insert an valid e-mail address!"
);
}
if (fiter_var($phone, FILTER_SANITIZE_NUMBER_INT) === false) {
throw new \InvalidArgumentException(
"You must insert an valid phone number!"
);
}
}
}
$user = User("Tadeu Barbosa", "tadeufbarbosa@gmail.com", "5531900000000");
Instead you can do:
class User
{
private $name;
private $email;
private $phone;
public function __construct(string $name, Email $email, Phone $phone)
{
$this->name = $name;
$this->email = $email;
$this->phone = $phone;
}
}
class Email
{
// all email logic here
}
class Phone
{
// all phone logic here
}
$email = new Email("tadeufbarbosa@gmail.com");
$phone = new Phone("5531900000000");
$user = User("Tadeu Barbosa", $email, $phone);
First class collections.
The collection classes must be only for an collection and nothing more. If you have a classe to manipulate an collection, then that class can't have other responsibility.
class Photos
{
private $photos = [];
public function add(string $photo) {/***/}
public function remove(int $photoIndex) {/***/}
public function count(): int {/***/}
}
One dot per line.
I'm writing examples in PHP, but PHP uses arrows and not dots. Then this rule is about how many arrows you can use. Lets do an example.
class Dog
{
public function __construct(Breed $breed)
{
$this->breed = $breed;
}
}
class Breed
{
public function __construct(string $color)
{
$this->color = $color;
}
}
$dogColor = $dog->breed->color;
// beeter way
class Dog
{
public function __construct(Breed $breed)
{
$this->breed = $breed;
}
public function breedColor()
{
return $this->breed->color;
}
}
$dogColor = $dog->breedColor();
This rule can be ignored in case of use: Fluent Interfaces. An example:
User::query()->where("is_sub", 1)
->where("active", 1)
->whereDate("last_access", Carbon::today()->subMonth())
->get();
Don't abbreviate.
This rule is extraordinary necessary! I saw a lot of code, and write a lot of ones too, that take some minutes to be understood. Did you use: $x, $y, $value, $i, $data, or something related? Maybe if we use something more descriptive, it will take less time to be understood.
class A
{
private $data = [];
public function hg()
{
foreach ($this->data as $i => $data) {
$this->eat($data);
$this->data[$i]--;
}
}
}
// an better way
class Animal
{
private $foods = [];
public function isHungry()
{
foreach ($this->foods as $foodIndex => $food) {
$this->eat($food);
$this->foods[$foodIndex]--;
}
}
}
Keep all classes less than "50" lines.
Believe it or not I had worked with an Laravel Controller Class, with approximately 4000 lines of code. When I open that one on VScode or PHPStorm, my PC was going mad.
Some people says that fifteen lines is insane, then they prefer to understand this rules as: "Keep all classes less than 100 lines". What is important here is to take carer with how many lines you're writing. If your class is taking more than 100 lines, then it is doing more than it must to do. Reread your class or file and separate it in another place.
Methods may take many lines of code too. Sometimes when we are writing some method with an complex logic, it can take many lines. But then when we finally complete what we want, then we must to refactor it, move some lines for an another method, or maybe to an another class.
Sometimes we need to write some codes so fast because of something that the our clients are requesting, or something that our gestor or leader is requesting. We think that we will understood it after, but believe me: We will remember nothing some days after.
No classes with more than two instance variables.
To me, like the last one, this rule can be understood a bit different. May we can use five or six instance variables in our classes. I will share a code that looks like one code that I learn in a course that I did.
class Student
{
public $name;
public $email;
public $phone;
public $address;
public $city;
public $country;
public $courses;
public $notes;
...
}
$student = new Student(
"Tadeu Barbosa",
"tadeufbarbosa@gmail.com",
"+5531900000000",
"...",
"...",
"...",
"...",
"..."
);
// an better way
class Student
{
public $person;
public $address;
public $courses;
}
$person = new Person(
"Tadeu Barbosa",
"tadeufbarbosa@gmail.com",
"+5531900000000"
);
$address = new Address("Lorem Ipsum dolor", "..", "..");
$courses = new Courses([...], [...]);
$student = new Student($person, $address, $courses);
No getters or setters.
I sincerely don't understand this one so clearly. But, the rule is that you should not give access to an class logic. An example is:
class Game
{
protected $score;
public function getScore(): int
{
return $this->score;
}
public function setScore(int $score)
{
$this->score = $score;
}
}
$game = new Game();
$game->setScore($game->getScore() + 300);
// an better way
class Game
{
protected $score;
public function getScore(): int
{
return $this->score;
}
public function addScore(int $score)
{
$this->score += $score;
}
public function removeScore(int $score)
{
$this->score -= $score;
}
}
$game = new Game();
$game->addScore(300);
$game->removeScore(100);
I hope that this post can help you! Share it with someone! ;)
Posted on December 27, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.