← Back to Dashboard

OOP Prerequisite Path

OOP Basics Refresher

OOP Basics Refresher

Object-Oriented Programming (OOP) is the foundation everything else in this course builds on. Before diving into design patterns, let's make sure we're all on the same page with the fundamentals.

Classes and Objects

A class is a blueprint. An object is an instance of that blueprint. Think of a class like a form template at a payment kiosk — the template defines what fields exist (amount, reference number, payment method), and each filled-out form is an object.

class Transaction {
    public string $reference;
    public float $amount;

    public function __construct(string $reference, float $amount) {
        $this->reference = $reference;
        $this->amount = $amount;
    }
}

$txn = new Transaction('TXN-001', 150.00);

Visibility: public, protected, private

Visibility controls who can access a property or method:

  • public — accessible from anywhere
  • protected — accessible within the class and its children
  • private — accessible only within the class itself
class PaymentGateway {
    public string $name;           // anyone can read this
    protected string $apiKey;      // only this class and subclasses
    private string $secretToken;   // only this class

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

Rule of thumb: Start with private. Promote to protected only when a subclass genuinely needs it. Use public only for the class's intended API.

Constructors and Property Promotion

PHP 8 introduced constructor property promotion, which eliminates boilerplate:

// Before PHP 8
class Receipt {
    private string $id;
    private float $total;

    public function __construct(string $id, float $total) {
        $this->id = $id;
        $this->total = $total;
    }
}

// PHP 8+ with promotion
class Receipt {
    public function __construct(
        private string $id,
        private float $total,
    ) {}
}

Both are equivalent. The promoted version is what you'll see in modern Laravel code.

Interfaces

An interface defines a contract — a list of methods a class must implement. It says what something can do without saying how.

interface PaymentMethodInterface {
    public function charge(float $amount): bool;
    public function refund(float $amount): bool;
}

Any class that implements this interface must provide both charge() and refund().

Abstract Classes

An abstract class sits between an interface and a concrete class. It can contain both abstract methods (no body, must be implemented by children) and concrete methods (with an implementation).

abstract class BaseGateway {
    abstract public function connect(): void;

    public function log(string $message): void {
        echo "[Gateway] {$message}\n";
    }
}

When to use which:

  • Use an interface when you want to define a contract that multiple unrelated classes can implement.
  • Use an abstract class when you want to share code between related classes.

Type Hinting

Type hints enforce what types a method accepts and returns. They turn runtime errors into compile-time-like errors:

function processPayment(PaymentMethodInterface $method, float $amount): bool {
    return $method->charge($amount);
}

With type hints, if someone passes the wrong type, PHP throws a TypeError immediately instead of failing silently deep in the code.

Nullable Types

Sometimes a method legitimately returns a value or nothing. A ? before the type makes it nullable:

public function findById(string $id): ?array   // returns array OR null
{
    return $this->store[$id] ?? null;
}

The ?? is the null coalescing operator — it returns the left side if it exists, or the right side otherwise. You'll see ?array, ?string, and ?int throughout this course.

echo vs return

This distinction trips up many beginners:

