Skip to content

PHP Classes and OOP — Objects, Properties, Methods, and Design Patterns

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about PHP Classes and OOP. We cover key concepts, practical examples, and best practices to help you master this topic.

PHP classes are blueprints for creating objects that encapsulate data and behavior through properties and methods, supporting visibility modifiers, inheritance, interfaces, and traits for modular application design.

Why It Matters

Object-oriented programming is the standard paradigm for modern PHP applications. Frameworks like Laravel and Symfony are built entirely around OOP concepts. Classes let you model real-world entities -- users, products, orders, payments -- as objects with data and behavior together. Understanding Encapsulation, inheritance, and polymorphism is essential for building maintainable, testable applications. Without OOP, complex applications become unmanageable spaghetti code.

Real-World Use

Laravel's Eloquent ORM represents database rows as objects with methods for relationships. Symfony's Dependency Injection container manages object creation and wiring. WordPress uses classes for WP_Query, WP_User, and WP_Post. E-commerce systems model products, carts, and orders as classes. DodaTech's licensing system uses classes for License, Product, and Customer entities with validation methods.

What You Will Learn

  • Defining classes with properties and methods
  • Visibility: public, protected, private
  • Constructors and destructors
  • Static properties and methods
  • Magic methods: __get, __set, __call, __toString
  • Type declarations for properties (PHP 7.4+)
  • Readonly properties (PHP 8.1+)
  • Basic SOLID principles

Learning Path

flowchart LR
  A[Error Handling] --> B[Classes and OOP
You are here] B --> C[Inheritance and Traits] C --> D[Namespaces] D --> E[PDO] style B fill:#f90,color:#fff

Defining a Class

A basic class with properties and methods:

<?php
class User {
    // Properties
    public string $name;
    public string $email;
    private string $password;
    protected array $roles = [];

    // Constructor
    public function __construct(string $name, string $email, string $password) {
        $this->name = $name;
        $this->email = $email;
        $this->setPassword($password);
    }

    // Methods
    public function getDisplayName(): string {
        return $this->name;
    }

    public function hasRole(string $role): bool {
        return in_array($role, $this->roles);
    }

    private function setPassword(string $password): void {
        $this->password = password_hash($password, PASSWORD_DEFAULT);
    }

    public function verifyPassword(string $password): bool {
        return password_verify($password, $this->password);
    }
}

// Usage
$user = new User('Alice', 'alice@example.com', 'secret123');
echo $user->getDisplayName();  // Alice
echo $user->hasRole('admin') ? 'Admin' : 'User';  // User

Property Visibility

<?php
class VisibilityExample {
    public string $public = 'accessible everywhere';
    protected string $protected = 'accessible in class and subclasses';
    private string $private = 'accessible only in this class';

    public function testAccess(): void {
        echo $this->public;    // OK
        echo $this->protected; // OK
        echo $this->private;   // OK
    }
}

$obj = new VisibilityExample();
echo $obj->public;     // OK
// echo $obj->protected; // Error
// echo $obj->private;   // Error

When to Use Each

Visibility Description
public Accessible from anywhere. Use for the class interface.
protected Accessible in the class and subclasses. Use for internal methods subclasses may override.
private Accessible only in the defining class. Use for internal implementation details.

Typed Properties

PHP 7.4 introduced type declarations for class properties:

<?php
class Product {
    public int $id;
    public string $title;
    public float $price;
    public bool $available = false;
    public ?string $description = null;
    public array $tags = [];
    public \DateTimeImmutable $createdAt;

    public function __construct(int $id, string $title, float $price) {
        $this->id = $id;
        $this->title = $title;
        $this->price = $price;
        $this->createdAt = new \DateTimeImmutable();
    }
}

Constructor Property Promotion

PHP 8 introduced constructor property promotion, eliminating boilerplate:

<?php
// Before PHP 8
class UserOld {
    public string $name;
    public string $email;

    public function __construct(string $name, string $email) {
        $this->name = $name;
        $this->email = $email;
    }
}

// PHP 8+
class UserNew {
    public function __construct(
        public string $name,
        public string $email,
        private string $password,
    ) {}
}

Readonly Properties

PHP 8.1 introduced readonly properties that can only be set once:

