Mastering Object Oriented Programming in PHP

Understanding Classes and Objects in PHP

Last Updated on November 14, 2023 15 min read

Hi friends! Welcome to the second installment of my Mastering Object-Oriented Programming in PHP tutorial series. If you're just joining us and haven't had the chance to explore our first tutorial, here's the link to catch up: Introduction to OOP in PHP. For those who are ready to dive right in, feel free to jump on board.

In the previous tutorial, we scratched the surface and watered the ground on OOP in PHP. Now, it's time to take that knowledge to the next level. In this tutorial, we'll explore classes and objects more comprehensively, We'll learn about constructors and destructors, class constants, static properties and methods and visibility.

Shall we?

Table of contents

Constructors and Destructors

In the previous tutorial, we learned that a class is a blueprint for creating objects. A constructor is a special method within a class. Its primary job is to initialize an object's properties when that object is created from the class blueprint. In simpler terms, constructors ensure that when a new instance of a class is born, it's set up correctly, ready to fulfill its role. Now that doesn't mean we can't do other things in the constructor, but typically, it's used to initialize properties.

Using Constructors

To define a constructor, simply define a method within your class with the special name __construct(). This method is automatically invoked when a new object is created from the class. A class can have only one constructor, and it can accept any number of parameters just like any other method (which means things like default values and type declarations are allowed). I'm a huge fan of type declarations, so I always use them in my code. If you're my reader, and you don't use type declarations, I'm coming for you! ?? Better check the Official PHP Documentation to learn more about type declarations before I get to you.

Anyways, let's take a look at an example:

class User
{
    public string $name;
    public string $email;

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

$user = new User('Kyrian Obikwelu', 'kyrianobikwelu@gmail.com');

echo $user->name; // Kyrian Obikwelu
echo $user->email; // kyrianobikwelu@gmail.com

In this example, our User class has a constructor that accepts two parameters: $name and $email. When we create a new instance of the User class, we pass in the required parameters and the constructor initializes the object's properties with the values we passed in.

A class can be defined with or without a constructor. If a class is defined without a constructor, PHP will automatically create an empty constructor for you. However, if a class is defined with a constructor, PHP will not create an empty constructor for you. So, if you want to define a constructor, you must do so explicitly.

While you can define a constructor with an infinite number of parameters, it's best to keep the number of parameters to a minimum. If you find yourself needing to pass in a lot of parameters, it's a good indication that your class is doing too much and should be refactored.

PHP 8 Constructor Property Promotion

Now, here's where things get even more exciting. In PHP 8, a new feature called "argument promotion" was introduced. This feature simplifies class construction by allowing you to define class properties directly in the constructor parameters. Let's see this in action:

class User
{
    public function __construct(
        public string $name,
        public string $email,
        public int $age = 25
    ) {}
}

With this new feature, we can now define our class properties directly in the constructor parameters. This means we no longer have to define our properties at the top of our class, and then start the rigorous process of setting them in the constructor. We can now define our properties and set them in the constructor in catch. This is a huge win for us developers. It makes our code cleaner and more concise. I love it!

Destructors

A destructor is another special method within a class. While constructors are responsible for initializing an object, destructors primarily perform cleanup operations just before an object is destroyed. In essence, they ensure that an object's resources are properly released and any final tasks are completed.

They're less commonly used compared to constructors. In fact, you may never need to use a destructor in your code. This is because, in many cases, PHP's garbage collection takes care of cleaning up after objects when they are no longer in use. Nevertheless, there are situations where destructors can be valuable. Resources like opening files, network connections, database connections, or any other external resource are not automatically cleaned up by PHP's garbage collector. By implementing a destructor, you can ensure these resources are released properly, preventing potential issues like memory leaks or resource exhaustion.

Destructors are automatically called just before an object is destroyed, whether it's due to the end of the script, when an object goes out of scope, or when an object is explicitly unset.

Here's a simple example:

class FileHandler {
    private $file;

    public function __construct(string $filename) {
        $this->file = fopen($filename, 'w');
    }

    public function write($data) {
        fwrite($this->file, $data);
    }

    public function __destruct() {
        fclose($this->file);
    }
}

$file = new FileHandler('example.txt');
$file->write('Hello, destructors!');
// File is automatically closed when $file goes out of scope

In this example, the destructor __destruct() ensures that the file handle is properly closed before the object is destroyed. It's like tidying up your workspace before you leave. You don't want to leave your workspace in a mess, do you?

Access Modifiers

Access modifiers are a fundamental part of object-oriented programming in PHP. They determine the visibility or accessibility of class properties and methods from outside the class. In PHP, there are three main visibility modifiers:

  1. public: This is the most permissive modifier. It allows a property or method to be accessed from anywhere, both inside and outside the class. It's like leaving your front door wide open for everyone to enter.

  2. protected: With this modifier, properties and methods can be accessed only within the class and its subclasses. It's like leaving your front door open for only your family members to enter.

