Skip to content

08: Introduction to Object-Oriented Programming

Introduction to Object-Oriented Programming

Chapter 08: Introduction to Object-Oriented Programming

Section titled “Chapter 08: Introduction to Object-Oriented Programming”

Until now, we’ve been writing code in a procedural way: a series of steps and functions that operate on data. This is great for simple scripts, but as applications grow, it can become disorganized. Data and the functions that operate on that data are separate, and it can be hard to manage the relationships between them.

Object-Oriented Programming (OOP) is a powerful paradigm that solves this problem. It allows us to bundle data and the functions that work on that data together into a single unit called an object. This helps us model real-world things (like a User, a Product, or a BlogPost) in a clean, reusable, and intuitive way.

This chapter is your first step into this new world. You’ll build working examples and see the immediate benefits of OOP. By the end, you’ll have created multiple classes with proper encapsulation, and you’ll understand why virtually all modern PHP frameworks are built around these principles.

  • PHP 8.4 installed and accessible from your command line
  • Completion of Chapter 07 (String Manipulation) or equivalent understanding of functions
  • A text editor
  • Familiarity with arrays and functions
  • Estimated time: 25-30 minutes

By the end of this chapter, you will have created:

  • A User class that models user data and behavior
  • Multiple user objects with independent state
  • A constructor method for clean initialization
  • Private properties with getter/setter methods (encapsulation)
  • Modern PHP 8.4 code using constructor property promotion and type declarations
  • A Database class demonstrating static properties, methods, and constants
  • Practical examples of factory methods and configuration classes
  • Working exercises: Car, BankAccount, Product, and Logger classes

All examples will be working PHP scripts you can run immediately.

Want to see OOP in action right away? Create this file and run it:

quick-user.php
<?php
class User
{
public function __construct(
private string $name,
private string $email
) {}
public function greet(): string
{
return "Hello, " . $this->name;
}
public function getName(): string
{
return $this->name;
}
}
$user = new User('Dale', 'dale@example.com');
echo $user->greet() . PHP_EOL;
echo "Name: " . $user->getName() . PHP_EOL;
Terminal window
# Run it
php quick-user.php

Expected output:

Hello, Dale
Name: Dale

This compact example uses PHP 8.4’s constructor property promotion, which we’ll explain below. Now let’s build this understanding step by step.

  • Understand the core concept of OOP: bundling data and behavior together.
  • Know the difference between a class (a blueprint) and an object (an instance).
  • Define a class with properties (data) and methods (behavior).
  • Instantiate objects from a class using the new keyword.
  • Use the special __construct method to initialize an object’s state.
  • Control access to properties and methods with visibility keywords (public, private).
  • Use modern PHP 8.4 features like constructor property promotion and type declarations.
  • Understand static properties, methods, and class constants.

Understanding how classes, objects, and visibility work together is essential to mastering OOP. Here’s how these concepts relate:

Class as Blueprint: A class defines the structure (properties) and behavior (methods) that all objects of that type will have. Think of it like an architectural blueprint—it specifies what will be built, but isn’t the building itself.

Objects as Instances: When you create an object from a class using the new keyword, you’re instantiating that blueprint into an actual entity in memory with its own data. Each object maintains its own independent state.

Example: User Class and Instances

The User class blueprint defines:

  • Private properties: name (string), email (string), age (int)
  • Public methods: __construct(), getName(), getEmail(), greet(), isAdult()

From this single blueprint, you can create multiple independent objects:

  • User Instance 1: name = "Alice", email = "alice@example.com", age = 30
  • User Instance 2: name = "Bob", email = "bob@example.com", age = 25

Visibility Controls Access:

  • private properties are only accessible within the class itself—external code cannot read or modify them directly
  • public methods are accessible from anywhere—they provide the controlled interface to interact with the object
  • protected (covered in the next chapter) allows access from the class and its subclasses

This separation between the class (blueprint) and objects (instances) allows you to create as many independent objects as you need, each maintaining its own data while sharing the same behavior defined in the class.

