Introduction to Object-Oriented Programming

The term “OOP” stands for Object-Oriented Programming, which is a way to organize and structure code. OOP allows us to view a program as a collection of objects that communicate with each other, rather than a sequence of commands and functions.

In OOP, an “object” is a unit that contains data and functions that operate on that data. Objects are created based on “classes”, which can be understood as blueprints or templates for objects. Once we have a class, we can create its “instance”, which is a specific object made from that class.

Let's look at how we can create a simple class in PHP. When defining a class, we use the keyword “class”, followed by the class name, and then curly braces that enclose the class's functions (called “methods”) and class variables (called “properties” or “attributes”):

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

In this example, we've created a class named Car with one function (or “method”) called honk.

Each class should solve only one main task. If a class is doing too many things, it may be appropriate to divide it into smaller, specialized classes.

Classes are typically stored in separate files to keep the code organized and easy to navigate. The file name should match the class name, so for the Car class, the file name would be Car.php.

When naming classes, it's good to follow the “PascalCase” convention, meaning each word in the name starts with a capital letter, and there are no underscores or other separators. Methods and properties follow the “camelCase” convention, meaning they start with a lowercase letter.

Some methods in PHP have special roles and are prefixed with __ (two underscores). One of the most important special methods is the “constructor”, labeled as __construct. The constructor is a method that's automatically called when creating a new instance of a class.

We often use the constructor to set the initial state of an object. For example, when creating an object representing a person, you might use the constructor to set their age, name, or other attributes.

Let's see how to use a constructor in PHP:

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Outputs: 25

In this example, the Person class has a property $age and a constructor that sets this property. The howOldAreYou() method then provides access to the person's age.

The new keyword is used to create a new instance of a class. In the example above, we created a new person aged 25.

You can also set default values for constructor parameters if they aren't specified when creating an object. For instance:

class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // if no argument is passed, parentheses can be omitted
echo $person->howOldAreYou(); // Outputs: 20

In this example, if you don't specify an age when creating a Person object, the default value of 20 will be used.

Lastly, property definition with its initialization via the constructor can be shortened and simplified like this:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

Namespaces

Namespaces allow us to organize and group related classes, functions, and constants while avoiding naming conflicts. You can think of them like folders on a computer, where each folder contains files related to a specific project or topic.

Namespaces are especially useful in larger projects or when using third-party libraries where class naming conflicts might arise.

Imagine you have a class named Car in your project, and you want to place it in a namespace called Transport. You would do it like this:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Beep beep!';
	}
}

If you want to use the Car class in another file, you need to specify from which namespace the class originates:

$car = new Transport\Car;

For simplification, you can specify at the beginning of the file which class from a particular namespace you want to use, allowing you to create instances without mentioning the full path:

use Transport\Car;

$car = new Car;

Inheritance

Inheritance is a tool of object-oriented programming that allows the creation of new classes based on existing ones, inheriting their properties and methods, and extending or redefining them as needed. Inheritance ensures code reusability and class hierarchy.

Simply put, if we have one class and want to create another derived from it but with some modifications, we can “inherit” the new class from the original one.

In PHP, inheritance is implemented using the extends keyword.

Our Person class stores age information. We can have another class, Student, which extends Person and adds information about the field of study.

Let's look at an example:

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		echo 'Age of student: ', $this->howOldAreYou();
		echo 'Field of study: ', $this->fieldOfStudy;
	}
}

$student = new Student(20, 'Computer Science');
$student->printInformation();

How does this code work?

  • We used the extends keyword to extend the Person class, meaning the Student class inherits all methods and properties from Person.
  • The parent:: keyword allows us to call methods from the parent class. In this case, we called the constructor from the Person class before adding our own functionality to the Student class.

Inheritance is meant for situations where there's an “is a” relationship between classes. For instance, a Student is a Person. A cat is an animal. It allows us in cases where we expect one object (e.g., “Person”) in the code to use a derived object instead (e.g., “Student”).

It's essential to realize that the primary purpose of inheritance is not to prevent code duplication. On the contrary, misuse of inheritance can lead to complex and hard-to-maintain code. If there's no “is a” relationship between classes, we should consider composition instead of inheritance.

Composition

Composition is a technique where, instead of inheriting properties and methods from another class, we simply use its instance in our class. This allows us to combine functionalities and properties of multiple classes without creating complex inheritance structures.

For example, we have a Engine class and a Car class. Instead of saying “A car is an engine”, we say “A car has an engine”, which is a typical composition relationship.

class Engine
{
	function start()
	{
		echo "Engine is running.";
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo "The car is ready to drive!";
	}
}

$car = new Car;
$car->start();

Here, the Car doesn't have all the properties and methods of the Engine, but it has access to it through the $engine property.

The advantage of composition is greater design flexibility and better adaptability for future changes.

Visibility

