DEV Community

loading...
Cover image for Object Calisthenics with PHP

Object Calisthenics with PHP

Tadeu Barbosa
I'm passionate with development.
・6 min read

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;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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".

Alt Text

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();
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

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");
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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 {/***/}
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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]--;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

I hope that this post can help you! Share it with someone! ;)

Discussion (0)