SOLID Principles with API & PHP Examples
Tags: solid, api, php
Published: 2022-10-15 21:16:51

Sometimes, while trying to implement some kind of conceptual things, it can be hard to find real-world / working examples, more practical and far from "Rectangle / Square" stuff. And as developers, we may just want to see them how they work in our day-to-day jobs to learn and keep them in mind better.

So, after reading a lot of conceptual / definitional articles but seeing few API-related examples, I decided to write about it using real-world API examples, partly I have used in the past. And here below, you will see many code pieces instead of plain texts (say I am better at coding than speaking).

In short, **SOLID stands for creating easy to reuse, maintain and understand software source codes and offers 5 elegant principles for that.**

Let’s see these principles in action one-by-one, with simple definitions, descriptions and code examples.

## 1- Single Responsibility Principle (SRP)
**Classes (also their methods, by the way) should have only one responsibility.** So, if we push a bunch of unrelated jobs into one class, this will break the principle, making that class unhappy. Instead, we can create different classes for different jobs to follow the principle.

Here we have an API endpoint `POST /token` for creating tokens, `TokenController` class with `tokenAction()` method for checking a user by username & password credentials using a `User` entity.

**Bad practice:**

```php
class User extends Entity {
  public int $id;
  public string $name, $email;
  public UserSettings $settings;

  // More props & setters/getters.

  // @tofix: Does not belong here.
  public function validatePassword(string $password): bool {
    // Run validation stuff.
  }

  // @tofix: Does not belong here.
  public function sendMail(string $subject, string $body): void {
    // Run mailing stuff.
  }
}
```

```php
class TokenControler extends Controller {
  // @call POST /token
  public function tokenAction(): Payload {
    [$username, $password]
      = $this->request->post(['username', 'password']);

    // @var User (say it's okay, no 404)
    $user = $this->repository->getUserByUsername($username);

    if ($user->validatePassword($password)) {
      if ($user->settings->isTrue('mailOnAuthSuccess')) {
        $user->sendMail(
         'Success login!',
         'New successful login, IP: ' . $this->request->getIp()
        );
      }

      $token = new Token($user);
      $token->persist();

      return $this->jsonPayload(Status::OK, [
        'token'  => $token->getValue(),
        'expiry' => $token->getExpiry()
      ]);
    }

    if ($user->settings->isTrue('mailOnAuthFailure')) {
      $user->sendMail(
        'Failed login!',
        'Suspicious login attempt, IP: ' . $this->request->getIp()
      );
    }

    return $this->jsonPayload(Status::UNAUTHORIZED, [
      'error' => 'Invalid credentials.'
    ]);
  }
}
```

Since we are stuffing `User` entity with two unrelated methods (not belong there), there will probably be many reasons to change this class over time.

So here, we can use a better approach by simply moving these unrelated methods into more specific and *one-reason-to-change* classes.

**Better practice:**

```php
class User extends Entity {
  public int $id;
  public string $name, $email;
  public UserSettings $settings;

  // More props & setters/getters.
}
```

```php
// Each item is in its own file.
class UserHolder {
  public function __construct(
    protected readonly User $user
  ) {}
}

class UserPasswordValidator extends UserHolder {
  public function validate(string $password): bool {
    // Run validation business using $this->user->password.
  }
}
class UserAuthenticationMailer extends UserHolder {
  public function sendSuccessMail(string $ip): void {
    // Run mailing business using $this->user->email.
  }
  public function sendFailureMail(string $ip): void {
    // Run mailing business using $this->user->email.
  }
}
```

```php
class TokenControler extends Controller {
  // @call POST /token
  public function tokenAction(): Payload {
    // ...

    $validator = new UserPasswordValidator($user);
    if ($validator->validate($password)) {
      if ($user->settings->isTrue('mailOnAuthSuccess')) {
        $mailer = new UserAuthenticationMailer($user);
        $mailer->sendSuccessMail($this->request->getIp());
      }

      // ...
    }

    if ($user->settings->isTrue('mailOnAuthFailure')) {
      $mailer ??= new UserAuthenticationMailer($user);
      $mailer->sendFailureMail($this->request->getIp());
    }

    // ...
  }
}
```