Step 1: From Associative Array to Object (~5 min)

Section titled “Step 1: From Associative Array to Object (~5 min)”

Goal: Transform procedural array-based code into an object-oriented class structure.

In the past, we’ve used associative arrays to represent structured data, like a user:

old-way.php
<?php
// The old procedural approach
$user = [
'name' => 'Dale',
'email' => 'dale@example.com'
];
function greet_user($user) {
return "Hello, " . $user['name'];
}
echo greet_user($user);

This works, but the user’s data ($user array) and the behavior related to it (greet_user function) are separate. OOP brings them together.

  1. Create a new file called oop-step1.php.

  2. Define your first class using this structure:

oop-step1.php
<?php
class User
{
// Properties are the data associated with the class.
// They are like variables that belong to the class.
public $name;
public $email;
// Methods are the behavior.
// They are like functions that belong to the class.
public function greet()
{
// Inside a method, we use the special variable `$this`
// to refer to the current object.
return "Hello, " . $this->name;
}
}
// Create and use a user object
$user = new User();
$user->name = 'Dale';
$user->email = 'dale@example.com';
echo $user->greet() . PHP_EOL;
  1. Run the script:
Terminal window
php oop-step1.php
Hello, Dale

A class is a blueprint that defines structure and behavior. Class names use PascalCase by convention. The $this pseudo-variable always refers to the current object instance, allowing methods to access the object’s own properties using the -> operator.

Step 2: Creating Objects (Instantiation) (~3 min)

Section titled “Step 2: Creating Objects (Instantiation) (~3 min)”

Goal: Create multiple independent object instances from a single class blueprint.

Now that we have the User blueprint, we can create individual objects from it. This process is called instantiation. Each object is an independent instance of the class with its own set of property values.

  1. Create a new file called oop-step2.php.

  2. Create multiple objects from the same class:

oop-step2.php
<?php
class User
{
public $name;
public $email;
public function greet()
{
return "Hello, " . $this->name;
}
}
// Create a new object (an instance of the User class)
$user1 = new User();
$user1->name = 'Dale';
$user1->email = 'dale@example.com';
// Create a second, independent object
$user2 = new User();
$user2->name = 'Alice';
$user2->email = 'alice@example.com';
// Each object maintains its own state
echo $user1->greet() . PHP_EOL;
echo $user2->greet() . PHP_EOL;
// Prove they are independent
$user1->name = 'Dale Hurley';
echo $user1->greet() . PHP_EOL;
echo $user2->greet() . PHP_EOL; // Alice hasn't changed
  1. Run the script:
Terminal window
php oop-step2.php
Hello, Dale
Hello, Alice
Hello, Dale Hurley
Hello, Alice

The new keyword creates a brand-new instance of the class in memory. Each object has its own copy of the properties, so changing $user1->name doesn’t affect $user2->name. The object operator -> accesses properties and calls methods on a specific object instance.

Step 3: Initializing Objects with a Constructor (~4 min)

Section titled “Step 3: Initializing Objects with a Constructor (~4 min)”

Goal: Use a constructor to initialize object properties automatically upon instantiation.

Setting each property manually after creating an object is tedious. A constructor is a special method that is called automatically when you create a new object, allowing you to initialize its state right away.

In PHP, the constructor method is always named __construct.

  1. Create a new file called oop-step3.php.

  2. Add a constructor to your class:

oop-step3.php
<?php
class User
{
public $name;
public $email;
// This method is called automatically when we use `new User()`
public function __construct($name, $email)
{
$this->name = $name;
$this->email = $email;
echo "A new user named '$name' has been created." . PHP_EOL;
}
public function greet()
{
return "Hello, " . $this->name;
}
}
// Now we can pass the initial values directly when creating the object
$user1 = new User('Dale', 'dale@example.com');
echo $user1->greet() . PHP_EOL;
$user2 = new User('Alice', 'alice@example.com');
echo $user2->greet() . PHP_EOL;
  1. Run the script:
Terminal window
php oop-step3.php
A new user named 'Dale' has been created.
Hello, Dale
A new user named 'Alice' has been created.
Hello, Alice