  • echo sends text directly to the screen. It does not give a value back to the calling code.
  • return sends a value back to the caller. The caller can store it, pass it to another function, or test it.
// echo — for demonstration and output
public function log(string $msg): void {
    echo "[LOG] {$msg}\n";  // prints to screen, returns nothing
}

// return — for passing data between objects
public function format(string $msg): string {
    return "[FORMATTED] {$msg}";  // gives the string BACK to whoever called this
}

In this course, examples often use echo to show output. In challenges, read the return type carefully. If a method signature says : string, you must return a string, not echo it.

Late Static Binding

Late static binding (LSB) resolves the class that actually called a static method, not the class where the method is defined. Use static:: instead of self:::

class BaseModel {
    public static function create(): static {
        echo "Creating a " . static::class . "\n";
        return new static();
    }
}

class User extends BaseModel {}

User::create(); // Outputs: "Creating a User" (not "BaseModel")

Laravel uses LSB extensively — User::find(1) works because Model::find() uses static:: to return the correct subclass.


Modern PHP Syntax You'll Need

This course uses PHP 8.x features that you may not have seen in CodeIgniter 2 or older PHP. Here's a quick reference — you don't need to memorise all of this now, but come back here when you hit unfamiliar syntax in a challenge.

declare(strict_types=1)

Every file in this course starts with this line. It tells PHP to enforce type hints strictly — no silent coercion:

declare(strict_types=1);

function add(float $a, float $b): float {
    return $a + $b;
}

add(1.5, 2.5);     // works — both are floats
add('1.5', '2.5'); // TypeError! Strings are NOT silently converted to floats

Without strict types, PHP would silently convert '1.5' to 1.5. With it, you get an explicit error. This catches bugs early. When you see a TypeError in a challenge, check whether you're passing the right type — and use casts like (float) or (int) when converting from raw input.

readonly Classes and Properties

PHP 8.2 introduced readonly class, which makes all properties immutable after construction:

readonly class PaymentData {
    public function __construct(
        public string $name,
        public float $amount,
    ) {}
}

$data = new PaymentData('Maria', 500.0);
$data->amount = 999; // Error! Cannot modify readonly property

You'll use this in the DTO module. If you see a test checking that properties "are readonly," use either readonly class or readonly on individual properties.

match Expression

match is PHP 8's cleaner replacement for switch. It uses strict comparison and returns a value:

// switch (old way)
switch ($type) {
    case 'gcash': $gateway = new GCashGateway(); break;
    case 'maya':  $gateway = new MayaGateway(); break;
    default: throw new \InvalidArgumentException("Unknown: {$type}");
}

// match (PHP 8 way)
$gateway = match ($type) {
    'gcash' => new GCashGateway(),
    'maya'  => new MayaGateway(),
    default => throw new \InvalidArgumentException("Unknown: {$type}"),
};

match is shorter, returns a value directly, and uses === (strict) comparison. You'll use it in Factory challenges.

Static Methods

A static method belongs to the class itself, not to an instance. You call it with :: instead of ->:

class IdGenerator {
    public static function generate(): string {
        return 'ID-' . strtoupper(substr(md5(uniqid()), 0, 8));
    }
}

$id = IdGenerator::generate();  // No need to write: new IdGenerator()

You'll see static factory methods (ClassName::create(), DTO::fromArray()) throughout this course. If you get "Non-static method cannot be called statically," add the static keyword to the method.


Anti-Pattern: Procedural Spaghetti

Here's the kind of code you might find in a legacy CodeIgniter 2 application — everything in one place, no objects, no contracts:

// ❌ Procedural approach — hard to test, impossible to extend
function process_payment($type, $amount, $reference) {
    if ($type === 'gcash') {
        $api_key = get_config('gcash_key');
        $response = curl_post('https://api.gcash.com/pay', [
            'amount' => $amount,
            'ref' => $reference,
            'key' => $api_key,
        ]);
        if ($response['status'] === 'ok') {
            log_to_file("GCash payment {$reference} succeeded");
            return true;
        }
    } elseif ($type === 'maya') {
        // completely different structure...
    }
    return false;
}

Problems:

  • Adding a new payment method means editing this function
  • No way to test without hitting real APIs
  • No contracts — nothing guarantees different methods have the same interface
  • Logging, API calls, and business logic are all tangled together

OOP gives us the tools to fix every one of these problems. The rest of this course shows you how.

What You Learned

  • Interfaces define contracts — method signatures without implementation — that classes must fulfill
  • Visibility modifiers (public, protected, private) control access and encapsulation
  • Type hinting in constructors and method signatures catches bugs early and makes dependencies explicit
  • Constructor property promotion (PHP 8.x) reduces boilerplate when storing injected dependencies

OOP in Laravel

Laravel is built from the ground up using OOP principles. Understanding these concepts isn't just academic — they are the daily tools you use when working with the framework.

Interfaces Everywhere

Laravel's core is defined by interfaces (called "contracts" in Laravel). For example:

// Illuminate\Contracts\Cache\Store
interface Store {
    public function get(string $key): mixed;
    public function put(string $key, mixed $value, int $seconds): bool;
    public function forget(string $key): bool;
}

This interface is implemented by RedisStore, FileStore, MemcachedStore, and others. Your application code doesn't care which one is being used — it only depends on the Store contract.

You'll find these contracts in the Illuminate\Contracts namespace:

  • Illuminate\Contracts\Mail\Mailer
  • Illuminate\Contracts\Queue\Queue
  • Illuminate\Contracts\Auth\Guard

Constructor Injection with Type Hints

Laravel's service container automatically resolves type-hinted constructor parameters. When you write this:

class PaymentController extends Controller {
    public function __construct(
        private PaymentService $paymentService,
    ) {}
}

Laravel reads the PaymentService type hint and automatically creates and injects an instance. No new PaymentService() needed.

Abstract Base Classes

Laravel provides abstract classes you extend to gain functionality:

  • Illuminate\Database\Eloquent\Model — all your models extend this
  • Illuminate\Console\Command — Artisan commands
  • Illuminate\Http\FormRequest — form validation classes
  • Illuminate\Notifications\Notification — notification channels

Each provides shared behavior (like Model::find()) while requiring subclasses to define their specific details (like $table or $fillable).

Late Static Binding in Practice

Every time you call User::find(1) or Order::where('status', 'pending')->get(), you're relying on late static binding. The Model class uses static:: so that the correct subclass is returned:

// In Illuminate\Database\Eloquent\Model
public static function find(mixed $id): ?static {
    return static::query()->find($id);
}

Without LSB, User::find(1) would return a generic Model instead of a User.

Visibility in Laravel

Laravel models use visibility carefully:

  • public for API methods like save(), delete()
  • protected for properties you configure: $fillable, $casts, $table
  • private is rarely used in framework base classes because it would prevent subclass access

This example is pre-filled and runnable. Click "Run" to see the output.

Output

                

            

Complete the code below to pass all test cases.