Now our code becomes more granular, so flexible and extendable. Testing is now more easy since we can test these unrelated methods separately as their own classes, as separate business owners.

But, wanna see one more benefit of this approach? Let's add one more check for IP validity into `tokenAction()` and one more class named `UserIpValidator`.

```php
class UserIpValidator extends UserHolder {
  public function validate(string $ip): bool {
    $ips = new IpList($this->user->settings->get('allowedIps'));
    return $ips->blank() || $ips->contains($ip);
  }
}
```

```php
class TokenControler extends Controller {
  // @call POST /token
  public function tokenAction(): Payload {
    // ...

    $validator = new UserIpValidator($user);
    if (!$validator->validate($this->request->getIp())) {
      return $this->jsonPayload(Status::FORBIDDEN, [
        'error' => 'Non-allowed IP.'
      ]);
    }

    // ...
  }
}
```

## 2- Open-Closed Principle (OCP)
**Classes (also their methods, by the way) should be open for extensions, but closed for modifications (behavioral changes).** In other words, we should be able to extend them without changing their behaviors. So, we can use abstractions to follow the principle.

Here we have an API endpoint `POST /payment` for accepting payments, `PaymentController` class with `paymentAction()` method for processing user's payments by their subscriptions, and `DiscountCalculator` for applying discounts to gross totals of these payments.

**Bad practice:**

```php
class DiscountCalculator {
  // @see Spaghetti Pattern.
  public function calculate(User $user, float $amount): float {
    $discount = match ($user->subscription->type) {
      'basic'  => $amount >= 100.0 ? 10.0 : 0,
      'silver' => $amount >= 75.0  ? 15.0 : 0,
      default  => throw new Error('Invalid subscription type!')
    };
    return $discount ? $amount / 100 * $discount : $amount;
  }
}
```

```php
class PaymentController extends Controller {
  // @call POST /payment
  public function paymentAction(): Payload {
    [$grossTotal, $creditCard]
      = $this->request->post(['grossTotal', 'creditCard']);

    // @var User (say it's okay, no 404)
    $user = $this->repository->getUserByToken($token_ResolvedInSomeWay);

    $calculator = new DiscountCalculator();
    $discount   = $calculator->calculate($user, $grossTotal);
    $netTotal   = $grossTotal - $discount;

    try {
      $payment = new Payment(amount: $netTotal, card: $creditCard);
      $payment->charge();

      if ($payment->okay()) {
        $this->repository->saveUserPayment($user, $payment);
      }

      return $this->jsonPayload(Status::OK, [
        'netTotal'      => $netTotal,
        'transactionId' => $payment->transactionId
      ]);
    } catch (PaymentError $e) {
      $this->logger->logError($e);

      return $this->jsonPayload(Status::INTERNAL, [
        'error'  => 'Payment error.'
        'detail' => $e->getMessage()
      ]);
    } catch (RepositoryError $e) {
        $this->logger->logError($e);

        $payment->cancel();

        return $this->jsonPayload(Status::INTERNAL, [
          'error'  => 'Repository error.',
          'detail' => $e->getMessage()
        ]);
      }
  }
}
```

Since `DiscountCalculator` class is not closed to changes, we will always need to change this class to support new subscription types whenever the system adds new subscription types for users.

So here, we can use a better approach by simply creating classes related to the subscription types based on an abstraction, and using it in `DiscountCalculator` class.

**Better practice:**

