PHP Classes and OOP — Objects, Properties, Methods, and Design Patterns
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
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.
What does constructor property promotion do? It combines property declaration and constructor initialization into a single syntax, reducing boilerplate code.
What is a readonly property? A property that can only be set once, typically in the constructor. Attempting to modify it causes an error.
Why should you avoid static methods? Static methods create tight coupling, are hard to mock in tests, and cannot be swapped with dependency injection.
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 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