In PHP, you can define “visibility” for class properties, methods, and constants. Visibility determines where you can access these elements.

  1. Public: If an element is marked as public, it means you can access it from anywhere, even outside the class.
  2. Protected: An element marked as protected is accessible only within the class and all its descendants (classes that inherit from it).
  3. Private: If an element is private, you can access it only from within the class where it was defined.

If you don't specify visibility, PHP will automatically set it to public.

Let's look at a sample code:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		echo $this->privateProperty;    // Works
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Works
// echo $object->protectedProperty;   // Throws an error
// echo $object->privateProperty;     // Throws an error

Continuing with class inheritance:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Works
		echo $this->protectedProperty;  // Works
		// echo $this->privateProperty;   // Throws an error
	}
}

In this case, the printProperties() method in the ChildClass can access the public and protected properties but cannot access the private properties of the parent class.

Data and methods should be as hidden as possible and only accessible through a defined interface. This allows you to change the internal implementation of the class without affecting the rest of the code.

Final Keyword

In PHP, we can use the final keyword if we want to prevent a class, method, or constant from being inherited or overridden. When a class is marked as final, it cannot be extended. When a method is marked as final, it cannot be overridden in a subclass.

Being aware that a certain class or method will no longer be modified allows us to make changes more easily without worrying about potential conflicts. For example, we can add a new method without fear that a descendant might already have a method with the same name, leading to a collision. Or we can change the parameters of a method, again without the risk of causing inconsistency with an overridden method in a descendant.

final class FinalClass
{
}

// The following code will throw an error because we cannot inherit from a final class.
class ChildOfFinalClass extends FinalClass
{
}

In this example, attempting to inherit from the final class FinalClass will result in an error.

Static Properties and Methods

When we talk about “static” elements of a class in PHP, we mean methods and properties that belong to the class itself, not to a specific instance of the class. This means that you don't have to create an instance of the class to access them. Instead, you call or access them directly through the class name.

Keep in mind that since static elements belong to the class and not its instances, you cannot use the $this pseudo-variable inside static methods.

Using static properties leads to obfuscated code full of pitfalls, so you should never use them, and we won't show an example here. On the other hand, static methods are useful. Here's an example:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// Using the static method without creating an instance of the class
echo Calculator::add(5, 3); // Output: 8
echo Calculator::subtract(5, 3); // Output: 2

In this example, we created a Calculator class with two static methods. We can call these methods directly without creating an instance of the class using the :: operator. Static methods are especially useful for operations that don't depend on the state of a specific class instance.

Class Constants

Within classes, we have the option to define constants. Constants are values that never change during the program's execution. Unlike variables, the value of a constant remains the same.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): int
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Output: 4

In this example, we have a Car class with the NumberOfWheels constant. When accessing the constant inside the class, we can use the self keyword instead of the class name.

Object Interfaces

Object interfaces act as “contracts” for classes. If a class is to implement an object interface, it must contain all the methods that the interface defines. It's a great way to ensure that certain classes adhere to the same “contract” or structure.

In PHP, interfaces are defined using the interface keyword. All methods defined in an interface are public (public). When a class implements an interface, it uses the implements keyword.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Meow';
	}
}

$cat = new Cat;
$cat->makeSound();

If a class implements an interface, but not all expected methods are defined, PHP will throw an error. A class can implement multiple interfaces at once, which is different from inheritance, where a class can only inherit from one class.

Abstract Classes

Abstract classes serve as base templates for other classes, but you cannot create their instances directly. They contain a mix of complete methods and abstract methods that don't have a defined content. Classes that inherit from abstract classes must provide definitions for all the abstract methods from the parent.

We use the abstract keyword to define an abstract class.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'This is a regular method';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'This is the implementation of the abstract method';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

In this example, we have an abstract class with one regular and one abstract method. Then we have a Child class that inherits from AbstractClass and provides an implementation for the abstract method.

Type Checking

In programming, it's crucial to ensure that the data we work with is of the correct type. In PHP, we have tools that provide this assurance. Verifying that data is of the correct type is called “type checking.”

Types we might encounter in PHP:

  1. Basic types: These include int (integers), float (floating-point numbers), bool (boolean values), string (strings), array (arrays), and null.
  2. Classes: When we want a value to be an instance of a specific class.
  3. Interfaces: Defines a set of methods that a class must implement. A value that meets an interface must have these methods.
  4. Mixed types: We can specify that a variable can have multiple allowed types.
  5. Void: This special type indicates that a function or method does not return any value.

Let's see how to modify the code to include types:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}

	public function printAge(): void
	{
		echo "This person is " . $this->age . " years old.";
	}
}

/**
 * A function that accepts a Person object and prints the person's age.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

In this way, we ensure that our code expects and works with data of the correct type, helping us prevent potential errors.

Comparison and Identity

In PHP, you can compare objects in two ways:

  1. Value comparison ==: Checks if the objects are of the same class and have the same values in their properties.
  2. Identity ===: Checks if it's the same instance of the object.
class Car
{
	public string $brand;

	public function __construct(string $brand)
	{
		$this->brand = $brand;
	}
}

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, because they have the same value
var_dump($car1 === $car2);  // false, because they are not the same instance
var_dump($car1 === $car3);  // true, because $car3 is the same instance as $car1

The instanceof Operator

The instanceof operator allows you to determine if a given object is an instance of a specific class, a descendant of that class, or if it implements a certain interface.

Imagine we have a class Person and another class Student, which is a descendant of Person:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}
}

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Computer Science');

// Check if $student is an instance of the Student class
var_dump($student instanceof Student);  // Output: bool(true)

// Check if $student is an instance of the Person class (because Student is a descendant of Person)
var_dump($student instanceof Person);   // Output: bool(true)

From the outputs, it's evident that the $student object is considered an instance of both the Student and Person classes.

Fluent Interfaces

A “Fluent Interface” is a technique in OOP that allows chaining methods together in a single call. This often simplifies and clarifies the code.

The key element of a fluent interface is that each method in the chain returns a reference to the current object. This is achieved by using return $this; at the end of the method. This programming style is often associated with methods called “setters”, which set the values of an object's properties.

Let's see what a fluent interface might look like for sending emails:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Hello, this is a message.')
		  ->send();
}

In this example, the methods setFrom(), setRecipient(), and setMessage() are used to set the corresponding values (sender, recipient, message content). After setting each of these values, the methods return the current object ($email), allowing us to chain another method after it. Finally, we call the send() method, which actually sends the email.

Thanks to fluent interfaces, we can write code that is intuitive and easily readable.

Copying with clone

In PHP, we can create a copy of an object using the clone operator. This way, we get a new instance with identical content.

If we need to modify some of its properties when copying an object, we can define a special __clone() method in the class. This method is automatically called when the object is cloned.

class Sheep
{
	public string $name;

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

	public function __clone()
	{
		$this->name = 'Clone of ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Outputs: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Outputs: Clone of Dolly

In this example, we have a Sheep class with one property $name. When we clone an instance of this class, the __clone() method ensures that the name of the cloned sheep gets the prefix “Clone of”.

Traits

Traits in PHP are a tool that allows sharing methods, properties and constants between classes and prevents code duplication. You can think of them as a “copy and paste” mechanism (Ctrl-C and Ctrl-V), where the content of a trait is “pasted” into classes. This allows you to reuse code without having to create complicated class hierarchies.

Let's take a look at a simple example of how to use traits in PHP:

trait Honking
{
	public function honk()
	{
		echo 'Beep beep!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // Outputs 'Beep beep!'

$truck = new Truck;
$truck->honk(); // Also outputs 'Beep beep!'

In this example, we have a trait named Honking that contains one method honk(). Then we have two classes: Car and Truck, both of which use the Honking trait. As a result, both classes “have” the honk() method, and we can call it on objects of both classes.

Traits allow you to easily and efficiently share code between classes. They do not enter the inheritance hierarchy, i.e., $car instanceof Honking will return false.

Exceptions

Exceptions in OOP allow us to handle and manage errors that may arise during the execution of our code. They are essentially objects designed to record errors or unexpected situations in your program.

In PHP, we have the built-in Exception class for these objects. It has several methods that allow us to get more information about the exception, such as the error message, the file, and the line where the error occurred, etc.

When a problem arises, we can “throw” an exception (using throw). If we want to “catch” and process this exception, we use try and catch blocks.

Let's see how it works:

try {
	throw new Exception('Message explaining the reason for the exception');

	// This code won't execute
	echo 'I am a message that nobody will read';

} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

It's important to note that an exception can be thrown deeper, during the call to other methods.

For one try block, multiple catch blocks can be specified if you expect different types of exceptions.

We can also create an exception hierarchy, where each exception class inherits from the previous one. As an example, consider a simple banking application that allows deposits and withdrawals:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

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

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Not enough funds in the account.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Daily withdrawal limit exceeded.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

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

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'An error occurred during the operation.';
}

In this example, it's important to note the order of the catch blocks. Since all exceptions inherit from BankingException, if we had this block first, all exceptions would be caught in it without the code reaching the subsequent catch blocks. Therefore, it's important to have more specific exceptions (i.e., those that inherit from others) higher in the catch block order than their parent exceptions.

Best Practices

Once you have the basic principles of object-oriented programming under your belt, it's crucial to focus on best practices in OOP. These will help you write code that is not only functional but also readable, understandable, and easily maintainable.

  1. Separation of Concerns: Each class should have a clearly defined responsibility and should address only one primary task. If a class does too many things, it might be appropriate to split it into smaller, specialized classes.
  2. Encapsulation: Data and methods should be as hidden as possible and accessible only through a defined interface. This allows you to change the internal implementation of a class without affecting the rest of the code.
  3. Dependency Injection: Instead of creating dependencies directly within a class, you should “inject” them from the outside. For a deeper understanding of this principle, we recommend the chapters on Dependency Injection.
version: 4.0