```php
// Each item is in its own file.
abstract class Discount {
  public abstract function calculate(float $amount): float;

  // In respect of DRY principle.
  protected final function calculateBy(
    float $amount, float $threshold, float $discount
  ): float {
    if ($amount >= $threshold) {
      return $amount / 100 * $discount;
    }
    return 0.0;
  }
}

// These classes can have such constants
// like THRESHOLD, DISCOUNT instead, BTW.
class BasicDiscount extends Discount {
  public function calculate(float $amount): float {
    return $this->calculateBy(
      $amount, threshold: 100.0, discount: 10.0
    );
  }
}
class SilverDiscount extends Discount {
  public function calculate(float $amount): float {
    return $this->calculateBy(
      $amount, threshold: 75.0, discount: 15.0
    );
  }
}

class DiscountFactory {
  public static function create(User $user): Discount {
    // Create a Discount instance by $user->subscription->type.
  }
}
```

```php
class DiscountCalculator {
  // @see Delegation Pattern.
  public function calculate(Discount $discount, float $amount): float {
    return $discount->calculate($amount);
  }
}
```

```php
class PaymentController extends Controller {
  // @call POST /payment
  public function paymentAction(): Payload {
    // ...

    $calculator = new DiscountCalculator();
    $discount   = $calculator->calculate(
      DiscountFactory::create($user),
      $grossTotal
    );
    $netTotal   = $grossTotal - $discount;

    // ...
  }
}
```

Now `DiscountCalculator` class uses the real calculator (actually becomes its delegate) and complies the principle. So, if any change becomes required in the future, we never need to change `calculate()` method anymore. We can simply add a new related class (e.g. `GoldDiscount` for the type of "gold" subscriptions) and update the factory class by this need.

## 3- Liskov Substitution Principle (LSP)
**Subclasses should be able to use all features of the superclasses, and all subclasses should be usable instead of their superclasses (by their same fields & behaviors with parental fields & behaviors).** So, if subclasses will not use all features of inherited classes, then there will be unnecessary code blocks, or if subclasses change how their superclasses methods work, then this code will be more error prone.

Here we have an API endpoint `POST /file` for working with files, `FileController` class with `writeAction()` method for writing a file, and `File` / `ReadOnlyFile` classes for related works.

**Bad practice:**

```php
class File {
  public function read(string $name): string {
    // Read file contents & return all read contents.
  }
  public function write(string $name, string $contents): int {
    // Write file contents & return written size in bytes.
  }
}

class ReadOnlyFile extends File {
  // @override Changes parent behavior.
  public function write(string $name, string $contents): int {
    throw new Error('Cannot write read-only file!');
  }
}

class FileFactory {
  public static function create(string $name): File {
    // Create a File instance controlling the name &
    // deciding the instance type in some logic way.
  }
}
```

```php
class FileController extends Controller {
  // @call POST /file
  public function writeAction(): Payload {
    // Auth / token check here.

    [$name, $contents]
      = $this->request->post(['name', 'contents']);

    // @var File
    $file = FileFactory::create($name);

    // We are blindly relying on write() method here,
    // & not doing any check or try/catch for errors.
    $writtenBytes = $file->write($name, $contents);

    return $this->jsonPayload(Status::OK, [
      'writtenBytes' => $writtenBytes
    ]);
  }
}
```

Since `writeAction()`, so client code, relies on `File` class and its `write()` method, this action cannot work as expected as it has no check for any error because of this reliance.

So here, we need to fix the file classes by using abstractions first and then the client code by adding a simple check for *writability*.

**Better practice:**

```php
// Each item is in its own file.
interface IFile {
  public function isReadable(): bool;
  public function isWritable(): bool;
}
interface IReadableFile {
  public function read(string $name): string;
}
interface IWritableFile {
  public function write(string $name, string $contents): int;
}

// For the sake of DRY.
trait FileTrait {
  public function isReadable(): bool {
    return $this instanceof IReadableFile;
  }
  public function isWritable(): bool {
    return $this instanceof IWritableFile;
  }
}

class File implements IFile, IReadableFile, IWritableFile {
  use FileTrait;
  public function read(string $name): string {
    // Read file contents & return all read contents.
  }
  public function write(string $name, string $contents): int {
    // Write file contents & return written size in bytes.
  }
}

class ReadOnlyFile implements IFile, IReadableFile {
  use FileTrait;
  public function read(string $name): string {
    // Read file contents & return all read contents.
  }
}

class FileFactory {
  public static function create(string $name): IFile {
    // Create a File instance controlling the name &
    // deciding the instance type in some logic way.
  }
}
```