The __construct method is a “magic method” (indicated by the double underscore prefix) that PHP calls automatically during instantiation. When you write new User('Dale', 'dale@example.com'), PHP creates the object and immediately calls __construct with those arguments, eliminating manual property assignment.

Step 4: Controlling Access with Visibility (~5 min)

Section titled “Step 4: Controlling Access with Visibility (~5 min)”

Goal: Protect object properties using visibility keywords and encapsulation principles.

Right now, we can read and change the $name of a user from anywhere ($user1->name = 'Bob';). This isn’t always desirable. You might want to protect certain properties from being changed accidentally or enforce validation rules.

Visibility keywords control where properties and methods can be accessed from:

  • public: Can be accessed from anywhere (outside the class and inside it).
  • private: Can only be accessed from within the class itself.
  • protected: Can be accessed from within the class and its subclasses (we’ll cover this in the next chapter on inheritance).

Let’s protect our properties by making them private and provide public methods to access them. This is a core OOP concept called encapsulation.

  1. Create a new file called oop-step4.php.

  2. Add private properties and getter/setter methods:

oop-step4.php
<?php
class User
{
private $name;
private $email;
public function __construct($name, $email)
{
// We can still access private properties from within the class
$this->name = $name;
$this->email = $email;
}
// A public "getter" method to allow controlled read-only access
public function getName()
{
return $this->name;
}
public function getEmail()
{
return $this->email;
}
// A public "setter" that allows changes with validation
public function setName($name)
{
// Add validation logic before setting
if (empty(trim($name))) {
echo "Error: Name cannot be empty." . PHP_EOL;
return;
}
$this->name = $name;
echo "Name updated successfully." . PHP_EOL;
}
}
$user1 = new User('Dale', 'dale@example.com');
// This is the correct way to access private properties
echo $user1->getName() . PHP_EOL;
echo $user1->getEmail() . PHP_EOL;
// Update the name through the setter
$user1->setName('Dale Hurley');
echo $user1->getName() . PHP_EOL;
// Try to set an empty name (validation will prevent it)
$user1->setName(' ');
echo $user1->getName() . PHP_EOL; // Still "Dale Hurley"
  1. Run the script:
Terminal window
php oop-step4.php
Dale
dale@example.com
Name updated successfully.
Dale Hurley
Error: Name cannot be empty.
Dale Hurley

The private keyword hides properties from external access, forcing interactions through public methods. This lets you add validation, logging, or other business logic whenever a property is read or written. If you tried to access $user1->name directly, PHP would throw a fatal error.

Try adding this line to your script to see the error:

<?php
// Add this to the bottom of oop-step4.php
echo $user1->name; // Fatal error: Cannot access private property

You’ll see:

Fatal error: Uncaught Error: Cannot access private property User::$name

Goal: Learn PHP 8.4’s constructor property promotion and typed properties for cleaner, more robust code.

PHP 8.4 offers powerful features that make OOP code more concise and type-safe. Let’s modernize our User class.

Instead of declaring properties separately and then assigning them in the constructor, you can do both at once:

  1. Create a new file called oop-modern.php.

  2. Use constructor property promotion and type declarations:

oop-modern.php
<?php
class User
{
// Constructor property promotion: declare and initialize in one step
// Add type declarations for better safety
public function __construct(
private string $name,
private string $email,
private int $age = 18 // Default value
) {
// The properties are automatically created and assigned!
// We can still add validation or other logic here if needed
if ($age < 0 || $age > 150) {
throw new InvalidArgumentException("Age must be between 0 and 150.");
}
}
public function getName(): string
{
return $this->name;
}
public function getEmail(): string
{
return $this->email;
}
public function getAge(): int
{
return $this->age;
}
public function greet(): string
{
return "Hello, I'm {$this->name}, {$this->age} years old.";
}
// Method to demonstrate business logic
public function isAdult(): bool
{
return $this->age >= 18;
}
}
// Create users with the modern syntax
$user1 = new User('Dale', 'dale@example.com', 35);
echo $user1->greet() . PHP_EOL;
echo "Adult? " . ($user1->isAdult() ? 'Yes' : 'No') . PHP_EOL;
// Using default age
$user2 = new User('Alice', 'alice@example.com');
echo $user2->greet() . PHP_EOL;
echo "Adult? " . ($user2->isAdult() ? 'Yes' : 'No') . PHP_EOL;
// Try to create an invalid user (uncomment to see the error)
// $user3 = new User('Bob', 'bob@example.com', 200);
  1. Run the script:
Terminal window
php oop-modern.php
Hello, I'm Dale, 35 years old.
Adult? Yes
Hello, I'm Alice, 18 years old.
Adult? Yes

Constructor property promotion (PHP 8.0+) eliminates boilerplate by combining property declaration, visibility, and assignment in the constructor signature. Type declarations ensure that properties can only hold values of the specified type, catching bugs early. PHP 8.4 fully supports these features with additional refinements.

  • Less code: No separate property declarations needed
  • Type safety: PHP enforces types at runtime
  • Clearer intent: The constructor signature shows exactly what data the object needs
  • Better IDE support: Editors can provide better autocomplete and error detection

::: tip Modern PHP frameworks like Laravel and Symfony use this pattern extensively. Learning it now will make framework code much easier to read. :::

Step 6: Static Properties, Methods, and Constants (~5 min)

Section titled “Step 6: Static Properties, Methods, and Constants (~5 min)”

Goal: Understand class-level members that don’t require object instantiation.

So far, every property and method we’ve worked with belongs to an object instance. But sometimes you want data or behavior that belongs to the class itself, not to any particular instance. This is where static members and class constants come in.

Constants are values that never change and belong to the class, not instances. They’re perfect for fixed configuration values or status codes.

Static members belong to the class itself. They’re shared across all instances and can be accessed without creating an object.

  1. Create a new file called oop-static.php.

  2. Use static members and constants:

oop-static.php
<?php
class Database
{
// Class constant - always accessible, never changes
public const HOST = 'localhost';
public const PORT = 3306;
// Static property - shared across all instances
private static int $connectionCount = 0;
// Instance property - unique per object
private string $name;
public function __construct(string $name)
{
$this->name = $name;
// Increment the shared counter
self::$connectionCount++;
}
// Static method - can be called without an object
public static function getConnectionCount(): int
{
return self::$connectionCount;
}
// Static method using constants
public static function getDefaultDSN(): string
{
// Use self:: to access class constants and static members
return self::HOST . ':' . self::PORT;
}
public function getName(): string
{
return $this->name;
}
}
// Access constants without creating an object
echo "Database host: " . Database::HOST . PHP_EOL;
echo "Default DSN: " . Database::getDefaultDSN() . PHP_EOL;
// Create instances - each increments the static counter
$db1 = new Database('users_db');
$db2 = new Database('products_db');
$db3 = new Database('orders_db');
// Static method shows the shared count
echo "Total connections: " . Database::getConnectionCount() . PHP_EOL;
// Each instance has its own name
echo "DB1 name: " . $db1->getName() . PHP_EOL;
echo "DB2 name: " . $db2->getName() . PHP_EOL;
  1. Run the script:
Terminal window
php oop-static.php
Database host: localhost
Default DSN: localhost:3306
Total connections: 3
DB1 name: users_db
DB2 name: products_db
  • Class constants (const) are accessed with ClassName::CONSTANT_NAME and never change
  • Static properties belong to the class, not instances—there’s only one $connectionCount shared by all Database objects
  • Static methods can be called without creating an object: Database::getConnectionCount()
  • Inside a class, use self:: to access static members and constants
  • Static members can’t access instance properties (like $this->name) because they don’t belong to any specific object

1. Configuration Values:

class Config
{
public const APP_NAME = 'My Application';
public const VERSION = '1.0.0';
public const DEBUG_MODE = true;
}
echo Config::APP_NAME; // No object needed

2. Factory Methods:

class User
{
public function __construct(
private string $name,
private string $email
) {}
// Static factory method
public static function createFromArray(array $data): self
{
return new self($data['name'], $data['email']);
}
}
$user = User::createFromArray(['name' => 'Dale', 'email' => 'dale@example.com']);

3. Counters and Shared State:

class RequestLogger
{
private static int $requestCount = 0;
public static function logRequest(): void
{
self::$requestCount++;
echo "Request #" . self::$requestCount . PHP_EOL;
}
}
RequestLogger::logRequest(); // Request #1
RequestLogger::logRequest(); // Request #2
RequestLogger::logRequest(); // Request #3

4. Status Enumerations:

class OrderStatus
{
public const PENDING = 'pending';
public const PROCESSING = 'processing';
public const SHIPPED = 'shipped';
public const DELIVERED = 'delivered';
public const CANCELLED = 'cancelled';
}
$order->setStatus(OrderStatus::SHIPPED); // Type-safe, autocomplete-friendly
  • $this->property - Access instance property (unique per object)
  • $this->method() - Call instance method (requires an object)
  • self::$staticProperty - Access static property (shared by all)
  • self::method() - Call static method (no object needed)
  • self::CONSTANT - Access class constant (never changes)

::: tip In modern PHP, you’ll often see static methods used as “named constructors” (factory methods) that provide convenient ways to create objects, like DateTime::createFromFormat() or Carbon::parse(). :::

::: warning Be careful with static properties! Because they’re shared across all instances, they can introduce unexpected behavior if you’re not mindful. Prefer instance properties unless you specifically need class-level state. :::

Goal: Learn cutting-edge PHP 8.4 features for cleaner, more maintainable code.

PHP 8.4 introduces powerful new features that make OOP code even more elegant. Let’s explore property hooks and asymmetric visibility—two features that eliminate boilerplate and make your intent crystal clear.

Property hooks provide a clean alternative to traditional getter and setter methods. Instead of writing separate getX() and setX() methods, you can add behavior directly to property access.

  1. Create a new file called oop-property-hooks.php.

  2. Basic Property Hooks Example:

<?php
declare(strict_types=1);
class User
{
// Set hook: automatically normalize email to lowercase
public string $email {
set => strtolower($value);
}
// Set hook: capitalize first name
public string $firstName {
set => ucfirst($value);
}
public function __construct(string $email, string $firstName)
{
$this->email = $email; // Triggers set hook
$this->firstName = $firstName; // Triggers set hook
}
}
$user = new User('JOHN.DOE@EXAMPLE.COM', 'john');
echo "Email: " . $user->email . PHP_EOL; // john.doe@example.com
echo "First Name: " . $user->firstName . PHP_EOL; // John
  1. Computed Properties with Get Hooks:
<?php
declare(strict_types=1);
class Product
{
public string $name;
public float $price;
public float $taxRate = 0.08;
// Computed property - calculated on each access
public float $priceWithTax {
get => $this->price * (1 + $this->taxRate);
}
public function __construct(string $name, float $price)
{
$this->name = $name;
$this->price = $price;
}
}
$product = new Product('Laptop', 999.99);
echo "Base Price: $" . $product->price . PHP_EOL;
echo "Price with Tax: $" . number_format($product->priceWithTax, 2) . PHP_EOL;
// No setter needed - priceWithTax is automatically calculated!
  1. Combined Get and Set Hooks:
<?php
declare(strict_types=1);
class Temperature
{
private float $celsius = 0;
// Expose as Fahrenheit, store as Celsius
public float $fahrenheit {
get => ($this->celsius * 9/5) + 32;
set => $this->celsius = ($value - 32) * 5/9;
}
public function getCelsius(): float
{
return $this->celsius;
}
}
$temp = new Temperature();
$temp->fahrenheit = 68; // Set in Fahrenheit
echo "Stored as: " . $temp->getCelsius() . "°C" . PHP_EOL; // 20°C
echo "Read as: " . $temp->fahrenheit . "°F" . PHP_EOL; // 68°F

