Online admissions open for the SEO, PHP web development and networking & cybersecurity courses. Enquire for the next batch.

Object-Oriented PHP: Classes, Interfaces, Traits and Enums

6 min read Updated By the SkillNewton team

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.6

Each 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.

  1. Install Composer and run composer init in your project folder.
  2. Add an autoload section to composer.json that maps the App\ namespace to the src/ folder, as shown below.
  3. Save each class in a file named after the class, in a folder matching its namespace: App\Billing\UpiPayment lives in src/Billing/UpiPayment.php and starts with namespace App\Billing;.
  4. Run composer dump-autoload to generate vendor/autoload.php.
  5. In your entry script, require vendor/autoload.php once and import classes with use App\Billing\UpiPayment;.
  6. Commit composer.json and composer.lock to version control with Git, but add the vendor/ folder to .gitignore.
{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    }
}

Design habits for maintainable object-oriented code

HabitWhy it helps
Give each class one clear jobSmall classes are easier to name, test and replace
Pass dependencies into the constructorYou can swap a real database for a test double
Prefer interfaces and composition to deep inheritanceChanges to a parent class do not ripple through many children
Make properties private by defaultOther code must use methods, so rules stay in one place
Mark classes final unless designed for extensionNobody relies on internals you did not intend to share
Use declare(strict_types=1) and type every propertyType 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 Book class with promoted readonly isbn and title properties.
  • Create a backed enum LoanStatus with Available, OnLoan and Overdue cases and a label() method.
  • Define a Notifier interface and two implementations, EmailNotifier and SmsNotifier, that simply echo a message.
  • Write a Library class that receives a Notifier through its constructor and has lend() and return() 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.

Want to Learn This Properly?

The Advanced PHP Web Development & Full-Stack Application Development (Online) turns guides like this into practised skills, with projects on real websites.

WhatsApp Call Us Now

Free counselling

Send an Enquiry

Tell us which course you are interested in. We reply by email or phone with the next batch and how to enrol.

We reply by email or phone within working hours. No spam.