```php
class FileController extends Controller {
  // @call POST /file
  public function writeAction(): Payload {
    // ...

    // @var IFile
    $file = FileFactory::create($name);

    // Now we have an option to check it,
    // whether file is writable or not.
    $writtenBytes = null;
    if ($file->isWritable()) {
      $writtenBytes = $file->write($name, $contents);
    }

    // ...
  }
}
```

**Signals of LSP violation**;

- If a subclass throws an error for a superclass behavior it can't fulfill (eg: write() method of File > ReadOnlyFile : ReadOnlyError). Inner (override) issue.
- If a subclass has no implementation for a superclass behavior it can't fulfill (eg: write() method of File > ReadOnlyFile : "Do nothing..."). Inner (override) issue.
- If a subclass method always returns the same (fixed or constant) value for an overridden method. This is a very subtle violation and hard to spot. Inner (override) issue.
- If the clients know about the subtypes, mostly using "instanceof" keyword (eg: delete() method of FileDeleter : "if file instanceof ReadOnlyFile then return"). Outer (client) issue.

## 4- Interface Segregation Principle (ISP)
**Interfaces should not be forced to take much responsibilities than they need, and also classes should not be forced to implement interfaces with features they don't need.** This principle is similar to Single Responsibility Principle (SRP), and SRP is about classes but ISP is about interfaces. So, we can create different interfaces for different jobs to follow the principle.

Here we have an API endpoint `POST /notify` for notifying users, `NotifyController` class with `notifyAction()` method for sending notifications to users, and `Notifier` class for this work implementing `INotifier` which is *verbosely* filled by methods.

**Bad practice:**

```php
// Each item is in its own file.
interface INotifier {
  public function sendSmsNotification(
    string $phone, string $subject, string $message
  ): void;
  public function sendPushNotification(
    string $devid, string $subject, string $message
  ): void;
  public function sendEmailNotification(
    string $email, string $subject, string $message
  ): void;
}

class Notifier implements INotifier {
  public function sendSmsNotification(
    string $phone, string $subject, string $message
  ): void {
    // Send a notification to given phone.
  }
  public function sendPushNotification(
    string $devid, string $subject, string $message
  ): void {
    // Send a notification to given device by id.
  }
  public function sendEmailNotification(
    string $email, string $subject, string $message
  ): void {
    // Send a notification to given email.
  }
}
```

```php
class NotifyController extends Controller {
  // @call POST /notify
  public function notifyAction(): Payload {
    [$subject, $message]
      = $this->request->post(['subject', 'message']);

    // @var User (say it's okay, no 404)
    $user = $this->repository->getUserByToken($token_ResolvedInSomeWay);

    $notifier = new Notifier();
    if ($user->settings->isTrue('notifyViaSms')) {
      $notifier->sendSmsNotification($user->phone, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaPush')) {
      $notifier->sendPushNotification($user->devid, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaEmail')) {
      $notifier->sendEmailNotification($user->email, $subject, $message);
    }

    return $this->jsonPayload(Status::OK);
  }
}
```

Since we are pushing many methods into `INotifier` interface, we are far away from this motto: “Many client-specific (or *niche*) interfaces are better than one general-purpose (or simply saying *fat*) interface.”

So here, what we need to do is to separate each job with a separate interface that is present for its own job.

**Better practice:**