Why it works: Property hooks intercept property access (get) and modification (set). The $value variable in a set hook contains the value being assigned. Get hooks compute values on-the-fly without needing separate storage.

Benefits:

  • Cleaner syntax than traditional getX() / setX() methods
  • Automatic validation and transformation
  • Computed properties without manual calculation methods
  • Reduces boilerplate code significantly

Asymmetric visibility allows you to specify different access levels for reading and writing a property. The most common pattern is public private(set), which means “anyone can read, only the class can write.”

  1. Create a new file called oop-asymmetric-visibility.php.

  2. Immutable Properties Example:

<?php
declare(strict_types=1);
class Order
{
// Public for reading, private for writing
public private(set) string $id;
public private(set) DateTime $createdAt;
public string $customerName; // Fully public
public float $total; // Fully public
public function __construct(string $customerName, float $total)
{
// Can set within the class
$this->id = uniqid('ORD_');
$this->createdAt = new DateTime();
$this->customerName = $customerName;
$this->total = $total;
}
}
$order = new Order('Alice Johnson', 99.99);
// ✓ Can read
echo "Order ID: " . $order->id . PHP_EOL;
echo "Created: " . $order->createdAt->format('Y-m-d H:i:s') . PHP_EOL;
// ✗ Cannot write from outside
// $order->id = 'ORD_123'; // Error!
// $order->createdAt = new DateTime(); // Error!
// ✓ Can still modify public properties
$order->customerName = 'Bob Smith';
  1. Controlled State Example:
<?php
declare(strict_types=1);
class ShoppingCart
{
private array $items = [];
// Total is publicly readable but only the class can update it
public private(set) float $total = 0.0;
public function addItem(string $name, float $price): void
{
$this->items[] = ['name' => $name, 'price' => $price];
$this->total += $price; // Only the class can modify total
}
public function removeLastItem(): void
{
if (!empty($this->items)) {
$item = array_pop($this->items);
$this->total -= $item['price'];
}
}
}
$cart = new ShoppingCart();
$cart->addItem('Book', 29.99);
$cart->addItem('Pen', 5.99);
echo "Total: $" . $cart->total . PHP_EOL; // ✓ Can read
// $cart->total = 1000000; // ✗ Error! Can't manipulate total directly

Why it works: The public private(set) syntax means:

  • Reading the property is public (accessible everywhere)
  • Writing the property is private (only accessible within the class)

This enforces immutability without requiring private properties plus public getter methods.

Common Use Cases:

  • IDs and unique identifiers: Set once in constructor, never changed
  • Timestamps: createdAt, updatedAt automatically managed by the class
  • Computed values: totals, counts that should only be updated through business logic
  • State flags: isPublished, isActive controlled by class methods
  • Version numbers: tracked internally, displayed publicly

Benefits:

  • Enforces immutability without getter boilerplate
  • Clear intent: “readable everywhere, writable only here”
  • Prevents accidental property modification
  • Reduces API surface area (fewer public methods)

Traditional Approach (Still Valid):

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
{
$this->email = strtolower($email);
}
}

Modern PHP 8.4 with Property Hooks:

class User
{
public string $email {
set => strtolower($value);
}
public function __construct(string $email)
{
$this->email = $email;
}
}

Traditional Immutable ID:

class Order
{
private string $id;
public function __construct()
{
$this->id = uniqid('ORD_');
}
public function getId(): string
{
return $this->id;
}
}

Modern with Asymmetric Visibility:

class Order
{
public private(set) string $id;
public function __construct()
{
$this->id = uniqid('ORD_');
}
// No getId() needed - access $order->id directly!
}

::: tip When to Use Each Feature Property Hooks: Use when you need to transform, validate, or compute values on read/write.

Asymmetric Visibility: Use when a value should be set internally but read publicly (IDs, timestamps, computed values).

Traditional Methods: Still useful for complex logic, multiple operations, or compatibility with older PHP versions. :::

::: info Code Examples Complete, runnable examples of these features are available in:

Error: “Cannot access private property”

