Reader Stacks

PHP OOP Fundamentals: Access Modifiers, Static Properties, and Traits

A trait solves the exact problem PHP's single-inheritance model creates — reusing the same method implementation across classes that can't share a common parent class at all.

Three core PHP OOP concepts — access modifiers, static properties, and traits — come up constantly in any real codebase, including Laravel's own internals, and are worth understanding together as a foundation.

Access modifiers: public, protected, and private

class BankAccount
{
    public string $accountHolder;
    protected float $balance = 0;
    private string $pin;

    public function __construct(string $accountHolder, string $pin)
    {
        $this->accountHolder = $accountHolder;
        $this->pin = $pin;
    }
}

public is accessible from anywhere; protected is accessible from within the class itself and any class that extends it; private is accessible only from within the exact class it's declared in, not even from a child class.

Why protected matters for inheritance specifically

class SavingsAccount extends BankAccount
{
    public function addInterest(float $rate): void
    {
        $this->balance += $this->balance * $rate; // works — balance is protected
    }
}
class SavingsAccount extends BankAccount
{
    public function debug(): void
    {
        echo $this->pin; // ERROR — pin is private, not accessible even from a child class
    }
}

This is the precise distinction between protected and private — a subclass can access an inherited protected property directly, but a private property remains genuinely locked to the exact class that declared it, even for its own subclasses.

Static properties and methods

class Counter
{
    public static int $count = 0;

    public static function increment(): void
    {
        self::$count++;
    }
}
Counter::increment();
Counter::increment();
echo Counter::$count; // 2

A static property belongs to the class itself, not to any individual instance — every instance (and code calling the class directly) shares the exact same single value, unlike a regular property, which each object instance has its own independent copy of.

self:: vs static:: for referencing the current class

class ParentClass
{
    public static function create(): static
    {
        return new static(); // late static binding — resolves to the actual called class
    }
}

class ChildClass extends ParentClass {}

$instance = ChildClass::create(); // returns a ChildClass instance, not ParentClass

static:: (late static binding) resolves to whichever class was actually called, even through inheritance — self:: always resolves to the exact class where the code is physically written, which matters specifically when a static method is inherited and called on a child class.

Traits: sharing method implementations across unrelated classes

trait Loggable
{
    public function log(string $message): void
    {
        echo "[" . static::class . "] {$message}" . PHP_EOL;
    }
}

class Order
{
    use Loggable;
}

class User
{
    use Loggable;
}
(new Order())->log('Order created'); // [Order] Order created
(new User())->log('User registered'); // [User] User registered

A trait solves the exact limitation PHP's single-inheritance model creates — Order and User here have no common meaningful parent class to share the log() method through, but both can independently use the same trait to get its methods, which is precisely the problem traits exist to solve.

Using multiple traits in one class

class Product
{
    use Loggable, HasSlug, SoftDeletes;
}

This is exactly the pattern behind Laravel's own SoftDeletes trait, and the custom HasSlug trait covered elsewhere on this site — Laravel's architecture relies heavily on traits precisely because Eloquent models need to mix in many independent, reusable behaviors without a rigid single-inheritance chain forcing them all through one base class.