Building modular software requires strong knowledge of the class design. There are numerous guidelines out there, addressing the way we name our classes, number of variables they should have, what the size of methods should be, and so on. The PHP ecosystem managed to pack these into official PSR standard, more precisely PSR-1: Basic Coding Standard and PSR-2: Coding Style Guide. These are all general programming guidelines that keep our code readable, understandable, and maintainable.
Aside from programming guidelines, there are more specific design principles that we can apply during the class design. Ones that address the notions of low coupling, high cohesion, and strong encapsulation. We call them SOLID design principles, a term coined by Robert Cecil Martin in the early 2000s.
SOLID is an acronym for the following five principles:
Over a decade old, the idea of SOLID principles is far from obsolete, as they are at the heart of good class design. Throughout this chapter, we will look into each of these principles, getting to understand them by observing some of the obvious violations that break them.
In this chapter, we will be covering the following topics:
The single responsibility principle deals with classes that try to do too much. The responsibility in this context refers to reason to change. As per the Robert C. Martin definition:
"A class should have only one reason to change."
The following is an example of a class that violates the SRP:
class Ticket {
const SEVERITY_LOW = 'low';
const SEVERITY_HIGH = 'high';
// ...
protected $title;
protected $severity;
protected $status;
protected $conn;
public function __construct(\PDO $conn) {
$this->conn = $conn;
}
public function setTitle($title) {
$this->title = $title;
}
public function setSeverity($severity) {
$this->severity = $severity;
}
public function setStatus($status) {
$this->status = $status;
}
private function validate() {
// Implementation...
}
public function save() {
if ($this->validate()) {
// Implementation...
}
}
}
// Client
$conn = new PDO(/* ... */);
$ticket = new Ticket($conn);
$ticket->setTitle('Checkout not working!');
$ticket->setStatus(Ticket::STATUS_OPEN);
$ticket->setSeverity(Ticket::SEVERITY_HIGH);
$ticket->save();The Ticket class deals with validation and saving of the ticket entity to the database. These two responsibilities are its two reasons to change. Whenever the requirements change regarding the ticket validation, or regarding the saving of the ticket, the Ticket class will have to be modified. To address the SRP violation here, we can use the assisting classes and interfaces to split the responsibilities.
The following is an example of refactored implementation, which complies with SRP:
interface KeyValuePersistentMembers {
public function toArray();
}
class Ticket implements KeyValuePersistentMembers {
const STATUS_OPEN = 'open';
const SEVERITY_HIGH = 'high';
//...
protected $title;
protected $severity;
protected $status;
public function setTitle($title) {
$this->title = $title;
}
public function setSeverity($severity) {
$this->severity = $severity;
}
public function setStatus($status) {
$this->status = $status;
}
public function toArray() {
// Implementation...
}
}
class EntityManager {
protected $conn;
public function __construct(\PDO $conn) {
$this->conn = $conn;
}
public function save(KeyValuePersistentMembers $entity)
{
// Implementation...
}
}
class Validator {
public function validate(KeyValuePersistentMembers $entity) {
// Implementation...
}
}
// Client
$conn = new PDO(/* ... */);
$ticket = new Ticket();
$ticket->setTitle('Payment not working!');
$ticket->setStatus(Ticket::STATUS_OPEN);
$ticket->setSeverity(Ticket::SEVERITY_HIGH);
$validator = new Validator();
if ($validator->validate($ticket)) {
$entityManager = new EntityManager($conn);
$entityManager->save($ticket);
}Here we introduced a simple KeyValuePersistentMembers interface with a single toArray method, which is then used with both EntityManager and Validator classes, both of which take on a single responsibility now. The Ticket class became a simple data holding model, whereas client now controls instantiation, validation, and save as three different steps. While this is certainly no universal formula of how to separate responsibilities, it does provide a simple and clear example of how to approach it.
Designing with the single responsibilities principle in mind yields smaller classes with greater readability and easier to test code.