Object-Oriented PHP: Classes, Interfaces, Traits and Enums
What is object-oriented programming in PHP?
Object-oriented programming (OOP) in PHP is a way of organising code into classes, which are blueprints that bundle data (properties) with the behaviour that works on it (methods). You create objects from a class with new. Features such as visibility, inheritance, interfaces, traits and enums let you build larger applications from small, testable parts instead of long scripts full of loose functions.
Classes, objects and the vocabulary you need
A class describes a kind of thing, such as a product or a customer. An object is one concrete instance of that class, created with the new keyword. Properties are variables that belong to the object, and methods are functions that belong to it. Inside a method, $this refers to the current object, and the -> operator reaches its properties and methods.
<?php
declare(strict_types=1);
class Product
{
public string $name = '';
public float $price = 0.0;
public function priceWithTax(float $taxRate): float
{
return round($this->price * (1 + $taxRate), 2);
}
}
$pen = new Product();
$pen->name = 'Gel pen';
$pen->price = 20.0;
echo $pen->priceWithTax(0.18); // 23.6Each call to new creates a separate object with its own property values, so a $pen and a $notebook built from the same class never share a price. Objects are passed to functions by handle: if a function changes a property on an object it receives, the caller sees that change. Use clone when you need an independent copy. Typed properties, such as float $price, make PHP reject values of the wrong type instead of quietly converting them.
If you are new to the language itself, start with the PHP basics guide first; this guide assumes you can already write functions and arrays.
Constructors, visibility and promoted properties
A constructor is the special __construct() method that runs when an object is created, so the object starts in a valid state. Visibility controls who may touch each member:
public: accessible from anywhere.protected: accessible inside the class and its child classes.private: accessible only inside the class that declares it.
Since PHP 8.0, constructor property promotion lets you declare and assign properties in the constructor signature. Since PHP 8.1, a readonly property can be set once and never changed afterwards.
<?php
declare(strict_types=1);
final class Customer
{
public function __construct(
public readonly string $email,
private string $name,
protected int $loyaltyPoints = 0,
) {}
public function greeting(): string
{
return "Hello, {$this->name}";
}
}
$customer = new Customer('[email protected]', 'Asha');
echo $customer->greeting(); // Hello, Asha
echo $customer->email; // [email protected]
// $customer->name; Error: private property
// $customer->email = 'x'; Error: readonly property Inheritance, interfaces, abstract classes and traits in one example
An interface is a contract listing methods a class must provide. An abstract class can hold shared code and state but cannot be instantiated; its abstract methods must be written by child classes. A class extends one parent but can implement many interfaces. A trait copies reusable methods into any class with use, which helps when unrelated classes need the same helper.
<?php
declare(strict_types=1);
interface PaymentMethod
{
public function pay(int $amountInPaise): string;
}
trait LogsActivity
{
protected function log(string $message): void
{
error_log(static::class . ': ' . $message);
}
}
abstract class OnlinePayment implements PaymentMethod
{
use LogsActivity;
public function pay(int $amountInPaise): string
{
$this->log("Charging {$amountInPaise} paise");
return $this->charge($amountInPaise);
}
abstract protected function charge(int $amountInPaise): string;
}
final class UpiPayment extends OnlinePayment
{
public function __construct(private string $upiId) {}
protected function charge(int $amountInPaise): string
{
return "Requested {$amountInPaise} paise from {$this->upiId}";
}
}
$method = new UpiPayment('asha@okbank');
echo $method->pay(49900);Code elsewhere can type-hint PaymentMethod and accept any class that implements it, so adding a card or wallet payment later does not change the checkout code. Storing money as an integer number of paise also avoids floating-point rounding errors.
Notice how each feature has a distinct job. The interface states what any payment method must offer. The abstract class holds the steps every online payment shares, such as logging, and leaves the provider-specific part to its children. The trait supplies a helper that a completely unrelated class, such as a stock importer, could also use. The static::class expression inside the trait returns the name of the class actually running, so the log shows UpiPayment rather than the parent name.
Static members, readonly data and enums
A static property or method belongs to the class rather than to any object, and you reach it with ::. Enums, added in PHP 8.1, define a fixed set of allowed values. A backed enum maps each case to a string or integer, which suits values stored in a database.
<?php
declare(strict_types=1);
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Shipped = 'shipped';
public function label(): string
{
return match ($this) {
self::Pending => 'Awaiting payment',
self::Paid => 'Paid, packing soon',
self::Shipped => 'On its way',
};
}
}
final class Order
{
private static int $created = 0;
public function __construct(
public readonly int $id,
public readonly OrderStatus $status = OrderStatus::Pending,
) {
self::$created++;
}
public static function createdCount(): int
{
return self::$created;
}
}
$order = new Order(101, OrderStatus::from('paid'));
echo $order->status->label(); // Paid, packing soon
echo Order::createdCount(); // 1 Organising classes with namespaces and Composer autoloading
Namespaces stop two classes with the same name from clashing, and the PSR-4 standard maps each namespace to a folder. Composer then generates an autoloader, so you never write require for your own classes again.
- Install Composer and run
composer initin your project folder. - Add an autoload section to
composer.jsonthat maps theApp\namespace to thesrc/folder, as shown below. - Save each class in a file named after the class, in a folder matching its namespace:
App\Billing\UpiPaymentlives insrc/Billing/UpiPayment.phpand starts withnamespace App\Billing;. - Run
composer dump-autoloadto generatevendor/autoload.php. - In your entry script, require
vendor/autoload.phponce and import classes withuse App\Billing\UpiPayment;. - Commit
composer.jsonandcomposer.lockto version control with Git, but add thevendor/folder to.gitignore.
{
"autoload": {
"psr-4": {
"App\\": "src/"
}
}
} Design habits for maintainable object-oriented code
| Habit | Why it helps |
|---|---|
| Give each class one clear job | Small classes are easier to name, test and replace |
| Pass dependencies into the constructor | You can swap a real database for a test double |
| Prefer interfaces and composition to deep inheritance | Changes to a parent class do not ripple through many children |
| Make properties private by default | Other code must use methods, so rules stay in one place |
| Mark classes final unless designed for extension | Nobody relies on internals you did not intend to share |
| Use declare(strict_types=1) and type every property | Type errors surface early instead of producing odd output |
PHP 8.3 also adds the #[\Override] attribute, which makes PHP report an error if a method you meant to override does not exist in the parent class or interface. These ideas carry straight into MVC application structure, where models, controllers and services are all classes.
Practical exercise: model a small library system
Build a command-line programme that manages book loans using the features in this guide.
- Create a
Bookclass with promoted readonlyisbnandtitleproperties. - Create a backed enum
LoanStatuswithAvailable,OnLoanandOverduecases and alabel()method. - Define a
Notifierinterface and two implementations,EmailNotifierandSmsNotifier, that simply echo a message. - Write a
Libraryclass that receives aNotifierthrough its constructor and haslend()andreturn()methods. - Load everything through Composer PSR-4 autoloading under the
App\namespace.
You are done when php run.php lends and returns two books, prints status labels, switching from email to SMS needs a change in only one line, and trying to change a book title throws an error. To store the books later, pair this with the MySQL guide. The advanced PHP course builds on these patterns in larger projects.
Common mistakes
- Making every property public, so any code can put an object into an invalid state.
- Building deep inheritance chains to share code, when a trait or an injected helper object would be simpler.
- Creating database connections inside classes with
new PDO(), which makes them impossible to test without a real database. - Mismatching file paths and namespaces, so the PSR-4 autoloader cannot find the class.
- Using plain strings for fixed sets of values such as order status, instead of an enum that rejects typos.
- Forgetting that objects are passed by handle, then being surprised when a function changes an object the caller still uses.
Frequently asked questions
When should I use an interface instead of an abstract class?
Use an interface when you only need a contract that unrelated classes can share. Use an abstract class when several closely related classes need the same code or properties. A class can implement many interfaces but extend only one parent.
Do I need OOP to use Laravel or CodeIgniter?
Yes. Both frameworks are built from classes, namespaces and interfaces, and your controllers and models are classes that extend framework base classes. Understanding OOP first makes framework code far easier to read.
Is object-oriented PHP slower than procedural code?
For typical web applications the difference is negligible compared with database queries and network time. The structure OOP gives you makes code easier to test, change and optimise where it actually matters.
What is the difference between self and static?
self always refers to the class where the code is written. static refers to the class that was actually called at runtime, which matters when a child class inherits a static method. This is called late static binding.