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 (variable) $age and a constructor that sets this property. The howOldAreYou() method then provides access to the person's age.

The $this pseudo-variable is used inside the class to access the properties and methods of the object.

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.

The nice thing is that the property definition with its initialization via the constructor can be shortened and simplified like this:

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

For completeness, in addition to constructors, objects can have destructors (method __destruct) that are called before the object is released from memory.

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 printInformation()
	{
		echo "Age: {$this->age} years\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

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

	function printInformation()
	{
		parent::printInformation();
		echo "Field of study: {$this->fieldOfStudy} \n";
	}
}

$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. And similarly, the printInformation() ancestor method before listing the student information.

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.

Note that the printInformation() methods in the Person and Student classes output slightly different information. And we can add other classes (such as Employee) that will provide other implementations of this method. The ability of objects of different classes to respond to the same method in different ways is called polymorphism:

$people = [
	new Person(30),
	new Student(20, 'Computer Science'),
	new Employee(45, 'Director'),
];

foreach ($people as $person) {
	$person->printInformation();
}

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:

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Bark';
	}

	public function guardHouse()
	{
		echo 'Dog diligently guards the house';
	}
}

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.

How are interfaces and abstract classes different? Abstract classes can contain both abstract and concrete methods, while interfaces only define what methods the class must implement, but provide no implementation. A class can inherit from only one abstract class, but can implement any number of interfaces.

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.

Some types cannot be written directly in PHP. In this case, they are listed in the phpDoc comment, which is the standard format for documenting PHP code, starting with /** and ending with */. It allows you to add descriptions of classes, methods, and so on. And also to list complex types using so-called annotations @var, @param and @return. These types are then used by static code analysis tools, but are not checked by PHP itself.

class Registry
{
	/** @var array<Person>  indicates that it's an array of Person objects */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

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 gracefully handle errors and unexpected situations in our code. They are objects that carry information about an error or unusual situation.

In PHP, we have a built-in class Exception, which serves as the basis for all exceptions. This has several methods that allow us to get more information about the exception, such as the error message, the file and line where the error occurred, etc.

When an error occurs in the code, we can “throw” the exception using the throw keyword.

function division(float $a, float $b): float
{
	if ($b === 0) {
		throw new Exception('Division by zero!');
	}
	return $a / $b;
}

When the division() function receives null as its second argument, it throws an exception with the error message 'Division by zero!'. To prevent the program from crashing when the exception is thrown, we trap it in the try/catch block:

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
}

Code that can throw an exception is wrapped in a block try. If the exception is thrown, the code execution moves to a block catch, where we can handle the exception (e.g., write an error message).

After the try and catch blocks, we can add an optional block finally, which is always executed whether the exception was thrown or not (even if we use return, break, or continue in the try or catch block):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'Exception caught: '. $e->getMessage();
} finally {
	// Code that is always executed whether the exception has been thrown or not
}

We can also create our own exception classes (hierarchy) that inherit from the Exception class. 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;
	}
}

Multiple catch blocks can be specified for a single try block if you expect different types of exceptions.

$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.

Iterations

In PHP, you can loop through objects using the foreach loop, much like you loop through an array. For this to work, the object must implement a special interface.

The first option is to implement the interface Iterator, which has methods current() returning the current value, key() returning the key, next() moving to the next value, rewind() moving to the beginning, and valid() checking to see if we're at the end yet.

The other option is to implement an interface IteratorAggregate, which has only one method getIterator(). This either returns a placeholder object that will provide the traversal, or it can be a generator, which is a special function that uses yield to return keys and values sequentially:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

	public function getIterator(): Generator
	{
		foreach ($this->people as $person) {
			yield $person;
		}
	}
}

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Age: {$person->age} years\n";
}

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