  3. private: The most restrictive modifier, private, restricts access to only within the defining class. It's like locking your front door and keeping the key to yourself.

Why Use Access Modifiers?

Now, you might be wondering, "Why should I care about visibility modifiers?" The answer lies in the art of encapsulation, a fundamental principle of object-oriented programming. Encapsulation means bundling the data (properties) and methods that operate on the data into a single unit, a class. By controlling access to class members, we maintain a clear separation between what's "inside" the class and what's "outside." This separation offers several advantages:

  • Data Protection: With private properties, you protect sensitive data from external interference. For example, think of a bank account class where the balance should not be directly modified from outside.

  • Code Flexibility: By restricting access to certain properties and methods, you can change the inner workings of a class without affecting the code that uses the class. This is vital for code maintenance and evolution.

  • Security: In some cases, you might have methods that should not be publicly accessible, like a method for changing a password.

private vs. protected

The choice between private and protected depends on your design goals. If a property or method is specific to a class and should not be inherited by subclasses, use private. On the other hand, if it should be inherited and used by subclasses, opt for protected.

Let's see some code, enough talk!

class BankAccount {

    public function __construct(private float $balance = 0)
    {
    }
       

    public function deposit(float $amount): void
    {
        $this->balance += $amount;
    }

    public function withdraw(float $amount) : void
    {
        $this->balance -= $amount;
    }

    public function getBalance(): float
    {
        return $this->balance;
    }
}

$account = new BankAccount(1000);
$account->deposit(500);
$account->withdraw(200);

echo $account->getBalance(); // 1300

echo $account->balance; // Fatal error: Uncaught Error: Cannot access private property BankAccount::$balance
$account->balance = 1000000; // Fatal error: Uncaught Error: Cannot access private property BankAccount::$balance

Here, the balance property is private, ensuring that it cannot be accessed or modified directly from outside the class. Before even running the code, the IDE will warn us that we're trying to access a private property. This is a great example of how visibility modifiers can help us write better code.

The deposit and withdraw methods act as setters, allowing us to control how the balance is modified. We can add code to these methods to enforce specific rules, like ensuring that the balance is never negative or verifying the balance sheet. Because we only modify the balance through the deposit and withdraw methods, we can be sure that the balance is always valid, and it's easy to track who invoked the methods.

Since the balance is private, the getBalance method acts as a getter, allowing us to retrieve the balance from outside the class. This is a common pattern in OOP, where properties are private and methods are public. This is known as the Encapsulation Principle.

The readonly Modifier

In PHP 8, a new visibility modifier called readonly was introduced. This modifier is similar to private, but it allows a property to be initialized only once. After initialization, the property cannot be modified. This is useful for properties that should be set only once during object construction eg. DTOs (Data Transfer Objects).

Let's see this in action:

class User
{
    public function __construct(
        public readonly string $name,
        public readonly string $email,
        public readonly int $age = 25
    ) {}
}

$user = new User('Kyrian Obikwelu', 'kyrianobikwelu@gmail.com', 23);

echo $user->name; // Kyrian Obikwelu
echo $user->email; // kyrianobikwelu@gmail
echo $user->age; // 23

$user->name = 'John Doe'; // Cannot write to 'readonly' property outside of declaration scope 

By using readonly, you ensure that these properties remain unchangeable once the object is created. This is a great way to ensure that your objects are immutable. To illustrate the need for immutable objects, imagine a scenario where you're working with a geographical point. You want the point's coordinates to remain constant once set, ensuring data integrity and predictability in your applications.

Static Properties and Methods

The static keyword in PHP allows properties and methods to be accessed without an instance of the class. WHen I say instance of a class, I simply mean an object created from that class. Static properties and methods are associated with the class itself, not with the objects created from the class. This means that all objects of the same class share the same static properties and methods. They are declared using the static keyword and accessed using the scope resolution operator :: (double colon).

Here's an example of static properties in action:

class User
{
    public static $count = 0;

    public function __construct(public string $name, public string $email)
    {
        self::$count++;
    }
}

$kyrian = new User('Kyrian Obikwelu', 'kyrianobikwelu@gmail.com');
$maria = new User('Maria Alumuko', 'mariaalumuko@gmail.com');

echo User::$count; // 2

In this example, we have a static property $count that keeps track of the number of users created. Static properties are incredibly useful for keeping track of global state. In this case, we're storing the number of users created.

Static properties can also be used to store configuration values that are shared across all objects of the same class. For example, if you have a class that connects to a database, you can store the database credentials in static properties. This way, you can easily access the credentials from anywhere in your code without having to create an instance of the class.

Static Methods

Static methods, like their sibling static properties, belong to the class rather than instances. These methods can be called directly on the class without creating an object.

class Math
{
    public static function add(int $a, int $b): int
    {
        return $a + $b;
    }
    
    public static function subtract(int $a, int $b): int
    {
        return $a - $b;
    }
}

echo Math::add(2, 3); // 5
echo Math::subtract(5, 2); // 3

In this scenario, you can access the add and multiply methods using Math::, bypassing the need to instantiate the class. Static methods are especially handy when you have utility functions that don't rely on object-specific data or state. One advantage of using static methods is that they're memory efficient and slightly faster than regular methods since they don't require an object to be instantiated. Another common use of static methods is to create factory methods that return an instance of the class. We'll explore this in more detail in future tutorials.

The self Keyword

In our previous example, you must have noticed that we used self::$count to access the static property $count. The self keyword is used to access static properties and methods within a class. It's similar to $this, which is used to access non-static properties and methods within a class.

Class Constants

Class constants are similar to static properties in terms of belonging to the class rather than instances. They're declared using the const keyword and accessed using the scope resolution operator ::. The difference between class constants and static properties is that class constants cannot be changed once they're defined. They're immutable and always public, so there's no need to specify the visibility modifier.

Class constants can also be accessed from the instance of a class, but this is not recommended. It's best to access class constants directly from the class itself.

class Math
{
    const float PI = 3.14159265359;
    const float E = 2.71828182846;
    
    public static function exponent(float $x): float
    {
        return self::E ** $x;
    }
    
    public static function circleArea(float $radius): float
    {
        return self::PI * ($radius ** 2);
    }
}

echo Math::PI; // 3.14159265359
echo Math::E; // 2.71828182846

echo Math::exponent(2); // 7.38905609893
echo Math::circleArea(5); // 78.5398163397

// Accessing class constants from an instance of the class (Not recommended)
$math = new Math();
echo $math::PI; // 3.14159265359

Here, we've defined class constants PI and E to represent mathematical constants. These constants belong to the Math class and are accessible without creating an instance.

Let's take a look at another example:

class Article
{
    const STATUS_DRAFT = 'draft';
    const STATUS_PUBLISHED = 'published';
    const STATUS_ARCHIVED = 'archived';
    
    public function __construct(
        public string $title,
        public string $content,
        public string $status = self::STATUS_DRAFT
    ) {}
    
    public function publish(): void
    {
        $this->status = self::STATUS_PUBLISHED;
    }
}

$article = new Article('Hello, World!', 'This is my first article.');
echo $article->status; // draft

$article->publish();
echo $article->status; // published

When to Use Class Constants

Class constants are useful in various scenarios, including:

  • Defining configuration values that are shared across all objects of the same class.
  • Defining mathematical constants.
  • Defining error codes.
  • Defining HTTP status codes.
  • Storing global settings.

Classes as Reference Types

Before I conclude this article, I'd love to discuss a crucial concept often overlooked by many PHP developers. In PHP, variables can be classified based on how they're stored in memory. There are two main types of variables to that regard: value types and reference types.

Value types store the actual value within the variable itself. Examples of value types include integers, floats, booleans, and strings. When you pass a value type variable to a function or method, a copy of the value is made, and the function works with that copy. The original value in the calling scope remains unchanged. Here's an example:

function increment(int $number): void
{
    $number++;
    echo "Inside function: $number\n";
}

$number = 5;
increment($number); // Inside function: 6
echo "Outside function: $number\n"; // Outside function: 5

As you can see, the value of $number remains unchanged outside the function. This is because the function works with a copy of the value, not the original value.

Reference types on the other hand store a reference to where the value is stored in memory (in the heap). When you pass a reference type variable to a function or method, it's not the actual object that's being copied. Instead, a reference to that object in memory is passed to the function. This means that any changes made to the object within the function will be reflected in the calling scope.

Now what does all this have to do with classes? Well, classes are reference types, so are arrays. This means that when you pass an object to a function or method, you're passing a reference to that object, not a copy of the object. Let's see this in action:

class User {
    public string $name;
    public function __construct(string $name) {
        $this->name = $name;
    }
}

function changeName(User $user, string $name): void {
    $user->name = $name;
    echo "Inside function: {$user->name}\n";
}

$user = new User('Kyrian');
changeName($user, 'Maria'); // Inside function: Maria
echo "Outside function: {$user->name}\n"; // Outside function: Maria

Unlike earlier, the original $user object is modified within the function. This is because we're working with a reference to the object, not a copy of the object. This is a crucial concept to understand when working with objects in PHP. If you're not careful, you might end up with unexpected results. Trust me, 😅 I've been there.

Conclusion

Whew! Let's call it a wrap for this one. That was a lot to take in, I suppose. But I hope you enjoyed it. If you've just been reading through without practicing, you're doing yourself a disservice. Please make sure you practice what you've learned so far, try out the code examples in your IDE, and play around with them. That's the best way to learn. Got any questions? Feel free to reach out to me on Twitter. I'd love to hear from you. Also, get ready. In my next tutorial, we'll explore inheritance and polymorphism, two foundational concepts in object-oriented programming (and one of those things they ask you in interviews). So, stay tuned! Until then, keep coding, keep learning, and stay curious! 🐘