08: Introduction to Object-Oriented Programming

Chapter 08: Introduction to Object-Oriented Programming
Section titled “Chapter 08: Introduction to Object-Oriented Programming”Overview
Section titled “Overview”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.
Prerequisites
Section titled “Prerequisites”- 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
What You’ll Build
Section titled “What You’ll Build”By the end of this chapter, you will have created:
- A
Userclass 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
Databaseclass demonstrating static properties, methods, and constants - Practical examples of factory methods and configuration classes
- Working exercises:
Car,BankAccount,Product, andLoggerclasses
All examples will be working PHP scripts you can run immediately.
Quick Start
Section titled “Quick Start”Want to see OOP in action right away? Create this file and run it:
<?phpclass 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;# Run itphp quick-user.phpExpected output:
Hello, DaleName: DaleThis compact example uses PHP 8.4’s constructor property promotion, which we’ll explain below. Now let’s build this understanding step by step.
Objectives
Section titled “Objectives”- 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
newkeyword. - Use the special
__constructmethod 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.
OOP Concepts: Core Principles
Section titled “OOP Concepts: Core Principles”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:
privateproperties are only accessible within the class itself—external code cannot read or modify them directlypublicmethods are accessible from anywhere—they provide the controlled interface to interact with the objectprotected(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:
<?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.
Actions
Section titled “Actions”-
Create a new file called
oop-step1.php. -
Define your first class using this structure:
<?phpclass 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;- Run the script:
php oop-step1.phpExpected Result
Section titled “Expected Result”Hello, DaleWhy This Works
Section titled “Why This Works”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.
Actions
Section titled “Actions”-
Create a new file called
oop-step2.php. -
Create multiple objects from the same class:
<?phpclass 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 stateecho $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- Run the script:
php oop-step2.phpExpected Result
Section titled “Expected Result”Hello, DaleHello, AliceHello, Dale HurleyHello, AliceWhy This Works
Section titled “Why This Works”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.
Actions
Section titled “Actions”-
Create a new file called
oop-step3.php. -
Add a constructor to your class:
<?phpclass 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;- Run the script:
php oop-step3.phpExpected Result
Section titled “Expected Result”A new user named 'Dale' has been created.Hello, DaleA new user named 'Alice' has been created.Hello, AliceWhy This Works
Section titled “Why This Works”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.
Actions
Section titled “Actions”-
Create a new file called
oop-step4.php. -
Add private properties and getter/setter methods:
<?phpclass 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 propertiesecho $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"- Run the script:
php oop-step4.phpExpected Result
Section titled “Expected Result”Daledale@example.comName updated successfully.Dale HurleyError: Name cannot be empty.Dale HurleyWhy This Works
Section titled “Why This Works”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.
Validation Check
Section titled “Validation Check”Try adding this line to your script to see the error:
<?php// Add this to the bottom of oop-step4.phpecho $user1->name; // Fatal error: Cannot access private propertyYou’ll see:
Fatal error: Uncaught Error: Cannot access private property User::$nameStep 5: Modern PHP 8.4 Features (~4 min)
Section titled “Step 5: Modern PHP 8.4 Features (~4 min)”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.
Constructor Property Promotion
Section titled “Constructor Property Promotion”Instead of declaring properties separately and then assigning them in the constructor, you can do both at once:
Actions
Section titled “Actions”-
Create a new file called
oop-modern.php. -
Use constructor property promotion and type declarations:
<?phpclass 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);- Run the script:
php oop-modern.phpExpected Result
Section titled “Expected Result”Hello, I'm Dale, 35 years old.Adult? YesHello, I'm Alice, 18 years old.Adult? YesWhy This Works
Section titled “Why This Works”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.
Benefits of This Approach
Section titled “Benefits of This Approach”- 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.
Class Constants
Section titled “Class Constants”Constants are values that never change and belong to the class, not instances. They’re perfect for fixed configuration values or status codes.
Static Properties and Methods
Section titled “Static Properties and Methods”Static members belong to the class itself. They’re shared across all instances and can be accessed without creating an object.
Actions
Section titled “Actions”-
Create a new file called
oop-static.php. -
Use static members and constants:
<?phpclass 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 objectecho "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 countecho "Total connections: " . Database::getConnectionCount() . PHP_EOL;
// Each instance has its own nameecho "DB1 name: " . $db1->getName() . PHP_EOL;echo "DB2 name: " . $db2->getName() . PHP_EOL;- Run the script:
php oop-static.phpExpected Result
Section titled “Expected Result”Database host: localhostDefault DSN: localhost:3306Total connections: 3DB1 name: users_dbDB2 name: products_dbWhy This Works
Section titled “Why This Works”- Class constants (
const) are accessed withClassName::CONSTANT_NAMEand never change - Static properties belong to the class, not instances—there’s only one
$connectionCountshared by allDatabaseobjects - 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
Real-World Use Cases
Section titled “Real-World Use Cases”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 needed2. 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 #1RequestLogger::logRequest(); // Request #2RequestLogger::logRequest(); // Request #34. 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-friendlyThe Difference: $this-> vs self::
Section titled “The Difference: $this-> vs self::”$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. :::
Step 7: PHP 8.4 Modern Features (~6 min)
Section titled “Step 7: PHP 8.4 Modern Features (~6 min)”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
Section titled “Property Hooks”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.
-
Create a new file called
oop-property-hooks.php. -
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.comecho "First Name: " . $user->firstName . PHP_EOL; // John- 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!- 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 Fahrenheitecho "Stored as: " . $temp->getCelsius() . "°C" . PHP_EOL; // 20°Cecho "Read as: " . $temp->fahrenheit . "°F" . PHP_EOL; // 68°FWhy 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
Section titled “Asymmetric Visibility”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.”
-
Create a new file called
oop-asymmetric-visibility.php. -
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 readecho "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';- 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 directlyWhy 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)
Comparison: Traditional vs Modern PHP 8.4
Section titled “Comparison: Traditional vs Modern PHP 8.4”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:
Troubleshooting
Section titled “Troubleshooting”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:
// Wrongecho $user->name;
// Correctecho $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');Error: “Call to undefined method”
Section titled “Error: “Call to undefined method””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 nameType Declaration Error
Section titled “Type Declaration Error”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); // IntegerNotice: “Undefined property”
Section titled “Notice: “Undefined property””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:
// Wrongpublic static function getCount() { return $this->count; // Error!}
// Correctpublic 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! }}Exercises
Section titled “Exercises”Exercise 1: Create a Car Class
Section titled “Exercise 1: Create a Car Class”Goal: Model a real-world object with proper encapsulation.
Create a file called car-exercise.php and implement:
- A
Carclass withprivateproperties formake,model, andyear - Use constructor property promotion with type declarations (modern PHP 8.4 style)
- Add a
publicmethod calleddisplayInfo()that returns a string like “This car is a 2023 Ford Mustang.” - Add a
publicmethod calledgetAge()that calculates how old the car is based on the current year - Instantiate two different
Carobjects 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 oldExercise 2: Create a BankAccount Class
Section titled “Exercise 2: Create a BankAccount Class”Goal: Implement business logic protection using encapsulation.
Create a file called bank-exercise.php and implement:
- A
BankAccountclass with aprivateproperty forbalance(usefloattype) - A
privateproperty foraccountNumber(usestringtype) - Use constructor property promotion to initialize both properties
- Add a
publicmethodgetBalance()that returns the current balance formatted as currency - Add a
publicmethoddeposit(float $amount)that adds to the balance (validate that amount is positive) - Add a
publicmethodwithdraw(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 failecho "Final balance: " . $account->getBalance() . PHP_EOL;Expected output:
Initial balance: $1000.00After deposit: $1500.00After withdrawal: $1300.00Error: Insufficient funds. Available: $1300.00Final balance: $1300.00Exercise 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 returnsprice * 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
Loggerclass 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
$logsarray with timestamp - Increments
$logCount - Echoes the formatted log message
- Adds the message to
- 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)Wrap-up
Section titled “Wrap-up”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.
What You’ve Achieved
Section titled “What You’ve Achieved”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.
Next Steps
Section titled “Next Steps”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.
Knowledge Check
Section titled “Knowledge Check”Test your understanding of Object-Oriented Programming concepts:
Further Reading
Section titled “Further Reading”- PHP Manual: Classes and Objects
- PHP 8.4 Release Notes — See all the latest OOP features
- Constructor Property Promotion
- SOLID Principles — Design principles for OOP (we’ll cover these concepts gradually)