<?php
class Configuration {
    public function __construct(
        public readonly string $dbHost,
        public readonly int $dbPort,
        public readonly string $appEnv,
    ) {}

    // This would error:
    // public function update(string $env): void { $this->appEnv = $env; }
}

$config = new Configuration('localhost', 5432, 'production');
echo $config->dbHost;  // localhost
// $config->dbHost = 'other';  // Error: readonly property

Static Properties and Methods

Static members belong to the class, not to instances:

<?php
class Logger {
    private static array $logs = [];
    private static int $maxLogs = 100;

    public static function log(string $message): void {
        self::$logs[] = '[' . date('H:i:s') . '] ' . $message;
        if (count(self::$logs) > self::$maxLogs) {
            array_shift(self::$logs);
        }
    }

    public static function getLogs(): array {
        return self::$logs;
    }

    public static function clear(): void {
        self::$logs = [];
    }
}

// Usage -- no instance needed
Logger::log('Application started');
Logger::log('User logged in');
print_r(Logger::getLogs());

Magic Methods

Magic methods provide hooks into PHP's object system:

<?php
class DynamicConfig {
    private array $data = [];

    // Called when reading inaccessible properties
    public function __get(string $name): mixed {
        return $this->data[$name] ?? null;
    }

    // Called when writing to inaccessible properties
    public function __set(string $name, mixed $value): void {
        $this->data[$name] = $value;
    }

    // Called when checking isset on inaccessible properties
    public function __isset(string $name): bool {
        return isset($this->data[$name]);
    }

    // Called when calling inaccessible methods
    public function __call(string $name, array $arguments) {
        if (isset($this->data[$name])) {
            $callable = $this->data[$name];
            return $callable(...$arguments);
        }
        throw new \BadMethodCallException("Method $name not found");
    }

    // Called when converting object to string
    public function __toString(): string {
        return json_encode($this->data, JSON_PRETTY_PRINT);
    }
}

$config = new DynamicConfig();
$config->api_key = 'abc123';
$config->timeout = 30;
echo $config->api_key;  // abc123
echo (string)$config;   // {"api_key":"abc123","timeout":30}

Method Chaining

Return $this from methods to enable fluent interfaces:

<?php
class QueryBuilder {
    private array $select = ['*'];
    private string $from = '';
    private array $where = [];
    private ?int $limit = null;

    public function select(array $columns): static {
        $this->select = $columns;
        return $this;
    }

    public function from(string $table): static {
        $this->from = $table;
        return $this;
    }

    public function where(string $column, string $operator, mixed $value): static {
        $this->where[] = [$column, $operator, $value];
        return $this;
    }

    public function limit(int $limit): static {
        $this->limit = $limit;
        return $this;
    }

    public function build(): string {
        $sql = 'SELECT ' . implode(', ', $this->select);
        $sql .= ' FROM ' . $this->from;
        if ($this->where) {
            $conditions = array_map(fn($w) => "{$w[0]} {$w[1]} ?", $this->where);
            $sql .= ' WHERE ' . implode(' AND ', $conditions);
        }
        if ($this->limit) {
            $sql .= ' LIMIT ' . $this->limit;
        }
        return $sql;
    }
}

// Fluent interface
$query = (new QueryBuilder())
    ->select(['id', 'name', 'email'])
    ->from('users')
    ->where('active', '=', 1)
    ->where('role', '=', 'admin')
    ->limit(10)
    ->build();

echo $query;
// SELECT id, name, email FROM users WHERE active = ? AND role = ? LIMIT 10

SOLID Principles Overview

Single Responsibility

A class should have one reason to change:

<?php
// Bad: mixed responsibilities
class Invoice {
    public function calculateTotal(): float { /* ... */ }
    public function generatePdf(): string { /* ... */ }
    public function sendEmail(): bool { /* ... */ }
}

// Good: separated responsibilities
class InvoiceCalculator { public function calculateTotal(Invoice $invoice): float {} }
class InvoicePdfGenerator { public function generate(Invoice $invoice): string {} }
class InvoiceMailer { public function send(Invoice $invoice, string $to): bool {} }

Open/Closed

Classes should be open for extension, closed for modification:

<?php
interface PaymentMethod {
    public function process(float $amount): bool;
}

class CreditCardPayment implements PaymentMethod {
    public function process(float $amount): bool { /* ... */ }
}

class PayPalPayment implements PaymentMethod {
    public function process(float $amount): bool { /* ... */ }
}

class PaymentProcessor {
    public function process(PaymentMethod $method, float $amount): bool {
        return $method->process($amount);
    }
}

Common Mistakes

1. Making Everything Public

class User {
    public $password;  // Accessible from anywhere
}

Use private for sensitive data. Expose only what is needed.

2. Not Using Type Declarations

class Product {
    public $price;  // Could be string, int, float
}

Always type properties. It catches bugs at assignment time.

3. Creating God Classes

class Application {
    public function handleRequest(): void {}
    public function renderView(): void {}
    public function queryDatabase(): void {}
    public function sendEmail(): void {}
    public function generateReport(): void {}
}

Split into smaller focused classes following Single Responsibility.

4. Forgetting to Call Parent Constructor

class Child extends Parent {
    public function __construct() {
        // Missing: parent::__construct();
    }
}

5. Overusing Static Methods

Static methods are convenient but create tight coupling and make testing difficult. Use dependency injection instead.

Practice Questions

  1. What is the difference between public, protected, and private? Public is accessible from anywhere. Protected is accessible in the class and subclasses. Private is only accessible in the defining class.

  2. What does constructor property promotion do? It combines property declaration and constructor initialization into a single syntax, reducing boilerplate code.

  3. What is a readonly property? A property that can only be set once, typically in the constructor. Attempting to modify it causes an error.

  4. Why should you avoid static methods? Static methods create tight coupling, are hard to mock in tests, and cannot be swapped with dependency injection.

  5. Challenge: Build a complete PHP class hierarchy for an e-commerce system with Cart, CartItem, Product, Customer, and Order classes with proper visibility, type hints, and methods.

Mini Project: Shopping Cart Class

Build a type-safe shopping cart system:

<?php
class CartItem {
    public function __construct(
        public readonly int $productId,
        public readonly string $name,
        public readonly float $price,
        public int $quantity,
    ) {
        if ($quantity < 1) {
            throw new \InvalidArgumentException('Quantity must be at least 1');
        }
    }

    public function getSubtotal(): float {
        return $this->price * $this->quantity;
    }
}

class Cart {
    /** @var CartItem[] */
    private array $items = [];

    public function addItem(CartItem $item): void {
        $existingId = $this->findItemIndex($item->productId);
        if ($existingId !== null) {
            $this->items[$existingId] = new CartItem(
                $item->productId, $item->name, $item->price,
                $this->items[$existingId]->quantity + $item->quantity
            );
        } else {
            $this->items[] = $item;
        }
    }

    public function removeItem(int $productId): void {
        $this->items = array_values(array_filter(
            $this->items,
            fn(CartItem $item) => $item->productId !== $productId
        ));
    }

    public function getTotal(): float {
        return array_sum(array_map(fn(CartItem $item) => $item->getSubtotal(), $this->items));
    }

    public function getItemCount(): int {
        return array_sum(array_map(fn(CartItem $item) => $item->quantity, $this->items));
    }

    private function findItemIndex(int $productId): ?int {
        foreach ($this->items as $index => $item) {
            if ($item->productId === $productId) return $index;
        }
        return null;
    }

    public function getItems(): array { return $this->items; }
}

FAQ

What is the difference between a class and an object?

A class is a blueprint or template. An object is an instance of a class created with 'new'. Multiple objects can be created from one class.

What is encapsulation?

Encapsulation is hiding internal state and requiring all interaction through public methods. Use private properties and public getter/setter methods.

What is the $this keyword?

$this refers to the current object instance. It is used inside class methods to access properties and call other methods on the same object.

What is the self keyword?

self refers to the current class, not the current instance. Use self:: for static properties and methods. Use $this-> for instance members.

What is dependency injection?

Dependency injection passes dependencies to a class through its constructor instead of creating them internally. It makes code testable and flexible.

What is Next

Now that you understand classes, proceed to Inheritance and Traits to learn how to reuse code through class hierarchies and horizontal composition. Then read Namespaces and Autoload to organize your classes in a standard directory structure.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro