Learn PHP: Objects and Classes
PHP is often introduced through variables, arrays, loops, and simple form handling, but once you start building real applications, you eventually run into a point where plain procedural code begins to feel cramped. That is usually the moment objects and classes start to matter. They are not just a fancy way to organize code; they are a practical way to model real things, keep related behavior together, and make your project easier to maintain as it grows. When you understand objects and classes in PHP, you stop writing code that merely works and begin writing code that can survive change without turning into a mess.
If you have ever built a blog, a store, a dashboard, or even a small API, you have probably already felt this pain. A page starts simple, then one feature becomes three, three becomes ten, and suddenly the same logic is scattered across files, repeated with tiny differences, and hard to test. Object-oriented programming gives you a cleaner way to think: instead of asking, “What steps do I need to run?” you begin asking, “What things exist in this system, and what can each thing do?” That shift sounds small, but it changes the shape of your code in a very real way.
PHP supports object-oriented programming well, and modern PHP makes it even better. If you learn classes, objects, properties, methods, constructors, visibility, inheritance, interfaces, traits, and a few best practices, you can build code that feels much more natural to work with. You do not need to become a computer science theorist to use it. You just need to understand the ideas clearly and practice them in code. That is what this guide is for.
What Are Objects and Classes in PHP?
Let’s start with the simplest idea.
A class is a blueprint. An object is a real thing made from that blueprint.
Think of a class like the design for a house. The blueprint says where the rooms go, how many windows exist, and what shape the building has. The object is the actual house built from that plan. You can build many houses from the same blueprint, and each house is its own separate thing. In PHP, a class defines the structure and behavior, while an object is an instance of that class.
Here is a very small example:
<?php
class Car
{
public $brand;
public $color;
public function drive()
{
return "The car is driving.";
}
}
$car = new Car();
$car->brand = "Toyota";
$car->color = "Red";
echo $car->drive();
In this example, Car is the class. $car is an object created from that class. The class defines two properties, brand and color, and one method, drive(). This is the heart of object-oriented programming in PHP: bundle data and behavior into one unit.
That idea may seem simple at first, but it is powerful. Instead of keeping a car’s data in one place and its actions in another, you keep them together. That makes your code easier to read, easier to expand, and easier to reason about.
Why Use Classes Instead of Only Functions?
A lot of beginners ask this question, and it is a fair one. If functions already exist, why not just use functions everywhere?
The short answer is that functions are excellent for small, independent tasks. Classes become useful when your code starts representing things that have state, identity, and related actions. A blog post has a title, content, author, published date, and methods like publish(), archive(), or updateTitle(). A user has a name, email, password, and methods like changePassword() or hasPermission(). These are not just actions floating in space. They belong to a specific entity.
Imagine a system for books. A book has a title, author, ISBN, and price. It can be discounted, displayed, or checked for availability. If you use only functions, you might end up passing a long list of arguments to every function, like this:
calculateDiscountedPrice($title, $author, $isbn, $price, $discount);
That works, but it quickly becomes awkward. With a class, you can group those values together and operate on them naturally:
$book->applyDiscount(15);
That is easier to read and much easier to maintain.
Creating Your First Class
Let’s build a simple Book class.
<?php
class Book
{
public $title;
public $author;
public $price;
}
$book1 = new Book();
$book1->title = "Clean Code";
$book1->author = "Robert C. Martin";
$book1->price = 25.99;
echo $book1->title;
This is the most basic class structure. The class defines properties, and the object stores actual values.
Notice the -> symbol. In PHP, that operator is used to access object properties and methods. If you are coming from procedural PHP, this is one of the first syntax changes that feels a little strange, but it becomes natural quickly.
You can also create multiple objects from the same class:
<?php
class Book
{
public $title;
public $author;
public $price;
}
$book1 = new Book();
$book1->title = "Clean Code";
$book1->author = "Robert C. Martin";
$book1->price = 25.99;
$book2 = new Book();
$book2->title = "The Pragmatic Programmer";
$book2->author = "Andrew Hunt";
$book2->price = 30.50;
echo $book1->title . PHP_EOL;
echo $book2->title . PHP_EOL;
Both objects share the same class, but each stores its own data. That is one of the most important ideas to understand early.
Properties: The Data Inside a Class
Properties are variables that belong to a class. They describe the state of an object.
In older PHP examples, you will often see properties declared as public, which means they can be accessed from anywhere. That is easy to understand, but in real projects, it is not always the best choice. We will talk about visibility soon. For now, focus on the idea that properties hold object data.
Example:
<?php
class User
{
public $name;
public $email;
public $age;
}
$user = new User();
$user->name = "Hassan";
$user->email = "hassan@example.com";
$user->age = 28;
echo $user->name;
A property can hold a string, integer, float, array, another object, or even null. PHP is flexible, but that flexibility should still be used carefully. In modern PHP, you can also define property types, which makes code safer and clearer.
<?php
class User
{
public string $name;
public string $email;
public int $age;
}
Typed properties help prevent bugs. If age should always be an integer, PHP can enforce that.
Methods: The Behavior Inside a Class
Methods are functions that belong to a class. They describe what an object can do.
Let’s extend the Book class:
<?php
class Book
{
public $title;
public $author;
public $price;
public function describe()
{
return $this->title . " by " . $this->author;
}
}
$book = new Book();
$book->title = "Clean Code";
$book->author = "Robert C. Martin";
$book->price = 25.99;
echo $book->describe();
The method describe() uses $this. That keyword refers to the current object. In other words, inside the class, $this->title means the title of this specific book object.
This is a critical concept. A class is a template, but $this connects the method to the particular object being used.
Let’s make the class a little more useful:
<?php
class Book
{
public string $title;
public string $author;
public float $price;
public function describe(): string
{
return $this->title . " by " . $this->author;
}
public function discountedPrice(float $discount): float
{
return $this->price - ($this->price * $discount / 100);
}
}
$book = new Book();
$book->title = "Clean Code";
$book->author = "Robert C. Martin";
$book->price = 25.99;
echo $book->describe() . PHP_EOL;
echo $book->discountedPrice(10);
Now the class not only stores data but also performs useful actions related to that data.
The Constructor: Setting Up Objects Properly
One of the first improvements most developers make is using a constructor. The constructor is a special method that runs automatically when an object is created. It is perfect for initializing values.
Without a constructor, you often create an object and then assign properties one by one. That works, but it can be clumsy and easy to forget something.
Here is a constructor example:
<?php
class Book
{
public string $title;
public string $author;
public float $price;
public function __construct(string $title, string $author, float $price)
{
$this->title = $title;
$this->author = $author;
$this->price = $price;
}
public function describe(): string
{
return $this->title . " by " . $this->author;
}
}
$book = new Book("Clean Code", "Robert C. Martin", 25.99);
echo $book->describe();
This is cleaner because the object is born in a valid state. That is one of the best habits you can develop in object-oriented programming: make invalid object states hard or impossible.
In modern PHP, many developers use constructor property promotion, which makes the code even shorter:
<?php
class Book
{
public function __construct(
public string $title,
public string $author,
public float $price
) {
}
public function describe(): string
{
return $this->title . " by " . $this->author;
}
}
$book = new Book("Clean Code", "Robert C. Martin", 25.99);
echo $book->describe();
This version does the same thing but with less boilerplate.
Visibility: Public, Private, and Protected
Visibility controls how properties and methods can be accessed. This is one of the most important parts of OOP because it helps protect your data and reduce accidental misuse.
There are three main visibility levels in PHP:
public means accessible from anywhere.
private means accessible only inside the same class.
protected means accessible inside the class and its child classes.
Let’s look at a real example:
<?php
class BankAccount
{
public string $accountNumber;
private float $balance = 0;
public function __construct(string $accountNumber, float $initialBalance)
{
$this->accountNumber = $accountNumber;
$this->balance = $initialBalance;
}
public function deposit(float $amount): void
{
if ($amount > 0) {
$this->balance += $amount;
}
}
public function withdraw(float $amount): bool
{
if ($amount > 0 && $amount <= $this->balance) {
$this->balance -= $amount;
return true;
}
return false;
}
public function getBalance(): float
{
return $this->balance;
}
}
$account = new BankAccount("AC-1001", 500);
$account->deposit(200);
$account->withdraw(150);
echo $account->getBalance();
Notice that balance is private. That means nobody can directly modify it from outside the class. This is a good thing. If someone could do $account->balance = -1000, your application would quickly become unreliable. By keeping the balance private and exposing safe methods like deposit() and withdraw(), you protect the rules of the system.
That is the real value of encapsulation. It is not about hiding things for no reason. It is about protecting the integrity of your data.
Encapsulation: Keeping Data and Rules Together
Encapsulation means wrapping data and behavior together and controlling access to the internal details.
A beginner-friendly way to think about it is this: an object should handle its own rules. A bank account should know whether withdrawals are allowed. A product should know how to calculate its discounted price. A user should know how to update its profile safely.
Here is a better Product example:
<?php
class Product
{
private string $name;
private float $price;
public function __construct(string $name, float $price)
{
$this->name = $name;
$this->price = $price;
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): float
{
return $this->price;
}
public function applyDiscount(float $percent): void
{
if ($percent < 0 || $percent > 100) {
throw new InvalidArgumentException("Discount must be between 0 and 100.");
}
$this->price -= ($this->price * $percent / 100);
}
}
This class keeps its values private and only exposes controlled behavior. That means the object remains in charge of its own data. That is a much healthier design than letting every part of your code modify everything directly.
Getters and Setters
You will often hear about getters and setters. These are methods used to read and update private properties.
A getter returns a value:
public function getName(): string
{
return $this->name;
}
A setter changes a value:
public function setName(string $name): void
{
$this->name = $name;
}
At first, it may seem strange to write methods just to access properties. Why not make them public and read them directly? The answer is control.
Using getters and setters lets you validate data, transform it, or prevent invalid changes.
Example:
<?php
class User
{
private string $email;
public function __construct(string $email)
{
$this->setEmail($email);
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email address.");
}
$this->email = $email;
}
}
$user = new User("test@example.com");
echo $user->getEmail();
This way, the class itself guarantees that the email is valid.
The $this Keyword
$this is one of those keywords that looks small but does a lot of work.
Inside a class method, $this refers to the current object. It lets you access its properties and methods.
Example:
<?php
class Person
{
public string $name;
public function sayHello(): string
{
return "Hello, my name is " . $this->name;
}
}
$person = new Person();
$person->name = "Hassan";
echo $person->sayHello();
Without $this, the method would not know which object’s name to use. That becomes especially important when you have many objects created from the same class.
Static Properties and Static Methods
Not everything belongs to a single object. Sometimes a value or behavior belongs to the class itself.
That is where static comes in.
A static property or method belongs to the class, not an instance of the class. You access it without creating an object.
Example:
<?php
class MathHelper
{
public static function square(int $number): int
{
return $number * $number;
}
}
echo MathHelper::square(5);
Here, square() does not need object state. It simply performs a calculation. So making it static makes sense.
You can also use static properties:
<?php
class Counter
{
public static int $count = 0;
public function __construct()
{
self::$count++;
}
}
$a = new Counter();
$b = new Counter();
$c = new Counter();
echo Counter::$count;
Static members are useful, but do not overuse them. A lot of beginners reach for static too early because it feels convenient. In real applications, too much static code can make testing and maintenance harder. Use it when the behavior truly belongs to the class itself.
The self Keyword
Inside a class, self:: is used to refer to static properties or methods of the same class.
Example:
<?php
class Example
{
public static string $message = "Hello";
public static function showMessage(): string
{
return self::$message;
}
}
echo Example::showMessage();
This is different from $this, which points to the current object instance. self points to the class definition.
Inheritance: Reusing and Extending Code
Inheritance allows one class to extend another class. The child class inherits properties and methods from the parent class.
This is useful when you have a general concept and a more specific version of it.
For example, Vehicle can be a parent class, and Car and Motorcycle can be child classes.
<?php
class Vehicle
{
protected string $brand;
public function __construct(string $brand)
{
$this->brand = $brand;
}
public function getBrand(): string
{
return $this->brand;
}
}
class Car extends Vehicle
{
public function drive(): string
{
return $this->brand . " car is driving.";
}
}
$car = new Car("Toyota");
echo $car->getBrand();
echo $car->drive();
The Car class inherits brand from Vehicle. Because brand is protected, the child class can access it.
Inheritance is helpful, but it should be used carefully. Not every shared idea needs inheritance. Sometimes composition is a better choice. We will talk about that later.
Method Overriding
A child class can replace a parent method with its own version. This is called overriding.
Example:
<?php
class Animal
{
public function sound(): string
{
return "Some generic animal sound";
}
}
class Dog extends Animal
{
public function sound(): string
{
return "Woof!";
}
}
$dog = new Dog();
echo $dog->sound();
The child class provides its own behavior. This is useful when a general rule exists, but a specific type needs a more precise implementation.
The parent Keyword
Sometimes a child class wants to use the parent’s constructor or method while adding its own logic.
<?php
class Person
{
protected string $name;
public function __construct(string $name)
{
$this->name = $name;
}
}
class Employee extends Person
{
private string $position;
public function __construct(string $name, string $position)
{
parent::__construct($name);
$this->position = $position;
}
public function getDetails(): string
{
return $this->name . " works as " . $this->position;
}
}
$employee = new Employee("Amina", "Developer");
echo $employee->getDetails();
The child constructor calls parent::__construct($name) so the name initialization is handled by the parent class. This keeps repeated logic in one place.
Abstract Classes
An abstract class is a class you cannot instantiate directly. It is meant to be extended. It can contain abstract methods, which child classes must implement.
This is useful when you want to define a common structure, but leave specific behavior to child classes.
Example:
<?php
abstract class Shape
{
abstract public function area(): float;
}
class Rectangle extends Shape
{
public function __construct(
private float $width,
private float $height
) {
}
public function area(): float
{
return $this->width * $this->height;
}
}
$rectangle = new Rectangle(10, 5);
echo $rectangle->area();
The Shape class defines the contract: every shape must have an area() method. The child class decides the actual implementation.
Abstract classes are useful when classes share some common code and also require a consistent structure.
Interfaces
An interface defines what methods a class must have, without saying how they should work.
If abstract classes are partly defined blueprints, interfaces are strict contracts.
Example:
<?php
interface PaymentGateway
{
public function pay(float $amount): string;
}
class StripeGateway implements PaymentGateway
{
public function pay(float $amount): string
{
return "Paid $" . number_format($amount, 2) . " using Stripe.";
}
}
class PayPalGateway implements PaymentGateway
{
public function pay(float $amount): string
{
return "Paid $" . number_format($amount, 2) . " using PayPal.";
}
}
Now any class that implements PaymentGateway must define pay().
Interfaces are extremely useful in larger applications because they let you depend on behavior rather than specific classes. That makes your code flexible and easier to swap later.
Example of using the interface:
<?php
function processPayment(PaymentGateway $gateway, float $amount): void
{
echo $gateway->pay($amount);
}
processPayment(new StripeGateway(), 49.99);
processPayment(new PayPalGateway(), 49.99);
This is elegant because the function does not care which gateway is used, as long as it follows the interface.
Traits
Sometimes PHP classes need to share behavior without using inheritance. That is where traits help.
A trait is a reusable chunk of code that can be inserted into multiple classes.
Example:
<?php
trait Logger
{
public function log(string $message): void
{
echo "[" . date('Y-m-d H:i:s') . "] " . $message . PHP_EOL;
}
}
class Order
{
use Logger;
public function place(): void
{
$this->log("Order placed.");
}
}
class User
{
use Logger;
public function register(): void
{
$this->log("User registered.");
}
}
$order = new Order();
$order->place();
$user = new User();
$user->register();
Traits are handy when multiple classes need the same helper behavior, and inheritance would not fit well.
That said, traits should not be used carelessly. If you overuse them, code can become hard to trace. Use them for shared behavior that truly belongs in more than one place.
Composition: Often Better Than Inheritance
A lot of developers start with inheritance because it seems natural. But in many real projects, composition is a better choice.
Composition means building objects out of other objects. Instead of saying “A is a kind of B,” you say “A has a B.”
For example, a Car has an Engine. A User has an Address. An Order has OrderItem objects.
Here is a simple example:
<?php
class Engine
{
public function start(): string
{
return "Engine started.";
}
}
class Car
{
private Engine $engine;
public function __construct()
{
$this->engine = new Engine();
}
public function drive(): string
{
return $this->engine->start() . " Car is moving.";
}
}
$car = new Car();
echo $car->drive();
This design is flexible because Car is not forced to be part of an inheritance tree. It simply uses an Engine.
In many cases, composition leads to more maintainable code than inheritance. Inheritance can be useful, but it is easy to create rigid designs if you use it everywhere.
Object Relationships in Real Life
The real power of objects and classes becomes clearer when you model real systems.
A blog system might have:
A User class for authors and readers.
A Post class for blog posts.
A Comment class for comments.
A Category class for grouping posts.
A Tag class for labels.
These classes can relate to each other through composition and references. That is how object-oriented thinking starts to feel practical instead of theoretical.
Here is a simplified example:
<?php
class User
{
public function __construct(
public string $name,
public string $email
) {
}
}
class Post
{
public function __construct(
public string $title,
public string $content,
public User $author
) {
}
public function summary(int $length = 100): string
{
return substr($this->content, 0, $length);
}
}
$author = new User("Hassan", "hassan@example.com");
$post = new Post("Learn PHP Objects", "This is a long blog post about PHP objects and classes...", $author);
echo $post->title;
echo $post->author->name;
This looks and feels close to the real world. That is one reason OOP is so widely used.
Type Declarations and Return Types
Modern PHP strongly encourages type declarations. This is a very good habit.
Type declarations tell PHP what kind of value a function expects and what it returns.
Example:
<?php
class Calculator
{
public function add(int $a, int $b): int
{
return $a + $b;
}
}
$calc = new Calculator();
echo $calc->add(5, 7);
This makes your code clearer and reduces bugs.
You can also use nullable types:
public function findUser(int $id): ?User
{
// returns User or null
}
The ?User means the method can return a User or null.
Types also work with properties:
public string $name;
public ?string $nickname = null;
The more your code documents itself through types, the easier it becomes to understand.
Constants in Classes
Classes can also define constants, which are values that do not change.
Example:
<?php
class OrderStatus
{
public const PENDING = 'pending';
public const PAID = 'paid';
public const SHIPPED = 'shipped';
}
echo OrderStatus::PAID;
Constants are useful for values that should stay fixed and reused across your application. They make your code more readable than scattering raw strings everywhere.
Magic Methods
PHP has special methods that start with __. These are called magic methods. Some of them are very useful, but they should be used carefully.
A few common ones are:
__construct() runs when an object is created.
__destruct() runs when an object is destroyed.
__toString() lets an object be treated as a string.
__get() and __set() intercept property access.
Example with __toString():
<?php
class Product
{
public function __construct(
private string $name,
private float $price
) {
}
public function __toString(): string
{
return $this->name . " costs $" . number_format($this->price, 2);
}
}
$product = new Product("Laptop", 899.99);
echo $product;
This is handy when you want a clean string representation of an object.
A more advanced magic method example is __get():
<?php
class Profile
{
private array $data = [
'name' => 'Hassan',
'country' => 'Morocco'
];
public function __get(string $key)
{
return $this->data[$key] ?? null;
}
}
$profile = new Profile();
echo $profile->name;
Magic methods can be powerful, but they can also hide behavior in ways that make code harder to understand. Use them only when they genuinely improve design.
Namespaces and Classes
As your project grows, you will have more classes. Namespaces help organize them and avoid naming collisions.
For example, two different parts of a project might both have a User class. Namespaces solve that problem.
<?php
namespace App\Models;
class User
{
public function __construct(public string $name)
{
}
}
Then elsewhere:
<?php
use App\Models\User;
$user = new User("Hassan");
Namespaces are essential in modern PHP applications, especially when you use autoloading and frameworks like Laravel or Symfony.
Autoloading Classes
One of the best parts of modern PHP is autoloading. Instead of manually including every class file with require, you can let Composer handle it.
That keeps your code cleaner and your project structure more organized.
A typical class file might look like this:
<?php
namespace App\Services;
class MailService
{
public function send(string $to, string $message): void
{
echo "Sending mail to {$to}: {$message}";
}
}
With autoloading, PHP can load the class automatically when it is used. This is a huge improvement over old-style manual includes.
If you are building a real application, learning Composer and PSR-4 autoloading goes hand in hand with learning classes and objects.
Building a Small OOP Example: A Library System
Let’s put a few ideas together in a simple library system.
We will create a Book class, a Library class, and some methods for adding and listing books.
<?php
class Book
{
public function __construct(
private string $title,
private string $author
) {
}
public function getTitle(): string
{
return $this->title;
}
public function getAuthor(): string
{
return $this->author;
}
public function describe(): string
{
return $this->title . " by " . $this->author;
}
}
class Library
{
private array $books = [];
public function addBook(Book $book): void
{
$this->books[] = $book;
}
public function listBooks(): void
{
foreach ($this->books as $book) {
echo $book->describe() . PHP_EOL;
}
}
public function countBooks(): int
{
return count($this->books);
}
}
$library = new Library();
$library->addBook(new Book("Clean Code", "Robert C. Martin"));
$library->addBook(new Book("The Pragmatic Programmer", "Andrew Hunt"));
$library->addBook(new Book("Refactoring", "Martin Fowler"));
$library->listBooks();
echo "Total books: " . $library->countBooks();
This example shows a few important patterns. The Book object represents a single book. The Library object manages a collection of books. The classes are separate because they do different jobs. That separation makes the code easier to understand.
Validating Data Inside Classes
One common mistake is allowing objects to be created with invalid data. A good class should protect itself as much as possible.
Here is a safer User example:
<?php
class User
{
private string $email;
public function __construct(string $email)
{
$this->setEmail($email);
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): void
{
$email = trim($email);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email address.");
}
$this->email = strtolower($email);
}
}
This class does two useful things. First, it trims whitespace. Second, it validates the email before saving it. That means the rest of the application can trust the object more easily.
This is one of the biggest practical benefits of object-oriented design: you can push rules into the object itself rather than repeating them all over the place.
Immutability: Objects That Do Not Change
Sometimes it is a good idea to make objects immutable. That means once they are created, they do not change.
Immutability reduces side effects and can make code safer and easier to reason about.
Example:
<?php
class Money
{
public function __construct(
private float $amount,
private string $currency
) {
}
public function amount(): float
{
return $this->amount;
}
public function currency(): string
{
return $this->currency;
}
public function add(Money $other): Money
{
if ($this->currency !== $other->currency()) {
throw new InvalidArgumentException("Currency mismatch.");
}
return new Money($this->amount + $other->amount(), $this->currency);
}
}
Instead of changing the current object, the add() method returns a new Money object. This style is especially useful in finance, date handling, and other areas where accidental mutation can cause bugs.
Common Mistakes Beginners Make
When learning objects and classes, a few mistakes show up again and again.
One common mistake is making everything public. That might feel easy at first, but it removes control and makes it harder to protect the object’s state.
Another mistake is putting too much logic into one class. A class should have a clear purpose. If your class becomes a giant box of unrelated code, it is time to split it up.
Another common issue is confusing inheritance with reuse. Just because two classes share some behavior does not mean one should extend the other. Sometimes composition is the cleaner option.
A fourth mistake is not using constructors well. If an object needs essential data to work, require it in the constructor instead of letting the object float around half-initialized.
A fifth mistake is ignoring types. Type hints are not there to make your code look modern. They help prevent errors and make intent clearer.
The Single Responsibility Principle
One design principle that fits very naturally with classes is the Single Responsibility Principle, often shortened to SRP.
It says that a class should have one main reason to change.
That means a class should not do too many unrelated things.
For example, a User class should not also send emails, manage database queries, generate PDFs, and calculate taxes all at once. That would become difficult to maintain. Instead, the User class should represent user-related data and behavior, while separate services handle email, persistence, and other responsibilities.
Here is the idea in simple terms: one class, one job.
This principle helps keep your code organized and reduces the feeling that everything is tangled together.
Example of a Better Design
Instead of this:
<?php
class Order
{
public function saveToDatabase()
{
}
public function sendEmailConfirmation()
{
}
public function generateInvoice()
{
}
}
A better structure might be:
<?php
class Order
{
public function __construct(
private int $id,
private float $total
) {
}
public function getId(): int
{
return $this->id;
}
public function getTotal(): float
{
return $this->total;
}
}
class OrderRepository
{
public function save(Order $order): void
{
// save order
}
}
class Mailer
{
public function sendOrderConfirmation(Order $order): void
{
// send email
}
}
class InvoiceGenerator
{
public function generate(Order $order): string
{
return "Invoice for order #" . $order->getId();
}
}
This version is much easier to work with because each class has a narrower, clearer responsibility.
A Practical Mini Project: Shopping Cart
Let’s build a slightly more realistic example using classes.
<?php
class CartItem
{
public function __construct(
private string $name,
private float $price,
private int $quantity = 1
) {
}
public function getName(): string
{
return $this->name;
}
public function getPrice(): float
{
return $this->price;
}
public function getQuantity(): int
{
return $this->quantity;
}
public function subtotal(): float
{
return $this->price * $this->quantity;
}
}
class Cart
{
private array $items = [];
public function addItem(CartItem $item): void
{
$this->items[] = $item;
}
public function getItems(): array
{
return $this->items;
}
public function total(): float
{
$sum = 0;
foreach ($this->items as $item) {
$sum += $item->subtotal();
}
return $sum;
}
public function display(): void
{
foreach ($this->items as $item) {
echo $item->getName() . " x " . $item->getQuantity() . " = $" . number_format($item->subtotal(), 2) . PHP_EOL;
}
echo "Total: $" . number_format($this->total(), 2) . PHP_EOL;
}
}
$cart = new Cart();
$cart->addItem(new CartItem("Laptop", 900, 1));
$cart->addItem(new CartItem("Mouse", 25, 2));
$cart->addItem(new CartItem("Keyboard", 70, 1));
$cart->display();
This example demonstrates how objects can work together. A Cart contains multiple CartItem objects. Each item knows how to calculate its own subtotal, while the cart knows how to total everything.
That division of responsibility is exactly the kind of thing that makes OOP useful in real software.
Debugging and Reading Object Data
When you are learning classes, you will often want to inspect objects. PHP gives you a few ways to do that.
You can use var_dump():
var_dump($book);
You can also use print_r():
print_r($book);
These functions are useful during development, especially when you want to see the structure of an object quickly. In bigger projects, you may use logging or debugging tools for better control.
Objects as Values and Objects as Entities
A useful way to think about objects is to separate them into two broad ideas.
Some objects are values. They are mostly defined by their data, like Money, DateRange, or Color. Two objects with the same value may be treated as equal.
Other objects are entities. They have identity and can change over time, like User, Order, or Post. Even if some fields are similar, the object itself represents a specific thing.
This distinction helps you design classes more naturally. Not every object should behave the same way.
How to Think Like an OOP Developer
When you build with objects and classes, the biggest shift is not syntax. It is thinking.
You start asking:
What things exist in this system?
What data belongs to each thing?
What actions belong to each thing?
Which class should own this rule?
Which behavior is shared, and which is specific?
That kind of thinking leads to cleaner code. It also helps you avoid the trap of making one giant file full of functions that gradually becomes hard to read.
A good class often feels like a small, self-contained part of a system. It knows enough to do its job, but not so much that it tries to run the whole application.
Best Practices for PHP Objects and Classes
A few habits go a long way.
Keep properties private unless there is a good reason not to. This protects your data.
Use constructors to create valid objects.
Give classes clear names that describe what they represent.
Keep methods focused on one task.
Prefer composition when it fits better than inheritance.
Use interfaces when you want flexibility between different implementations.
Validate input as early as possible.
Use type declarations for clarity and safety.
Avoid huge classes that do too many different things.
These are not just style preferences. They make your code easier to understand, test, and maintain.
A Final Example: User Profile Class
Let’s finish with a polished example that combines several ideas.
<?php
class UserProfile
{
private string $name;
private string $email;
private ?string $bio;
public function __construct(string $name, string $email, ?string $bio = null)
{
$this->setName($name);
$this->setEmail($email);
$this->bio = $bio;
}
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getBio(): ?string
{
return $this->bio;
}
public function setName(string $name): void
{
$name = trim($name);
if ($name === '') {
throw new InvalidArgumentException("Name cannot be empty.");
}
$this->name = $name;
}
public function setEmail(string $email): void
{
$email = trim($email);
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
throw new InvalidArgumentException("Invalid email address.");
}
$this->email = strtolower($email);
}
public function setBio(?string $bio): void
{
$this->bio = $bio !== null ? trim($bio) : null;
}
public function summary(): string
{
$bio = $this->bio ?? "No bio available.";
return $this->name . " (" . $this->email . "): " . $bio;
}
}
$user = new UserProfile("Hassan Agmir", "hassan@example.com", "Full Stack Developer from Morocco.");
echo $user->summary();
This class does a lot right. It validates data. It stores state privately. It offers methods for controlled changes. It also includes a clean summary method that makes the object easy to use.
That is the kind of design you should aim for: simple, readable, and protected from bad data.
Conclusion
Learning PHP objects and classes is one of the biggest steps you can take from beginner-friendly coding toward real application development. At first, it may feel like extra work compared to simple functions and arrays, especially if you are used to procedural PHP. But once the codebase grows, the benefits become hard to ignore. Classes help you organize logic, objects help you model real things, and methods let each part of the system handle its own behavior. That is how software becomes easier to maintain instead of harder.
The best way to get comfortable with OOP is to use it often. Start by turning a small procedural idea into a class. Build a Book, a User, a Cart, or an Invoice. Add constructors, methods, private properties, and validation. Then refactor small pieces until the code starts feeling more natural. You do not need to understand every advanced pattern right away. The important thing is to build the habit of thinking in objects and letting each class do one clear job.