Skip to main content
Home
Drupal life hacks

Main navigation

  • Drupal
  • React
  • WP
  • Contact
  • About
User account menu
  • Log in

Breadcrumb

  1. Home

Mastering OOP and SOLID Principles in PHP with Drupal Examples: A Complete Guide

By admin, 23 June, 2025

Object-Oriented Programming (OOP) is a programming paradigm based on the concept of "objects", which can contain data (fields, often called attributes or properties) and code (methods, which are functions associated with the object).

There are 4 main principles of OOP, often remembered by the acronym "PIES":

๐Ÿ”  What is PIES?

PIES stands for the four core OOP principles:

AbbreviationPrinciple
PPolymorphism
IInheritance
EEncapsulation
SAbstraction

โœ… 1. P โ€” Polymorphism (P)

  • Definition: The ability for different classes to respond to the same method call in different ways.
  • Example:
    • A Bird class and a Dog class can both have a speak() method.
    • When you call speak() on a Bird object, it chirps. On a Dog, it barks.
  • Benefit: Allows for flexible and extensible code.

One interface, many implementations

Drupal makes great use of polymorphism, especially via its plugin system.

๐Ÿ“Œ Example: Blocks (BlockPluginInterface)

class MyCustomBlock extends BlockBase {
  public function build() {
    return ['#markup' => $this->t('Hello world!')];
  }
}

You can create as many block plugins as you want. Each one implements the same interface, but the behavior inside build() is completely different.

๐Ÿ“Œ This is polymorphism โ€” you call $block->build() regardless of what block it is. The output depends on the underlying class.


โœ… 2. I โ€” Inheritance (I)

  • Definition: A way to form new classes using classes that have already been defined.
  • Example:
    • A class Car can inherit from a class Vehicle, meaning it gets all the behavior of Vehicle but can also have extra features like airConditioning.
  • Benefit: Promotes code reuse and hierarchical classification.

A class extends another to reuse or override functionality

Inheritance is everywhere in Drupal, especially when creating forms, blocks, controllers, etc.

๐Ÿ“Œ Example: Configuration form

use Drupal\Core\Form\ConfigFormBase;

class MySettingsForm extends ConfigFormBase {
  public function buildForm(...) {
    // Form elements for site configuration
  }

  public function submitForm(...) {
    // Save configuration values
  }
}

By extending ConfigFormBase, you get a full config-backed form system, routing integration, validation โ€” without writing boilerplate.


โœ… 3. E โ€” Encapsulation (E)

  • Definition: Bundling data and the methods that operate on that data within one unit (class), and restricting access to some of the object's components.
  • Example:
    • An objectโ€™s internal state is hidden from the outside world, and accessed only through getter/setter methods.
  • Benefit: Protects the integrity of data and simplifies maintenance.

Hide internal details; expose only what's needed

Drupal uses protected/private properties and getters/setters or methods to restrict direct access to data.

๐Ÿ“Œ Example: Service accessing configuration

class MyService {
  protected $config;

  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->config = $config_factory->get('my_module.settings');
  }

  public function getApiKey() {
    return $this->config->get('api_key');
  }
}

Here, $config is a protected property โ€” external code canโ€™t access it directly, only via getApiKey().

๐Ÿ“Œ This is encapsulation: the class controls access to its data and how itโ€™s exposed.


โœ… 4. S โ€” Abstraction (S โ€“ Sometimes represented by the 'S' in SOLID, but conceptually the fourth pillar)

  • Definition: Hiding complex internal details and showing only the necessary features of an object.
  • Example:
    • You use a print() method without knowing how the printer communicates with the computer.
  • Benefit: Reduces complexity and allows the programmer to focus on interactions at a higher level.

Expose high-level usage while hiding internal complexity

Drupal provides interfaces, traits, and abstract base classes to let you work at a high level without worrying about low-level details.

๐Ÿ“Œ Example: Translation service

$this->t('Hello world!');

When you call $this->t() inside a class using StringTranslationTrait, you donโ€™t need to know:

  • How translations are stored
  • How caching works
  • Where it writes translation strings