Section titled “Error: “Cannot access private property””

Symptom: Fatal error: Uncaught Error: Cannot access private property User::$name

Cause: You tried to access a private property directly from outside the class.

Solution: Use public getter/setter methods instead:

// Wrong
echo $user->name;
// Correct
echo $user->getName();

Error: “Too few arguments to function __construct()”

Section titled “Error: “Too few arguments to function __construct()””

Symptom: ArgumentCountError: Too few arguments to function User::__construct()

Cause: You didn’t provide all required constructor parameters.

Solution: Check the constructor signature and provide all required arguments:

// Wrong
$user = new User('Dale'); // Missing email parameter
// Correct
$user = new User('Dale', 'dale@example.com');

Symptom: Fatal error: Uncaught Error: Call to undefined method User::greet()

Cause: You’re calling a method that doesn’t exist, or you have a typo.

Solution: Check your method name spelling and ensure it exists in the class:

// Wrong
$user->greet(); // If method is named "greeting()"
// Correct
$user->greeting(); // Match the actual method name

Symptom: TypeError: User::__construct(): Argument #3 ($age) must be of type int, string given

Cause: You passed the wrong type to a typed parameter.

Solution: Ensure you pass the correct type:

// Wrong
$user = new User('Dale', 'dale@example.com', '35'); // String instead of int
// Correct
$user = new User('Dale', 'dale@example.com', 35); // Integer

Symptom: Warning: Undefined property: User::$name

Cause: You’re trying to use a property that wasn’t initialized.

Solution: Always initialize all properties in the constructor:

public function __construct($name) {
$this->name = $name; // Don't forget this!
}

Error: “Using $this when not in object context”

Section titled “Error: “Using $this when not in object context””

Symptom: Fatal error: Uncaught Error: Using $this when not in object context

Cause: You tried to use $this inside a static method. Static methods don’t belong to any object instance.

Solution: Use self:: instead of $this-> for static members:

// Wrong
public static function getCount() {
return $this->count; // Error!
}
// Correct
public static function getCount() {
return self::$count;
}

Error: “Cannot access non-static property”

Section titled “Error: “Cannot access non-static property””

Symptom: Fatal error: Uncaught Error: Cannot access non-static property User::$name in static context

Cause: You tried to access an instance property from a static method.

Solution: Static methods can only access static members. Either:

  • Make the property static: private static string $name;
  • Or don’t call it from a static method
class Example {
private string $name;
private static int $count = 0;
// This works - static method accessing static property
public static function getCount(): int {
return self::$count;
}
// This won't work - static method can't access instance property
public static function getName(): string {
return $this->name; // ERROR!
}
}

Goal: Model a real-world object with proper encapsulation.

Create a file called car-exercise.php and implement:

  • A Car class with private properties for make, model, and year
  • Use constructor property promotion with type declarations (modern PHP 8.4 style)
  • Add a public method called displayInfo() that returns a string like “This car is a 2023 Ford Mustang.”
  • Add a public method called getAge() that calculates how old the car is based on the current year
  • Instantiate two different Car objects and display their information

Validation: Your output should look like:

This car is a 2023 Ford Mustang.
Car age: 2 years old
This car is a 2010 Toyota Camry.
Car age: 15 years old

Goal: Implement business logic protection using encapsulation.

Create a file called bank-exercise.php and implement:

  • A BankAccount class with a private property for balance (use float type)
  • A private property for accountNumber (use string type)
  • Use constructor property promotion to initialize both properties
  • Add a public method getBalance() that returns the current balance formatted as currency
  • Add a public method deposit(float $amount) that adds to the balance (validate that amount is positive)
  • Add a public method withdraw(float $amount) that subtracts from the balance, but only if there are sufficient funds
  • If withdrawal fails, display an error message

Validation: Test your class with this code:

$account = new BankAccount('ACC-12345', 1000.00);
echo "Initial balance: " . $account->getBalance() . PHP_EOL;
$account->deposit(500.00);
echo "After deposit: " . $account->getBalance() . PHP_EOL;
$account->withdraw(200.00);
echo "After withdrawal: " . $account->getBalance() . PHP_EOL;
$account->withdraw(2000.00); // Should fail
echo "Final balance: " . $account->getBalance() . PHP_EOL;

