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:
| Abbreviation | Principle |
|---|---|
| P | Polymorphism |
| I | Inheritance |
| E | Encapsulation |
| S | Abstraction |
โ 1. P โ Polymorphism (P)
- Definition: The ability for different classes to respond to the same method call in different ways.
- Example:
- A
Birdclass and aDogclass can both have aspeak()method. - When you call
speak()on aBirdobject, it chirps. On aDog, it barks.
- A
- 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
Carcan inherit from a classVehicle, meaning it gets all the behavior ofVehiclebut can also have extra features likeairConditioning.
- A class
- 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.
- You use a
- 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:
| Principle | What it means | Why it matters, Drupal Example |
|---|---|---|
| Polymorphism | Same method, different behavior | Flexibility in code |
| One interface, many implementations | Block plugins, formatter plugins, validator plugins | |
| Inheritance | Reuse code via parent-child relationships | Avoids duplication, DRY principle |
| A class extends another | BlockBase, FormBase, ControllerBase, ConfigFormBase | |
| Encapsulation | Keep data private and safe | Security and easier debugging |
| Hide internal state, expose via methods | Services using protected properties and DI (e.g., ConfigFactoryInterface) | |
| Abstraction | Show only whatโs needed | Simplifies 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.
| Letter | Principle |
|---|---|
| S | Single Responsibility Principle |
| O | Open/Closed Principle |
| L | Liskov Substitution Principle |
| I | Interface Segregation Principle |
| D | Dependency 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 logicMailerServiceโ handles emailsLoggerServiceโ 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:
Queryclasses โ for building queriesSchemaโ for table structure managementSelect,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 injectionPluginInspectionInterfaceโ for plugin metadataConfigEntityInterface,ContentEntityInterfaceโ for different types of entitiesFormInterface,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
| Principle | Name | Key Idea, In Drupal |
|---|---|---|
| SRP | Single Responsibility | Each class does one thing only |
| One class = one responsibility | Easier to maintain | |
Separate logic into services, pluginsConnection, Query, Schema classes | ||
| OCP | Open/Closed | Code is extendable, not modifiable |
| Extend without modifying | Safer to add features | |
| Extend with plugins or events Plugins, services, custom blocks | ||
| LSP | Liskov Substitution | Subclasses replace base classes without side effects |
| Subclasses respect base contracts | Robust inheritance | |
| Blocks, formatters, forms All blocks replaceable via BlockPluginInterface | ||
| ISP | Interface Segregation | Use many small interfaces, not large all-in-ones |
| Small, focused interfaces | No extra responsibility | |
ContainerInjectionInterface, etc.ContainerInjectionInterface, PluginInterface | ||
| DIP | Dependency Inversion | Depend on interfaces, not concrete classes |
| Depend on abstractions | Better testability & flexibility | |
| Depend on interfaces, not concrete classes | Inject via service container DI through interfaces like ConfigFactoryInterface |
๐ Summary
| Principle | Real Drupal Practice |
|---|---|
| SRP | Split form building, saving, and email into separate services |
| OCP | Use plugins, events, services โ avoid modifying core/contrib code |
| LSP | Custom blocks follow the same interface contract as core blocks |
| ISP | Implement only what you need (e.g., ContainerInjectionInterface alone if needed) |
| DIP | Depend on interfaces like ConfigFactoryInterface, not concrete service classes |
Comments