Skip to main content
    Courses/PHP/Object-Oriented PHP

    Lesson 11 • Expert

    Object-Oriented PHP 🏗️

    By the end of this lesson you'll be able to model real things as classes — bundling their data and behaviour into objects with constructors, visibility, static members, constants, and readonly properties — the foundation of every modern PHP application.

    What You'll Learn in This Lesson

    • Define a class with typed properties and methods, and build objects with new
    • Use public, private, and protected to control who can touch each property
    • Initialise objects with a __construct() constructor and the $this keyword
    • Cut boilerplate with PHP 8 constructor property promotion
    • Share data and helpers across a class with static properties, static methods, and self::/static::
    • Lock down values with class constants and readonly properties

    1️⃣ Classes, Properties & Objects

    Up to now your data has lived in loose variables. Object-oriented programming (OOP) bundles related data and the code that works on it into one unit. A class is the blueprint: it lists the properties (the data each object holds) and the methods (the things it can do). An object is a single thing built from that blueprint with the new keyword. You reach inside an object with the arrow operator ->. Typing each property (string $name) tells PHP — and your editor — exactly what kind of value belongs there.

    A class, and two objects built from it
    <?php
    // A class is a BLUEPRINT. It describes what every object made from it
    // will hold (properties) and what it can do (methods). Nothing happens
    // until you build an actual object from it with 'new'.
    
    class Dog
    {
        // Typed properties with visibility:
        //   public    -> readable/writable from anywhere
        //   private   -> only inside THIS class
        public string $name;     // the dog's name
        public int    $age;      // the dog's age in years
    }
    
    // 'new' builds an OBJECT (an instance) from the blueprint.
    $rex = new Dog();   // $rex is now a Dog object
    
    // Reach inside the object with -> to set its properties.
    $rex->name = "Rex";
    $rex->age  = 3;
    
    echo "Name: {$rex->name}\n";   // {$rex->name} pulls the value back out
    echo "Age:  {$rex->age}\n";
    
    // Each object is independent — a second Dog has its own data.
    $buddy = new Dog();
    $buddy->name = "Buddy";
    $buddy->age  = 7;
    
    echo "Name: {$buddy->name}\n";
    echo "Age:  {$buddy->age}\n";
    Output
    Name: Rex
    Age:  3
    Name: Buddy
    Age:  7
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Notice $rex and $buddy are completely separate — each carries its own name and age. That independence is the whole point: one blueprint, many self-contained objects.

    2️⃣ The Constructor, $this & Methods

    Setting every property by hand is tedious. A constructor — the special method named __construct() — runs automatically the instant you write new, so an object is born ready to use. Inside any method, $this means "this particular object", so $this->name = $name stores the passed-in value on the object. A method is just a function that lives in the class and can use $this.

    One line builds a ready-to-use object
    <?php
    // Setting every property by hand (like the last example) is tedious and
    // easy to forget. A CONSTRUCTOR fixes that: __construct() runs automatically
    // the moment you write 'new', so an object is fully built in one line.
    
    class Dog
    {
        public string $name;
        public int    $age;
    
        // $this means "this particular object". $this->name is the property
        // on the object being built; $name is the value passed in.
        public function __construct(string $name, int $age)
        {
            $this->name = $name;   // store the argument on the object
            $this->age  = $age;
        }
    
        // A METHOD is a function that belongs to the class and can use $this.
        public function describe(): string
        {
            return "{$this->name} is {$this->age} years old";
        }
    }
    
    // The arguments go straight into __construct — no manual setup needed.
    $rex   = new Dog("Rex", 3);
    $buddy = new Dog("Buddy", 7);
    
    echo $rex->describe() . "\n";     // calls the method with ->
    echo $buddy->describe() . "\n";
    Output
    Rex is 3 years old
    Buddy is 7 years old
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    3️⃣ Visibility & Constructor Promotion

    Each property has a visibility that decides who may touch it. public means anyone, anywhere; private means only code inside the same class; protected is like private but also visible to classes that extend it. Hiding data behind private lets a class guarantee it stays valid — outsiders go through methods (a getter like getBalance()) instead of poking the value directly. PHP 8's constructor property promotion declares the property and assigns the argument in one move: put the visibility keyword right in the constructor's parameter list and PHP does the $this->x = $x for you.

    Promotion + private data behind a getter
    <?php
    // PHP 8 CONSTRUCTOR PROPERTY PROMOTION: declare the property AND assign it
    // in one step by putting the visibility keyword in the constructor signature.
    // PHP creates the property and runs $this->x = $x for you. Same result,
    // far less boilerplate.
    
    class BankAccount
    {
        // 'private' here both DECLARES $balance and assigns the argument to it.
        // 'protected' would let subclasses see it; 'public' anyone.
        public function __construct(
            public string $owner,        // promoted public property
            private float  $balance = 0, // promoted private property, default 0
        ) {}
    
        public function deposit(float $amount): void
        {
            $this->balance += $amount;   // private is reachable from INSIDE
        }
    
        public function getBalance(): float
        {
            return $this->balance;       // a 'getter' exposes it safely
        }
    }
    
    $acc = new BankAccount("Alice", 100);
    $acc->deposit(50);
    
    echo "Owner:   {$acc->owner}\n";              // public -> read directly
    echo "Balance: {$acc->getBalance()}\n";       // private -> via the getter
    Output
    Owner:   Alice
    Balance: 150
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    The whole class is just a constructor and two short methods, yet $balance can never be set to a nonsense value from outside — that's encapsulation, the real payoff of private.

    4️⃣ Static Members & Class Constants

    Most properties belong to an object. A static property or method belongs to the class itself — there's one shared copy and you reach it with Class::member, no new needed. From inside the class you write self:: to mean "this class" (static:: is its close cousin that respects subclasses — handy once you use inheritance). A class constant (const MAX = 100;) is a fixed value attached to the class that can never change, written in UPPER_CASE by convention.

    A shared counter with static + a constant
    <?php
    // STATIC members belong to the CLASS itself, not to any one object.
    // There is exactly one copy, shared by everything. Use Class::member to
    // reach it — no 'new' required.
    
    class Counter
    {
        // A class CONSTANT never changes. By convention it's UPPER_CASE.
        const MAX = 100;
    
        // A static PROPERTY is shared across every object of the class.
        public static int $count = 0;
    
        public function __construct()
        {
            self::$count++;          // self:: = "this class" — bumps the shared count
        }
    
        // A static METHOD is called on the class, not an instance.
        public static function howMany(): string
        {
            // self::MAX reads the constant; self::$count reads the static property.
            return self::$count . " of " . self::MAX . " created";
        }
    }
    
    new Counter();   // each 'new' runs the constructor, so $count goes up
    new Counter();
    new Counter();
    
    echo Counter::howMany() . "\n";   // Class::method() — no object needed
    echo "MAX is " . Counter::MAX . "\n";
    Output
    3 of 100 created
    MAX is 100
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Now you try. The class below is almost complete — fill in each ___ using the 👉 hint, then run it and check it against the Output panel.

    🎯 Your turn: build a Book class
    <?php
    // 🎯 YOUR TURN — build a Book class, then run it.
    // Fill in each blank marked ___ using the 👉 hints.
    
    class Book
    {
        // 1) Declare two PUBLIC typed properties: a string $title and an int $pages
        ___ string $title;       // 👉 add the visibility keyword:  public
        public ___ $pages;       // 👉 add the type:  int
    
        // 2) Finish the constructor so it stores both arguments on the object
        public function __construct(string $title, int $pages)
        {
            $this->title = ___;  // 👉 the argument to save here is  $title
            ___->pages   = $pages;   // 👉 the object refers to itself as  $this
        }
    
        public function summary(): string
        {
            return "{$this->title} — {$this->pages} pages";
        }
    }
    
    $b = new Book("PHP Basics", 240);
    echo $b->summary() . "\n";
    
    // ✅ Expected output:
    //    PHP Basics — 240 pages
    ?>
    Output
    PHP Basics — 240 pages
    Add the public keyword, the int type, the $title argument, and the $this reference, then run it. You should see one summary line.

    One more — this time the blanks are about static members. Fill them in so the shared counter works.

    🎯 Your turn: a static robot counter
    <?php
    // 🎯 YOUR TURN — finish the static counter, then run it.
    // Replace each ___ using the 👉 hints.
    
    class Robot
    {
        public static int $built = 0;   // shared across ALL robots
    
        public function __construct()
        {
            // 1) Increase the SHARED counter by one.
            ___::$built++;              // 👉 "this class" keyword is  self
        }
    }
    
    new Robot();
    new Robot();
    
    // 2) Read the shared count off the CLASS, not an object.
    echo Robot::___ . " robots built\n";   // 👉 the static property is  $built
    
    // ✅ Expected output:
    //    2 robots built
    ?>
    Output
    2 robots built
    Use self to bump the shared count and $built to read it off the class. Two robots are built, so the count is 2.

    5️⃣ Readonly Properties

    Some values should be set once and then frozen — an id, a created-at timestamp, a primary key. PHP 8.1's readonly keyword does exactly that: the property can be assigned a single time (almost always inside the constructor) and any later attempt to change it throws an error. It pairs perfectly with constructor promotion, giving you tamper-proof objects with almost no code.

    A value that locks after construction
    <?php
    // A READONLY property (PHP 8.1) can be set ONCE — usually in the constructor —
    // then never changed again. Perfect for an ID or anything that must stay fixed.
    
    class User
    {
        public function __construct(
            public readonly int    $id,     // set once, then locked
            public string          $name,   // a normal, changeable property
        ) {}
    }
    
    $u = new User(1, "Alice");
    
    echo "ID:   {$u->id}\n";
    echo "Name: {$u->name}\n";
    
    $u->name = "Alicia";   // fine — $name is a normal property
    echo "Renamed to: {$u->name}\n";
    
    // $u->id = 2;   // would crash: "Cannot modify readonly property User::\$id"
    echo "ID is still: {$u->id}\n";
    Output
    ID:   1
    Name: Alice
    Renamed to: Alicia
    ID is still: 1
    This is real code — run it for free atonecompiler.com/phpor in your own editor.

    Common Errors (and the fix)

    • "Cannot access private property User::$balance" — you tried to read or write a private property from outside the class (e.g. $acc->balance). Private means in-class only. Add a public method — a getter like getBalance() — and call that instead.
    • "Undefined variable $name" inside a method — you forgot $this->. A bare $name is a local variable; the property is $this->name. Inside any non-static method, reach properties through $this.
    • "Using $this when not in object context" — you used $this inside a static method. Static methods belong to the class, not an object, so there's no $this. Use self:: for static members, or make the method non-static if it needs object data.
    • "Too few arguments to function __construct(), 0 passed" — you wrote new Dog() but the constructor requires arguments. Pass them: new Dog("Rex", 3), or give the parameters defaults (int $age = 0).
    • "Cannot modify readonly property User::$id" — you tried to reassign a readonly property after it was first set. That's the whole point of readonly: set it once in the constructor and never again. If it must change, don't mark it readonly.

    Pro Tips

    • 💡 Start private, widen only when needed. It's easy to make a property more visible later; locking down a public property others already use is painful.
    • 💡 Reach for constructor promotion by default. It removes the declare-then-assign boilerplate and is the modern, idiomatic style.
    • 💡 Use constants and readonly for "facts that never change." They document intent and let PHP catch accidental edits for you.

    📋 Quick Reference — PHP OOP

    SyntaxExampleWhat It Does
    classclass Dog { }Define a blueprint
    newnew Dog("Rex", 3)Build an object (instance)
    public/private/protectedprivate int $age;Set who can access a property
    __constructfunction __construct(...)Runs automatically on new
    $this->$this->nameThis object's property/method
    static + self::self::$countClass-level data, one shared copy
    constconst MAX = 100;A fixed value on the class
    readonlyreadonly int $idSet once, then locked

    Frequently Asked Questions

    Q: What is the difference between a class and an object?

    A class is the blueprint and an object is a thing built from it. The class Dog describes that every dog has a name and an age and can bark; each time you write new Dog() you get a separate object with its own name and age. One class can produce as many independent objects as you like — changing one object never affects the others.

    Q: When should a property be public, private, or protected?

    Default to private and only widen it when you have a reason. private hides a property so only code inside the same class can touch it, which lets the class guarantee its own data stays valid (you expose it through methods like getBalance() instead). protected is like private but also visible to child classes that extend it. public means anyone, anywhere can read and write it — convenient but it removes the safety the other two give you.

    Q: What does $this actually mean?

    $this is a reference to the specific object a method is running on. When you call $rex->describe(), inside describe() $this IS $rex, so $this->name reads Rex's name; call $buddy->describe() and the same code sees Buddy. You can only use $this inside a non-static method, because static methods belong to the class and have no particular object to point at.

    Q: What is constructor property promotion and why use it?

    It is PHP 8 shorthand that lets you declare a property and assign its constructor argument in a single line, by writing the visibility keyword (public/private/protected) right in the constructor's parameter list. Instead of declaring private float $balance, listing it as a parameter, and writing $this->balance = $balance, you write just private float $balance in the signature. It is the same behaviour with far less boilerplate, which is why modern PHP code uses it everywhere.

    Q: When should I use static instead of a normal property or method?

    Use static when the data or behaviour belongs to the class as a whole rather than to one object — for example a shared counter of how many objects exist, or a utility method that does not need any object's data. You reach static members with Class::member (or self:: from inside the class) and they exist without ever calling new. If a method needs $this, it cannot be static.

    Mini-Challenge: Rectangle Class

    No code is filled in this time — just a brief and an outline. Write the class yourself, run it on onecompiler.com/php or your own machine, then check your result against the expected output in the comments. This is exactly the write-run-check loop you'll use on every real class.

    🎯 Mini-Challenge: write a Rectangle class
    <?php
    // 🎯 MINI-CHALLENGE: A Rectangle class.
    // No code is filled in — work from the steps below, then run it.
    //
    // 1. Declare a class called  Rectangle
    // 2. Give it a constructor using PROPERTY PROMOTION that takes
    //      public float $width  and  public float $height
    // 3. Add a method  area(): float  that returns $this->width * $this->height
    // 4. Add a method  describe(): string  that returns
    //      "Wx H = AREA"   e.g. "4x3 = 12"
    // 5. Build  new Rectangle(4, 3)  and echo its describe() on its own line
    //
    // Tip: inside double quotes, "{$this->width}x{$this->height}" reads both
    //      properties; call $this->area() to get the area.
    //
    // ✅ Expected output:
    //    4x3 = 12
    
    // your code here
    ?>
    Use constructor promotion for $width and $height, add area() and describe() methods, then echo describe(). It should print one line.

    🎉 Lesson Complete!

    • ✅ A class is a blueprint; new builds independent objects from it
    • Typed properties with public/private/protected control who can touch each value
    • __construct() runs on new, and $this refers to the current object inside methods
    • Constructor promotion declares and assigns a property in one line
    • static members + self::/static:: live on the class; const and readonly lock values down
    • Next lesson: Database Integration — connect your classes to a MySQL database and store objects for real

    Sign up for free to track which lessons you've completed and get learning reminders.

    Previous

    Cookie & Privacy Settings

    We use cookies to improve your experience, analyze traffic, and show personalized ads. You can manage your preferences below.

    By clicking "Accept All", you consent to our use of cookies for analytics and personalized advertising. You can customize your preferences or reject non-essential cookies.

    Privacy PolicyTerms of Service