```php
// Each item is in its own file.
interface ISmsNotifier {
  public function send(
    string $phone, string $subject, string $message
  ): void;
}
interface IPushNotifier {
  public function send(
    string $devid, string $subject, string $message
  ): void;
}
interface IEmailNotifier {
  public function send(
    string $email, string $subject, string $message
  ): void;
}

class SmsNotifier implements ISmsNotifier {
  public function send(
    string $phone, string $subject, string $message
  ): void {
    // Send a notification to given phone.
  }
}
class PushNotifier implements IPushNotifier {
  public function send(
    string $devid, string $subject, string $message
  ): void {
    // Send a notification to given device by id.
  }
}
class EmailNotifier implements IEmailNotifier {
  public function send(
    string $email, string $subject, string $message
  ): void {
    // Send a notification to given email.
  }
}
```

```php
class NotifyController extends Controller {
  // @call POST /notify
  public function notifyAction(): Payload {
    // ...

    if ($user->settings->isTrue('notifyViaSms')) {
      $notifier = new SmsNotifier();
      $notifier->send($user->phone, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaPush')) {
      $notifier = new PushNotifier();
      $notifier->send($user->devid, $subject, $message);
    }
    if ($user->settings->isTrue('notifyViaEmail')) {
      $notifier = new EmailNotifier();
      $notifier->send($user->email, $subject, $message);
    }

    // ...
  }
}
```

Let's do it better for `NotifyController`, making it more programmatic and less verbosive, using a factory to get notifier instances by a user settings.

```php
class NotifierFactory {
  public static function generate(User $user): iterable {
    if ($user->settings->isTrue('notifyViaSms')) {
      yield [$user->phone, new SmsNotifier()];
    }
    if ($user->settings->isTrue('notifyViaPush')) {
      yield [$user->devid, new PushNotifier()];
    }
    if ($user->settings->isTrue('notifyViaEmail')) {
      yield [$user->email, new EmailNotifier()];
    }
  }
}
```


```php
class NotifyController extends Controller {
  // @call POST /notify
  public function notifyAction(): Payload {
    // ...

    // Iterate over available notifier instances & call send() for all.
    foreach (NotifierFactory::generate($user) as [$target, $notifier]) {
      $notifier->send($target, $subject, $message);
    }

    // ...
  }
}
```

## 5- Dependency Inversion Principle (DIP)
**Changes in the subclasses should not affect the superclasses.** In the other words, high-level classes should not depend on low-level classes, both should depend on abstractions. Also abstractions should not depend on details, details (concrete implementations) should depend on abstractions. So, we can use abstractions (mostly interfaces) between high-level and low-level classes to follow the principle.

Here we have an API endpoint `POST /log` for logging some application activities, `LogController` class with `logAction()` method for logging these activities, and `Logger` class as a service for these works.

**Bad practice:**

```php
// Each item is in its own file.
class FileLogger {
  public function log(string $data): void {
    // Put given log data into file.
  }
}

class Logger {
  public function __construct(
    private readonly FileLogger $logger
  ) {}
}
```

```php
class LogController extends Controller {
  // @call POST /log
  public function logAction(): Payload {
    // Auth / token check here.

    $logger = new Logger();
    $logger->log($this->request->post('log'));

    return $this->jsonPayload(Status::OK);
  }
}
```

Since `Logger` class is using a specific logger implementation, this is not flexible code at all and will cause problems if any replacement or additional log service becomes required over time. We will need to change `Logger` class whenever we want to send the logs to a database or other places.

So here, what can solve this issue dropping that concrete class injection (detailed implementation) from `Logger` constructor and using an abstraction (interface) without changing the client code.

**Better practice:**

```php
// Each item is in its own file.
interface ILogger {
  public function log(string $data): void;
}

class FileLogger implements ILogger {
  public function log(string $data): void {
    // Put given log data into file.
  }
}

// For future, maybe.
class DatabaseLogger implements ILogger {
  public function log(string $data): void {
    // Put given log data into database.
  }
}

class Logger {
  public function __construct(
    private readonly ILogger $logger
  ) {}
}
```