๐Ÿ“Œ You just use the high-level API. Abstraction hides all complexity behind a clean, simple method.


Summary Table:

PrincipleWhat it meansWhy it matters, Drupal Example
PolymorphismSame method, different behaviorFlexibility in code
One interface, many implementationsBlock plugins, formatter plugins, validator plugins
InheritanceReuse code via parent-child relationshipsAvoids duplication, DRY principle
A class extends anotherBlockBase, FormBase, ControllerBase, ConfigFormBase
EncapsulationKeep data private and safeSecurity and easier debugging
Hide internal state, expose via methodsServices using protected properties and DI (e.g., ConfigFactoryInterface)
AbstractionShow only whatโ€™s neededSimplifies how we use complex systems
Hide complexity behind simple interfaces$this->t(), \Drupal::service(), DI interfaces like LoggerInterface

 

A detailed explanation of SOLID principles in object-oriented programming, using Python syntax for consistency (just like your original), with examples and context relevant to professional PHP development โ€” including Drupal usage where applicable.


๐Ÿ”  What is SOLID?

SOLID is an acronym for five key object-oriented design principles that help build scalable, maintainable, and testable software architecture.

LetterPrinciple
SSingle Responsibility Principle
OOpen/Closed Principle
LLiskov Substitution Principle
IInterface Segregation Principle
DDependency Inversion Principle

โœ… 1. S โ€” Single Responsibility Principle (SRP)

A class should have only one reason to change.

One class = one responsibility

This means a class should do only one thing โ€” one clear area of responsibility.

โŒ Bad Example:

class Report:
    def generate(self):
        pass

    def save_to_file(self):
        pass

This class handles both report generation and file saving โ€” two responsibilities.

โœ… Good Example:

class ReportGenerator:
    def generate(self):
        pass

class ReportSaver:
    def save(self, report):
        pass

Now each class has only one job, making the code cleaner and more maintainable.

๐Ÿงฑ PHP Example:

In other words, each class should handle one responsibility only.

โŒ Bad:

class User {
    public function saveToDatabase() {
        // logic to save to DB
    }

    public function sendEmail() {
        // logic to send email
    }
}

The User class has two responsibilities: persistence and communication.

โœ… Good:

class User {
    // just user properties
}

class UserRepository {
    public function save(User $user) {
        // save logic
    }
}

class EmailService {
    public function sendEmail(User $user) {
        // email logic
    }
}

Now each class is focused and has a single purpose.

๐Ÿ“Œ Example in Drupal:

Instead of writing a Form class that also sends emails and logs activity:

class MyForm extends FormBase {
  public function submitForm(...) {
    // send email
    // log message
    // update config
  }
}

Split into:

  • MyForm โ†’ handles UI/form logic
  • MailerService โ†’ handles emails
  • LoggerService โ†’ handles logging

This makes each class easier to maintain and test.

๐Ÿงฑ Drupal Example:

The class \Drupal\Core\Database\Connection is responsible only for managing the database connection.

Other responsibilities are delegated to:

  • Query classes โ†’ for building queries
  • Schema โ†’ for table structure management
  • Select, Insert, Update, Delete โ†’ for specific query types

๐Ÿ“Œ Conclusion: Each class handles a single concern. This is clean SRP in practice.

 

๐Ÿ“Œ Real example from your custom module:

โŒ Bad: One class does too much โ€” builds the form, validates input, saves data, sends email.

class UserProfileForm extends FormBase {
  public function buildForm(...) {
    // builds the form
  }

  public function validateForm(...) {
    // form validation
  }

  public function submitForm(...) {
    // saves to DB, sends email, logs something
  }
}

โœ… Good: Separate responsibilities

class UserProfileForm extends FormBase {
  protected UserSaverInterface $saver;
  protected MailerInterface $mailer;

  public function submitForm(...) {
    $this->saver->save($data);
    $this->mailer->sendNotification($data['email']);
  }
}

๐Ÿ”น UserSaver โ€” handles data persistence
๐Ÿ”น Mailer โ€” handles email sending
๐Ÿ”น Form class โ€” handles the form only