Expected output:

Initial balance: $1000.00
After deposit: $1500.00
After withdrawal: $1300.00
Error: Insufficient funds. Available: $1300.00
Final balance: $1300.00

Exercise 3: Create a Product Class (Challenge)

Section titled “Exercise 3: Create a Product Class (Challenge)”

Goal: Combine multiple OOP concepts in a practical scenario.

Create a Product class with:

  • Private properties: name, price, quantity
  • Constructor property promotion with type declarations
  • A method applyDiscount(int $percentage) that reduces the price
  • A method restock(int $amount) that increases quantity
  • A method sell(int $amount) that decreases quantity (with stock validation)
  • A method getTotalValue() that returns price * quantity
  • Proper input validation on all methods

Exercise 4: Create a Logger Class with Static Methods

Section titled “Exercise 4: Create a Logger Class with Static Methods”

Goal: Practice using static properties, methods, and class constants.

Create a file called logger-exercise.php and implement:

  • A Logger class with class constants for log levels:
    • const DEBUG = 'debug'
    • const INFO = 'info'
    • const WARNING = 'warning'
    • const ERROR = 'error'
  • A private static property $logs (array) to store all log messages
  • A private static property $logCount (int) to track total number of logs
  • A static method log(string $level, string $message) that:
    • Adds the message to $logs array with timestamp
    • Increments $logCount
    • Echoes the formatted log message
  • A static method getLogCount() that returns the total count
  • A static method getLogs() that returns all logs
  • A static method clearLogs() that empties the logs array

Validation: Test your class with this code:

Logger::log(Logger::INFO, 'Application started');
Logger::log(Logger::DEBUG, 'Loading configuration');
Logger::log(Logger::WARNING, 'Deprecated function used');
Logger::log(Logger::ERROR, 'Database connection failed');
echo "\nTotal logs: " . Logger::getLogCount() . PHP_EOL;
echo "\nAll logs:\n";
print_r(Logger::getLogs());

Expected output (timestamps will vary):

[INFO] Application started
[DEBUG] Loading configuration
[WARNING] Deprecated function used
[ERROR] Database connection failed
Total logs: 4
All logs:
Array
(
[0] => [2024-01-15 14:30:22] [INFO] Application started
[1] => [2024-01-15 14:30:22] [DEBUG] Loading configuration
[2] => [2024-01-15 14:30:22] [WARNING] Deprecated function used
[3] => [2024-01-15 14:30:22] [ERROR] Database connection failed
)

Congratulations! You’ve just learned the foundational concepts of Object-Oriented Programming, one of the most important paradigms in modern software development. You now understand:

  • How to create classes (blueprints) with properties (data) and methods (behavior)
  • How to instantiate objects (actual instances) from those blueprints
  • How constructors initialize object state automatically
  • How visibility keywords protect data and enforce encapsulation
  • Modern PHP 8.4 features like constructor property promotion and type declarations
  • How static properties and methods belong to the class itself, not instances
  • How class constants provide unchanging configuration values

These concepts are the foundation of virtually all modern PHP frameworks (Laravel, Symfony, WordPress plugins) and professional PHP development. Every time you use $request->get() or $user->save() in a framework, you’re working with objects and methods. When you see Config::get('app.name') or Carbon::now(), you’re using static methods.

You can now model real-world concepts in code, bundle data with behavior, and protect object state from unwanted modifications. This is a massive leap from procedural programming.

In Chapter 09, we’ll build on this foundation by learning about:

  • Inheritance: How classes can extend other classes to share behavior
  • Abstract classes: Templates that enforce structure in child classes
  • Interfaces: Contracts that guarantee specific methods exist
  • Polymorphism: How different objects can be used interchangeably

These advanced OOP concepts will enable you to build flexible, maintainable applications.

Test your understanding of Object-Oriented Programming concepts: