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 thePerson
class, meaning theStudent
class inherits all methods and properties fromPerson
. - The
parent::
keyword allows us to call methods from the parent class. In this case, we called the constructor from thePerson
class before adding our own functionality to theStudent
class. And similarly, theprintInformation()
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.
- Public: If an element is marked as
public
, it means you can access it from anywhere, even outside the class. - Protected: An element marked as
protected
is accessible only within the class and all its descendants (classes that inherit from it). - 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:
- Basic types: These include
int
(integers),float
(floating-point numbers),bool
(boolean values),string
(strings),array
(arrays), andnull
. - Classes: When we want a value to be an instance of a specific class.
- Interfaces: Defines a set of methods that a class must implement. A value that meets an interface must have these methods.
- Mixed types: We can specify that a variable can have multiple allowed types.
- 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:
- Value comparison
==
: Checks if the objects are of the same class and have the same values in their properties. - 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.
- 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.
- 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.
- 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.