๐Ÿ“Œ This is classic SRP: each class has only one reason to change.


โœ… 2. O โ€” Open/Closed Principle (OCP)

Software entities should be open for extension, but closed for modification.

You should be able to add new functionality without changing existing code.

โœ… Example:

class Shape:
    def area(self):
        pass

class Circle(Shape):
    def __init__(self, radius):
        self.radius = radius

    def area(self):
        return 3.14 * self.radius ** 2

To support a new shape, you simply add another class โ€” no need to modify Shape.

๐Ÿงฑ PHP Example:

You shouldnโ€™t have to change existing code to add new features โ€” instead, you extend it.

โŒ Bad:

class Discount {
    public function calculate($type) {
        if ($type === 'regular') return 10;
        if ($type === 'vip') return 20;
        return 0;
    }
}

Adding a new discount type requires modifying the method.

โœ… Good:

interface DiscountStrategy {
    public function calculate(): int;
}

class RegularDiscount implements DiscountStrategy {
    public function calculate(): int {
        return 10;
    }
}

class VipDiscount implements DiscountStrategy {
    public function calculate(): int {
        return 20;
    }
}

class DiscountCalculator {
    public function getDiscount(DiscountStrategy $strategy): int {
        return $strategy->calculate();
    }
}

You can now add new strategies without changing existing code.

๐Ÿ“Œ Example in Drupal:

Use plugins or event subscribers instead of modifying core behavior.

Instead of:

// modifying an existing formatter

Do:

/**
 * @FieldFormatter(
 *   id = "custom_formatter",
 *   label = @Translation("My custom formatter"),
 *   field_types = { "string" }
 * )
 */
class CustomFormatter extends FormatterBase {
  public function viewElements(...) {
    // your custom logic
  }
}

Youโ€™ve added new behavior without changing existing classes.

๐Ÿงฑ Drupal Example:

Drupalโ€™s plugin system allows extending functionality without changing the core code.

class MyCustomBlock extends BlockBase {
  public function build() {
    return ['#markup' => $this->t('Hello!')];
  }
}

You're adding a new block type without touching BlockBase or BlockPluginInterface.

๐Ÿ“Œ Conclusion: Drupalโ€™s plugin architecture supports OCP by design.

 

๐Ÿ“Œ Example: Creating a new field formatter

interface FieldFormatterInterface {
  public function format($value): array;
}

โœ… You define a new plugin, without touching the base system:

/**
 * @FieldFormatter(
 *   id = "custom_link",
 *   label = @Translation("Custom link"),
 *   field_types = {
 *     "link"
 *   }
 * )
 */
class CustomLinkFormatter extends FormatterBase implements FieldFormatterInterface {
  public function viewElements(...) {
    // custom rendering logic
  }
}

๐Ÿ“Œ Youโ€™re extending, not modifying existing formatters like LinkFormatter.


โœ… 3. L โ€” Liskov Substitution Principle (LSP)

Subclasses should be substitutable for their base classes without breaking the application.

A subclass should behave in a way that doesnโ€™t surprise the code that uses it.

โŒ Bad Example:

class Bird:
    def fly(self): pass

class Penguin(Bird):
    def fly(self): raise Exception("Penguins can't fly!")

If code expects a bird to fly, a Penguin will break expectations.

โœ… Good Example:

class Bird:
    pass

class FlyingBird(Bird):
    def fly(self):
        pass

class Sparrow(FlyingBird):
    def fly(self):
        print("Sparrow is flying")

class Penguin(Bird):
    def swim(self):
        print("Penguin is swimming")

Now all behavior is consistent and logical.

๐Ÿงฑ PHP Example:

In practice, subclasses must respect the contracts defined by their parent classes.

โŒ Bad:

class Bird {
    public function fly() {
        echo "Flying!";
    }
}

class Penguin extends Bird {
    public function fly() {
        throw new Exception("Penguins can't fly!");
    }
}

Penguin violates LSP because it breaks expectations.

โœ… Good:

abstract class Bird {}

interface CanFly {
    public function fly();
}

class Sparrow extends Bird implements CanFly {
    public function fly() {
        echo "Sparrow is flying!";
    }
}

class Penguin extends Bird {
    public function swim() {
        echo "Penguin is swimming!";
    }
}

Subtypes honor the contract of their base types.


๐Ÿงฑ Drupal Example:

Any custom block extending BlockBase should work wherever a block is expected.

class CustomBlock extends BlockBase {
  public function build() {
    return ['#markup' => $this->t('Custom block')];
  }
}

You can place this block in any region โ€” the system treats it the same as any other block.

If you returned invalid render arrays or threw exceptions, you'd break the LSP and possibly crash the site.

 

๐Ÿ“Œ Conclusion: As long as the block respects BlockPluginInterface, it will integrate smoothly โ€” fulfilling LSP.


โœ… 4. I โ€” Interface Segregation Principle (ISP)

Prefer many small interfaces over one large, all-encompassing one.

Clients should not be forced to implement methods they do not use.

Split large interfaces into smaller, focused ones.

โŒ Bad Example:

class Machine:
    def print(self): pass
    def scan(self): pass
    def fax(self): pass

What if your machine only prints?

โœ… Good Example:

class Printer:
    def print(self): pass

class Scanner:
    def scan(self): pass

Now classes implement only what they need.

๐Ÿงฑ PHP Example:

Better to have several small interfaces than a large one with unused methods.

โŒ Bad:

interface Worker {
    public function work();
    public function eat();
}

class Robot implements Worker {
    public function work() {}
    public function eat() {
        throw new Exception("Robots don't eat");
    }
}

Robot shouldnโ€™t be forced to implement eat().

โœ… Good:

interface Workable {
    public function work();
}

interface Eatable {
    public function eat();
}

class Human implements Workable, Eatable {
    public function work() {}
    public function eat() {}
}

class Robot implements Workable {
    public function work() {}
}

Now, each class implements only what it needs.

This means your classes can implement only what they need and remain focused.

๐Ÿงฑ Drupal Example:

Drupal provides small, specific interfaces:

  • ContainerInjectionInterface โ€” for dependency injection
  • PluginInspectionInterface โ€” for plugin metadata
  • ConfigEntityInterface, ContentEntityInterface โ€” for different types of entities
  • FormInterface, ConfigFormBase, EntityFormInterface โ†’ for specific form contexts

๐Ÿ“Œ Example:
If you just need DI via the container, you only implement ContainerInjectionInterface โ€” you're not forced to implement anything else.

๐Ÿ“Œ Conclusion: Drupal promotes narrow interfaces that align perfectly with ISP.

 

๐Ÿ“Œ Example: Only implement what you need

Need access to services from the container? Just implement:

use Drupal\Core\DependencyInjection\ContainerInjectionInterface;

class MyService implements ContainerInjectionInterface {
  public static function create(ContainerInterface $container) {
    return new static(
      $container->get('config.factory')
    );
  }
}

๐Ÿ“Œ Youโ€™re not forced to implement ConfigEntityInterface or LoggerAwareInterface unless theyโ€™re relevant to your class.


โœ… 5. D โ€” Dependency Inversion Principle (DIP)

High-level modules should depend on abstractions, not on concrete implementations.

High-level classes shouldnโ€™t depend on low-level ones directly โ€” use interfaces or abstract classes.

โŒ Bad Example:

class MySQLDatabase:
    def connect(self): pass

class App:
    def __init__(self):
        self.db = MySQLDatabase()

Tightly coupled. You canโ€™t easily test or swap the DB.

โœ… Good Example:

class DatabaseInterface:
    def connect(self): pass

class MySQLDatabase(DatabaseInterface):
    def connect(self): pass

class App:
    def __init__(self, db: DatabaseInterface):
        self.db = db

Now you can easily pass a different database (e.g., PostgreSQL, SQLite, or a mock).

๐Ÿงฑ PHP Example:

Instead of hard-coding dependencies, inject interfaces, not concrete implementations.

โŒ Bad:

class MySQLConnection {
    public function connect() {}
}

class PasswordReminder {
    private $db;

    public function __construct() {
        $this->db = new MySQLConnection();
    }
}

Tightly coupled โ€” you canโ€™t replace MySQLConnection.

โœ… Good:

interface DBConnectionInterface {
    public function connect();
}

class MySQLConnection implements DBConnectionInterface {
    public function connect() {
        // connect to MySQL
    }
}

class PasswordReminder {
    private $db;

    public function __construct(DBConnectionInterface $db) {
        $this->db = $db;
    }
}

Now you can easily swap or mock the DB connection.

๐Ÿ“Œ Example in Drupal:

Bad:

$this->mailer = new DrupalMailer(); // tight coupling

Good:

use Drupal\Core\Mail\MailManagerInterface;

class MyService {
  protected $mailer;

  public function __construct(MailManagerInterface $mailer) {
    $this->mailer = $mailer;
  }
}

This makes your service loosely coupled, easier to test, and more flexible.

๐Ÿงฑ Drupal Example:

Drupal uses Symfony's service container and interface-based dependency injection.

use Drupal\Core\Config\ConfigFactoryInterface;

class MyService {
  protected $configFactory;

  public function __construct(ConfigFactoryInterface $config_factory) {
    $this->configFactory = $config_factory;
  }

  public function getSiteName() {
    return $this->configFactory->get('system.site')->get('name');
  }
}

You're injecting ConfigFactoryInterface, not a concrete class.

๐Ÿ“Œ Benefits:

  • Easier testing (can mock interface)
  • Easier substitution
  • Follows DIP exactly

๐Ÿ“Œ Example: Service using ConfigFactoryInterface, not hardcoded class

use Drupal\Core\Config\ConfigFactoryInterface;

class SiteNamePrinter {
  protected ConfigFactoryInterface $configFactory;

  public function __construct(ConfigFactoryInterface $configFactory) {
    $this->configFactory = $configFactory;
  }

  public function printName() {
    return $this->configFactory->get('system.site')->get('name');
  }
}

๐Ÿ“Œ Now you can mock ConfigFactoryInterface in unit tests โ€” no need for real config objects.


๐Ÿงฉ Bonus: Service definition in .services.yml

services:
  my_module.site_name_printer:
    class: Drupal\my_module\SiteNamePrinter
    arguments: ['@config.factory']

๐Ÿ”š Summary Table

PrincipleNameKey Idea, In Drupal
SRPSingle ResponsibilityEach class does one thing only
One class = one responsibilityEasier to maintain
Separate logic into services, plugins
Connection, Query, Schema classes
OCPOpen/ClosedCode is extendable, not modifiable
Extend without modifyingSafer to add features
Extend with plugins or events
Plugins, services, custom blocks
LSPLiskov SubstitutionSubclasses replace base classes without side effects
Subclasses respect base contractsRobust inheritance
Blocks, formatters, forms
All blocks replaceable via BlockPluginInterface
ISPInterface SegregationUse many small interfaces, not large all-in-ones
Small, focused interfacesNo extra responsibility
ContainerInjectionInterface, etc.
ContainerInjectionInterface, PluginInterface
DIPDependency InversionDepend on interfaces, not concrete classes
Depend on abstractionsBetter testability & flexibility
Depend on interfaces, not concrete classesInject via service container
DI through interfaces like ConfigFactoryInterface

๐Ÿ”š Summary

PrincipleReal Drupal Practice
SRPSplit form building, saving, and email into separate services
OCPUse plugins, events, services โ€” avoid modifying core/contrib code
LSPCustom blocks follow the same interface contract as core blocks
ISPImplement only what you need (e.g., ContainerInjectionInterface alone if needed)
DIPDepend on interfaces like ConfigFactoryInterface, not concrete service classes

 

 

 

 

 

Tags

  • #Drupal Planet
  • OOP
  • SOLID

Comments

About text formats

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • Lines and paragraphs break automatically.
  • Web page addresses and email addresses turn into links automatically.
Powered by Drupal