Skip to content

5. The classes

Vocabulary: A class (keyword [class]) is a type PHP. A variable of this type is called an object. An object is an instance of a class.

It is in this chapter that the evolution of PHP between versions 7 and 8 is most evident. PHP 8 introduced a series of new features that make writing classes more concise and safer: typed attributes, constructor parameter promotion, read-only attributes ([readonly]), enumerations ([enum]), property hooks and asymmetric visibility (PHP 8.4), cloning with modification (PHP 8.5)... We introduce these concepts through the examples, following the progression of the PHP 7 course: we start with the most basic class and work our way up to a class written according to current best practices.

5.1. The script directory structure

The scripts for this chapter are located in the [scripts-console/classes] folder:

php8-oct-2026/scripts-console/classes
├── json
│   ├── json-01.php
│   ├── json-02.php
│   ├── json-03.php
│   ├── json-04.php
│   ├── Person.php
│   ├── Person2.php
│   ├── Person3.php
│   └── Person4.php
├── classes-01.php
├── classes-02.php
├── ...
├── classes-26.php
├── Gender.php
├── Person.php
├── Student.php
└── Teacher.php

As in previous chapters, a console script is run in a [VS Code] terminal opened in the script’s folder, using the [php script-name.php] command.

5.2. Can any variable become an object with attributes?

In PHP 7, it was possible to write [$obj1->attr1 = "one";] to a nonexistent variable [$obj1]: PHP would then silently (with a simple warning) create a [stdClass]-type object with the [attr1] attribute. This is no longer possible in PHP 8. The [classes-01.php] script is as follows:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// in PHP 8, a non-existent variable no longer magically becomes an object
try {
    $obj0->attr1 = "one";
} catch (\Error $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// a generic object must be created explicitly
$obj1 = new \stdClass();
// a [stdClass] object can receive dynamically created attributes
$obj1->attr1 = "one";
$obj1->attr2 = 100;
// displays the object
print "object1=[$obj1->attr1,$obj1->attr2]\n";
// modifies the object
$obj1->attr2 += 100;
// displays the object
print "object1=[$obj1->attr1,$obj1->attr2]\n";
// copies the value of object1 (address of the pointed object) into object2
// the two variables are then different but point to the same object
$obj2 = $obj1;
// modifies obj2
$obj2->attr2 = 0;
// displays the two objects
print "object1=[$obj1->attr1,$obj1->attr2]\n";
print "object2=[$obj2->attr1,$obj2->attr2]\n";
// changes the object pointed to by obj1
$obj1 = new \stdClass();
print "obj1:\n";
print_r($obj1);
print "obj2:\n";
print_r($obj2);
// assigns the reference (the address) of object2 to object3
// $obj2 and $obj3 are then one and the same variable
$obj3 = &$obj2;
print "obj2:\n";
print_r($obj2);
print "obj3:\n";
print_r($obj3);
// modifies obj3
$obj3->attr2 = 10;
// displays the two objects
print "object2=[$obj2->attr1,$obj2->attr2]\n";
print "object3=[$obj3->attr1,$obj3->attr2]\n";
// changes the object pointed to by obj2
$obj2 = new \stdClass();
$obj2->attr3 = "two";
$obj2->attr4 = 20;
// displays the two objects $obj2 and $obj3
print "obj2:\n";
print_r($obj2);
print "obj3:\n";
print_r($obj3);

// is an object a dictionary?
try {
    print count($obj3) . "\n";
} catch (\TypeError $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// the number of attributes is obtained with [get_object_vars]
print "number of attributes: " . count(get_object_vars($obj3)) . "\n";
// an object can be iterated over with [foreach]
foreach ($obj3 as $attribute => $value) {
    print "obj3[$attribute]=$value\n";
}
// an associative array can be turned into a [stdClass] object and vice versa
$obj4 = (object) ["name" => "Langevin", "age" => 48];
print "obj4=[$obj4->name,$obj4->age]\n";
print_r((array) $obj4);
// end
exit;

Results:

> php classes-01.php
Error: Attempt to assign property "attr1" on null
object1=[one,100]
object1=[one,200]
object1=[one,0]
object2=[one,0]
obj1:
stdClass Object
(
)
obj2:
stdClass Object
(
    [attr1] => one
    [attr2] => 0
)
obj2:
stdClass Object
(
    [attr1] => one
    [attr2] => 0
)
obj3:
stdClass Object
(
    [attr1] => one
    [attr2] => 0
)
object2=[one,10]
object3=[one,10]
obj2:
stdClass Object
(
    [attr3] => two
    [attr4] => 20
)
obj3:
stdClass Object
(
    [attr3] => two
    [attr4] => 20
)
Error: count(): Argument #1 ($value) must be of type Countable|array, stdClass given
number of attributes: 2
obj3[attr3]=two
obj3[attr4]=20
obj4=[Langevin,48]
Array
(
    [name] => Langevin
    [age] => 48
)

Comments

  • Lines 7–9: In PHP 8, the variable [$obj0] does not exist, and the operation [$obj0->attr1 = "one"] throws an error of type [\Error] (line 2 of the results) instead of creating an object. We will examine the [try / catch] structure in the chapter on exceptions. For now, just note that it allows you to catch the error and continue executing the script;
  • line 13: a generic object must be explicitly created using the [new] operator. The predefined class [stdClass] is an empty class;
  • Lines 15–16: The notation [$obj->attr] refers to the attribute [attr] of the object [$obj]. If it does not exist, it is created; this is called a dynamic attribute. The class [stdClass] is designed for this purpose: you can add as many attributes to it as you want;
  • Line 25: The expression [$obj2 = $obj1], when [$obj1] is an object, is a copy of references: [$obj2] and [$obj1] are references (addresses) to the same object. The object itself can be modified by either reference;
  • Lines 27–30: are intended to show that [$obj1] and [$obj2] are two different variables that point to the same object. [$obj2 = $obj1] copied the value of [$obj1] into the variable [$obj2] (operation 1 below). The value of [$obj1] is the address of an object. Thus, [$obj1] and [$obj2] point to the same object. When a variable [$obj] that points to an object is manipulated, PHP manipulates the object being pointed to. As shown in the diagram below, we can modify the referenced object either via [$obj1] or via [$obj2]. This is illustrated in lines 5 and 6 of the results;

Image

  • line 39: the expression [$obj3 = &$obj2] causes [$obj2] and [$obj3] to be at the same address as [1 below]. One could say that the two variables are aliases for the same memory location. They both point to an object, Object A below: [2];
  • Line 50: The operation [$obj2 = new \stdClass()] creates a new object, Object B [3 below], and the address of this new object is assigned to the variable [$obj2] [4]. Since [$obj2] and [$obj3] are two aliases for the same memory location, [$obj3] also points to the new Object B. This is shown in lines 31–42 of the results;

Image

  • Line 61: Is an object a dictionary? In PHP 7, the function [count] accepted an object (with a warning) and returned 1, which made no sense. In PHP 8, it throws an error [TypeError]: [count] accepts only arrays or objects that implement the [Countable] interface (line 43 of the results);
  • line 66: the function [get_object_vars] returns an object’s attributes as an associative array. The number of elements in the array is equal to the number of attributes of the object;
  • Lines 68–69: An object can be iterated over using [foreach], just like a dictionary. The keys are the names of the attributes, and the values are the values of those same attributes. Thus, an object shares similarities with a dictionary but is not one. In PHP 7, this iteration was performed using the [each] function, which has since been removed;
  • lines 72–74: the conversion operator [(object)] converts an associative array into a [stdClass] object, and the operator [(array)] performs the reverse operation.

5.3. A Person class with no declared attributes

In PHP 7, a class could use attributes it had not declared: it was sufficient to write [$p->firstName = "Paul";] to create the attribute [firstName] in the object [$p], just as one does with [stdClass]. These dynamic attributes on objects of an ordinary class have been deprecated since PHP 8.2: each time such an attribute is created, the message “Deprecated: Creation of dynamic property Person::$firstName is deprecated” is displayed. They will be removed in a future version of PHP. The reason is simple: a typo in an attribute name ([$p->firstNam = "Paul"]) would silently create a new attribute instead of reporting the error.

There are three exceptions to this rule:

  • the [stdClass] class, which we just used, still accepts dynamic attributes;
  • a class preceded by the attribute [#[\AllowDynamicProperties]] also accepts them. This attribute was designed to facilitate the migration of legacy code. We will not use it in this course;
  • a class that defines the magic methods [__get] and [__set] decides for itself what to do with an undeclared attribute.

It is this third solution that the script [classes-02.php] presents:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Person
{
    // the attributes are not declared one by one:
    // they are stored in a private array
    private array $attributes = [];

    // called when writing an undeclared attribute: $p->x = value
    public function __set(string $name, mixed $value): void
    {
        $this->attributes[$name] = $value;
    }

    // called when reading an undeclared attribute: $p->x
    public function __get(string $name): mixed
    {
        return $this->attributes[$name] ?? null;
    }

    // called by isset($p->x)
    public function __isset(string $name): bool
    {
        return isset($this->attributes[$name]);
    }

    // method
    public function identity(): string
    {
        // uses attributes that have not been declared
        return "[$this->firstName,$this->lastName,$this->age]";
    }
}

// test
// the attributes are created by the magic method [__set]
$p = new Person();
$p->firstName = "Paul";
$p->lastName = "Langevin";
$p->age = 48;
// calling a method
print "person=" . $p->identity() . "\n";
// isset
print "isset(\$p->age): " . var_export(isset($p->age), true) . "\n";
print "isset(\$p->address): " . var_export(isset($p->address), true) . "\n";
// end
exit;

Results:

1
2
3
4
> php classes-02.php
person=[Paul,Langevin,48]
isset($p->age): true
isset($p->address): false

Comments

  • Lines 6–38: define a class named [Person]. A class is a template used to create objects. It consists of attributes and functions called methods;
  • line 10: The only attribute declared for the class is a private array (we’ll soon see the meaning of the keyword [private]). This array will store the “undeclared” attributes;
  • lines 13–16: The magic method [__set] is automatically called by PHP when writing to an attribute that does not exist (or is not accessible). It receives the attribute’s name and the value to be assigned to it. Here, it stores this value in the array [$attributes]. Methods whose names begin with two underscores are called magic methods: they are not called explicitly; instead, PHP calls them;
  • lines 19–22: The magic method [__get] is called when reading an attribute that does not exist. It returns the stored value or [null]. The operator [??] returns its left operand if it exists and is not [null]; otherwise, it returns its right operand;
  • line 25: the magic method [__isset] is called by the function [isset];
  • Line 31: The method [identity] uses three attributes that are not declared in the class. The keyword [$this] refers to the object to which the method is applied. Each call to [$this->firstName] triggers a call to [__get];
  • Line 40: A [$p] object of type [Person] is created. The keyword [new] is used to create a new object. The operation returns a reference to the created object (i.e., an address). Various notations are possible: [new Person()], [new Person], [new person]. The class name is case-insensitive;
  • lines 41–43: The three attributes required by the [identity] method are created in the [$p] object by the [__set] method;
  • line 45: the [identity] method of the [Person] class is applied to the [$p] object. In the code for the [identity] method, [$this] references the same object as [$p].

This technique has its uses (configuration objects, encapsulation of data read from a file), but it is not the standard way to write a class. In a regular class, you declare its attributes.

5.4. The Person class with declared attributes

The script [classes-03.php] is as follows:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Person
{
    // attributes of the class, public and typed
    public string $firstName;
    public string $lastName;
    public int $age;

    // method
    public function identity(): string
    {
        return "[$this->firstName,$this->lastName,$this->age]";
    }
}

// test
// the attributes are public
$p = new Person();
$p->firstName = "Paul";
$p->lastName = "Langevin";
$p->age = 48;
// calling a method
print "person=" . $p->identity() . "\n";
// a typed attribute only accepts values of its type
try {
    $p->age = "forty-eight";
} catch (\TypeError $e) {
    print "Error 1: " . $e->getMessage() . "\n";
}
// an uninitialized typed attribute cannot be read
$p2 = new Person();
try {
    print "person=" . $p2->identity() . "\n";
} catch (\Error $e) {
    print "Error 2: " . $e->getMessage() . "\n";
}
// end
exit;

Results:

1
2
3
4
> php classes-03.php
person=[Paul,Langevin,48]
Error 1: Cannot assign string to property Person::$age of type int
Error 2: Typed property Person::$firstName must not be accessed before initialization

Comments

  • Lines 9–11: The class attributes are explicitly declared. The keyword [public] means that they are accessible from outside the class. In PHP 7, the course used the keyword [var], inherited from PHP 4, which is a synonym for [public] and is no longer used today;
  • Each attribute is preceded by its type: [string] for first name and last name, [int] for age. These are typed attributes, which were introduced with PHP 7.4. The possible types are those of function parameters: [int], [float], [string], [bool], [array], a class or interface name, a nullable type [?string], a union type [int|float]...;
  • line 30: a typed attribute accepts only values of its type. Any other value causes an error [TypeError] (line 3 of the results);
  • lines 35–37: A typed attribute without a default value is in the “uninitialized” state until a value is assigned to it. Reading it causes an error (line 4 of the results). Untyped attributes, on the other hand, have a default value of [null]. This is one reason to initialize all of an object’s attributes as soon as it is created: that is the role of the constructor.

5.5. The Person class with a constructor

The previous examples showed exotic classes such as those found in PHP 4. It is not recommended to follow these examples. We now present a [Person] [classes-04.php] class that follows standard best practices, as found in all object-oriented languages (Java, C#, etc.):

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Person
{
    // attributes of the class
    private string $firstName;
    private string $lastName;
    private int $age;

    // getters and setters
    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getAge(): int
    {
        return $this->age;
    }

    public function setFirstName(string $firstName): void
    {
        $this->firstName = $firstName;
    }

    public function setLastName(string $lastName): void
    {
        $this->lastName = $lastName;
    }

    public function setAge(int $age): void
    {
        $this->age = $age;
    }

    // constructor
    public function __construct(string $firstName, string $lastName, int $age)
    {
        // we go through the setters
        $this->setFirstName($firstName);
        $this->setLastName($lastName);
        $this->setAge($age);
    }

    // toString method
    public function __toString(): string
    {
        return "[$this->firstName,$this->lastName,$this->age]";
    }
}

// test
// creating a Person object
$p = new Person("Paul", "Langevin", 48);
// identity of this person
print "person=$p\n";
// we change the age
$p->setAge(14);
// identity of the person
print "person=$p\n";
// a class having a [__toString] method implements the [Stringable] interface
print "Stringable: " . var_export($p instanceof \Stringable, true) . "\n";
// a private attribute is not accessible from outside the class
try {
    print $p->lastName;
} catch (\Error $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// end
exit;

Results:

1
2
3
4
5
> php classes-04.php
person=[Paul,Langevin,48]
person=[Paul,Langevin,14]
Stringable: true
Error: Cannot access private property Person::$lastName

Comments

  • Line 4: We specify that the declared types of the function parameters must be strictly adhered to. Without this declaration, PHP would attempt to convert a value of the wrong type (for example, the string ["48"] to an integer [48]);
  • Lines 6–60: the class [Person];
  • Lines 9–11: the class’s private attributes ([private]). These attributes are visible only within the class. Other keywords that can be used are:
    • [public]: makes the attribute a public attribute visible from outside the class;
    • [protected]: makes the attribute a protected attribute visible from within the class and its derived classes;
  • Because the attributes are private, they cannot be accessed from outside the class. Therefore, we cannot write [$p->lastName = "Landru"]: we are outside the class [Person], and the attribute [lastName] is private. Lines 72–74 verify this: attempting to read [$p->lastName] results in an error (line 5 of the results). To initialize the private attributes of the [$p] object, there are two ways:
    • use the public methods [set] and [get] (the names of these methods can be anything) in lines 14–39. In this case, we write: [$p->setLastName("Landru")];
    • use the constructor in lines 45–50. You would then write: [$p = new Person("Michel", "Landru", 44)]. This syntax automatically calls the method of the [Person] class named [__construct];
  • line 64: This line displays the person [$p] as a string. To do this, the method of the [Person] class called [__toString] (line 54) is used;
  • line 70: Starting with PHP 8.0, any class that defines a method named [__toString] automatically implements the predefined interface [Stringable] (we will cover interfaces in the next chapter). The [instanceof] operator checks whether an object is of a given type. A function can thus declare a parameter of type [string|Stringable]: it then accepts a string or any object that can be converted to a string;
  • all methods of the class have been prefixed with the keyword [public], which indicates that the method is visible outside the class. The other keywords that can be used are, as with attributes and with the same meaning, [private] and [protected]. Without an explicit visibility keyword, the method is implicitly [public]. The PSR-12 style guidelines, which the scripts in this course follow, require that visibility always be specified explicitly.

5.6. Promotion of Constructor Parameters

The previous class is verbose: each attribute is named four times (declaration, constructor parameter, assignment in the constructor, getter). Since PHP 8.0, constructor parameter promotion allows you to declare and initialize an attribute directly in the list of constructor parameters. The script [classes-22.php] demonstrates this:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Person
{
    // constructor with parameter promotion:
    // each parameter preceded by a visibility becomes an attribute of the class
    public function __construct(
        private string $firstName,
        private string $lastName,
        private int $age = 0,
    ) {
        // nothing else to do: the attributes are already initialized
    }

    // getters
    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getAge(): int
    {
        return $this->age;
    }

    // setter
    public function setAge(int $age): void
    {
        $this->age = $age;
    }

    // toString method
    public function __toString(): string
    {
        return "[$this->firstName,$this->lastName,$this->age]";
    }
}

// test
// classic call: the arguments are passed in the order of the parameters
$p1 = new Person("Paul", "Langevin", 48);
print "p1=$p1\n";
// named arguments: the order no longer matters
$p2 = new Person(lastName: "Lefur", age: 70, firstName: "Sylvie");
print "p2=$p2\n";
// the [age] parameter has a default value: it can be omitted
$p3 = new Person(firstName: "Steve", lastName: "Boer");
print "p3=$p3\n";
// the promoted attributes are real attributes
var_dump($p3);
// end
exit;

Results:

> php classes-22.php
p1=[Paul,Langevin,48]
p2=[Sylvie,Lefur,70]
p3=[Steve,Boer,0]
object(Person)#3 (3) {
  ["firstName":"Person":private]=>
  string(5) "Steve"
  ["lastName":"Person":private]=>
  string(4) "Boer"
  ["age":"Person":private]=>
  int(0)
}

Comments

  • Lines 10–14: Each constructor parameter preceded by a visibility keyword ([private], [protected], [public]) is “promoted”: PHP declares an attribute with the same name and type and assigns it the value of the parameter. These three lines replace the attribute declarations and assignments from the previous class;
  • line 13: A promoted parameter can have a default value. The comma after the last parameter has been allowed since PHP 8.0; it makes it easier to add a parameter later;
  • line 15: The body of the constructor can contain code (such as validations). Here, there is nothing to do;
  • line 52: Named arguments (PHP 8.0) allow arguments to be passed in any order by referring to them by the parameter name. This makes the code more readable, especially when a function has many parameters;
  • line 55: the parameter [age] is omitted; it takes its default value;
  • line 58: the function [var_dump] shows that the promoted attributes are true private attributes of the class (lines 5–12 of the results).

5.7. The Person Class with Validity Checks in the Constructor

A class’s constructor is the appropriate place to verify that the object’s initialization values are correct. However, an object can also be initialized using its [set] methods or equivalents. To avoid duplicating the same checks in two different places, these checks can be placed in the [set] methods. If an object’s initialization value turns out to be incorrect, an exception will be thrown. Here is an example.

First, move the definition of the [Person] class to its own file, [Person.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// namespace
namespace Examples;

// class Person
class Person
{
    // attributes of the class
    private string $firstName;
    private string $lastName;
    private int $age;

    // getters and setters
    public function getFirstName(): string
    {
        return $this->firstName;
    }

    public function getLastName(): string
    {
        return $this->lastName;
    }

    public function getAge(): int
    {
        return $this->age;
    }

    public function setFirstName(string $firstName): void
    {
        // the first name must not be empty
        $firstName = trim($firstName);
        if ($firstName === "") {
            throw new \Exception("The first name must not be empty");
        }
        $this->firstName = $firstName;
    }

    public function setLastName(string $lastName): void
    {
        // the last name must not be empty
        $lastName = trim($lastName);
        if ($lastName === "") {
            throw new \Exception("The last name must not be empty");
        }
        $this->lastName = $lastName;
    }

    public function setAge(int $age): void
    {
        // the age must be valid
        if ($age < 0) {
            throw new \Exception("The age must be a positive or zero integer");
        }
        $this->age = $age;
    }

    // constructor
    public function __construct(string $firstName, string $lastName, int $age)
    {
        // we go through the setters
        $this->setFirstName($firstName);
        $this->setLastName($lastName);
        $this->setAge($age);
    }

    // method acting as a second constructor
    public function initWithPerson(Person $p): void
    {
        // initializes the current object with a person $p
        $this->__construct($p->firstName, $p->lastName, $p->age);
    }

    // static factory method (named constructor)
    public static function fromPerson(Person $p): self
    {
        // creates a new person, a copy of the person $p
        return new self($p->firstName, $p->lastName, $p->age);
    }

    // toString method
    public function __toString(): string
    {
        return "[$this->firstName,$this->lastName,$this->age]";
    }
}

Comments

  • Line 4: Specifies that the types of function parameters must be respected as declared;
  • Line 7: Defines a namespace. The full (or qualified) name of the class [Person] is therefore [\Examples\Person]. Note the character [\] at the beginning of the qualified name: this results in an absolute qualified name. If this character is absent, the name is a relative qualified name (relative to the current namespace). Thus, if two classes, A and B, belong to the same namespace E, in the code for class A, class B can be accessed using the relative notation B. If class A is part of the namespace E1 and class B is part of the namespace E2, then in class A’s code, class B can be accessed using the absolute notation [\E2\B]. Defining a class within a namespace is not mandatory but is considered best practice: two libraries can then each define a class named [Person] without conflict;
  • namespaces often correspond to the file directory structure. Thus, class A in the namespace E1 is often located in a file named [E1/A.php]. This is the PSR-4 convention used by the [Composer] tool to automatically load classes, which we will use in later chapters. The [PHP Intelephense] extension of [VS Code] does not require this. Do not confuse the file tree with a namespace: the fully qualified name of a class uses a namespace and has nothing to do with the file tree of the PHP file containing the class. Here, the class [\Examples\Person] is in the file [scripts-console/classes/Person.php];
  • lines 13–15: the class’s three private attributes;
  • lines 33–41: initialization of the [firstName] attribute and verification of the initialization value;
  • line 36: the function [trim($string)] removes the spaces at the beginning and end of [$string]. Thus, [trim(" abcd ")] is the string ["abcd"], and [trim(" ")] is the empty string.;
  • line 37: if the first name is empty, then an exception is thrown (line 38); otherwise, the first name is stored (line 40). To throw an exception, the predefined class [Exception] is used here. Here, we are required to use its absolute name, [\Exception]. If we use its relative name, [Exception], then this class will be searched for in the current namespace, i.e., in the namespace [Examples] of the class [Person]. Thus, the PHP interpreter will search for a class with the absolute name [\Examples\Exception], which does not exist;
  • lines 63–69: the constructor uses the methods [set] to initialize the attributes: the validations are written only once;
  • The methods on lines 72 and 79 are described in the following paragraph.

The class [Person] is used by the following script, [classes-05.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// inclusion of the definition of the Person class
require_once __DIR__ . "/Person.php";
// for demonstration
print __DIR__ . "/Person.php\n";

// qualified name of the Person class
use Examples\Person;

// test
// creating a Person object
$p = new Person("Paul", "Langevin", 48);
// identity of this person
print "Example1, person=$p\n";
// creating an invalid Person object
try {
    $p = new Person("xx", "yy", "zz");
} catch (\Exception $e1) {
    print "Example2, error: " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
    print "Example2, error: " . $e2->getMessage() . "\n";
}
// creating an invalid Person object
try {
    $p = new Person("", "yy", 10);
} catch (\Exception $e1) {
    print "Example3, error: " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
    print "Example3, error: " . $e2->getMessage() . "\n";
}

// end
exit;

Results:

1
2
3
4
5
> php classes-05.php
.../php8-oct-2026/scripts-console/classes/Person.php
Example1, person=[Paul,Langevin,48]
Example2, error: Examples\Person::__construct(): Argument #3 ($age) must be of type int, string given, called in .../php8-oct-2026/scripts-console/classes/classes-05.php on line 21
Example3, error: The first name must not be empty

Comments

  • Line 7: The script will use the [Person] class. You must therefore tell the PHP interpreter where to find the definition of this class. This is the purpose of the [include file] and [require file] statements. The difference between the two statements is as follows: if the [include file] statement encounters errors while loading [file], a [E_WARNING]-level error is issued, but execution continues, whereas [require], in the same situation, generates a fatal error and stops the script from running. Each of these two instructions has a variant: [include_once] and [require_once]. These two variants allow for handling cases where the same file is included multiple times. Consider, for example, a project consisting of several PHP scripts, some of which reference the [Person] class. Executing these scripts would then cause the file [Person.php] to be included multiple times, resulting in an error because a class cannot be defined twice. The solution is to use the [_once] variants, which ensure that the file is included only once in the project’s global script;
  • The constant [__DIR__] is a PHP constant that refers to the full name of the folder containing the script with the constant [__DIR__]. Line 9 displays it (line 2 of the results). On the reader’s machine, the result will be something like [C:\laragon\www\php8-oct-2026\scripts-console\classes/Person.php]. In a file path, you can use either [/] or [\] interchangeably on Windows. Using [__DIR__] makes the script independent of the current folder at the time of execution;
  • Line 12: The script [classes-05.php] has no namespace. When the code uses the relative name [Person], it is searched for in the root namespace [\] and will therefore not be found. There are two solutions to this problem:
    • use the full class name [\Examples\Person];
    • use the statement [use], which specifies that, in the code that follows, the name [Person] refers to the class [\Examples\Person]. In a [use] statement, the name is always absolute: the initial [\] is unnecessary (this is the recommended notation). The statement is equivalent to [use Examples\Person as Person;], which specifies that the alias [Person] refers to the class [\Examples\Person];
  • the instruction [use] tells the interpreter that the class [Person] is actually the class [\Examples\Person]. That said, where will the interpreter find the code for this class? The instruction [require_once] tells it. The two instructions have very different roles: [use] works on names, [require_once] loads a file;
  • line 16: a [Person] object is created. The [__construct] method of the [Person] class will be implicitly executed here;
  • line 18: displays the person [$p]. To be displayed, the value of the variable [$p] must be converted to a string. Implicitly, the method [Person::__toString] is then executed. This method must therefore return a string;
  • we have seen that the constructor of the [Person] class can throw a [\Exception] exception. This exception must therefore be handled. Consequently, the creation of a person should always be enclosed within a ***[try / catch] structure, as shown in lines 20–34. Here, we intentionally trigger an error by passing an age that is not an integer. In this specific case, the error is thrown by the PHP interpreter and not by the code in the [Person] class. This is because the signature of the [Person::__construct]* method is [__construct(string $firstName, string $lastName, int $age)]. Therefore, the [age] parameter passed to the constructor must be of type integer. If this is not the case, the PHP interpreter throws an error of type [TypeError]. Furthermore, the methods [set] of the class [Person]* throw an exception of type [\Exception]. Since the constructor that calls them does not have a [try / catch] structure, the exception is propagated up one level to the code that called the constructor, i.e. the code in the [classes-05.php] script. Finally, the script [classes-05.php]* can receive two types of errors: [\Exception] or [\TypeError]. For this reason, the [try] structure has two [catch] clauses to handle the two types of errors separately. When the developer is certain that certain errors cannot occur, they will not use the corresponding [catch]** clauses. Here, they are used systematically solely for demonstration purposes;
  • Line 22: You can write either [Exception] or [\Exception]. The first version uses the relative class name, relative to the script’s namespace. Since the script has no namespace, its namespace is the root of the namespaces: [\]. Therefore, writing [Exception] here is equivalent to writing [\Exception]. It is preferable to use the absolute names of the predefined PHP classes, in a script that does not have its own namespace. Thus, if you decide to assign a namespace to this script, using the absolute class names remains valid, whereas in the other case, changing the namespace will cause errors with the relative class names;
  • Line 23: When an exception occurs, the [Exception::getMessage] method retrieves the error message for the exception. The same applies to an error of type [TypeError]. In the [Person::setFirstName] method, [throw new \Exception("The first name must not be empty")] was written. This is the message retrieved by the [getMessage] method in Example 3 (line 5 of the results);
  • line 4 of the results: the error message for [TypeError]. Starting with PHP 8.0, it identifies the problematic parameter by its number and name: “Argument #3 ($age) must be of type int, string given.” The error messages in PHP 8 are more specific than those in PHP 7.

5.8. Adding a method that acts as a second constructor

In PHP, it is not possible to have multiple constructors with different parameters that would allow an object to be constructed in various ways. Therefore, methods can be used to act as constructors. The [Person] class has two such methods:

    public function initWithPerson(Person $p): void
    {
        // initializes the current object with a person $p
        $this->__construct($p->firstName, $p->lastName, $p->age);
    }

    // static factory method (named constructor)
    public static function fromPerson(Person $p): self
    {
        // creates a new person, a copy of the person $p
        return new self($p->firstName, $p->lastName, $p->age);
    }

Comments

  • Lines 72–76: The [initWithPerson] method allows the values of the attributes of another [Person] object to be assigned to the current object. Here, it calls the constructor [__construct], but this is not required. It could initialize the attributes of the class [Person] on its own. Note that a method of the [Person] class has access to the private attributes of all [Person] objects, not just those of [$this];
  • lines 79–83: The [fromPerson] method is another way of doing this, which is more common today. It is a static method (keyword [static]): it does not apply to an object but to the class. It is called using the notation [Person::fromPerson($p)]. It creates a new object using [new self(...)]: the keyword [self] refers to the class in which the code is written, in this case [Person]. The return type [self] indicates that the method returns an object of this class. Such a method is called a named constructor or factory method. We’ll come back to static methods a little later.

The new class [Person] is used by the following script, [classes-06.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// inclusion of the definition of the Person class
require_once __DIR__ . "/Person.php";
// declaration of the qualified name of the Person class
use Examples\Person;

// test
// creating a Person object
try {
    $p = new Person("Paul", "Langevin", 48);
} catch (\Exception $e) {
    print "error: " . $e->getMessage() . "\n";
    exit;
}
// identity of this person
print "person=$p\n";
// creating a second person
try {
    $p2 = new Person("Laure", "Adeline", 67);
} catch (\Exception $e) {
    print "error: " . $e->getMessage() . "\n";
    exit;
}
// initializing the first one with the values of the second one
try {
    $p->initWithPerson($p2);
} catch (\Exception $e) {
    print "error: " . $e->getMessage() . "\n";
    exit;
}
// check
print "person=$p\n";

// creating a third person with the named constructor
$p3 = Person::fromPerson($p2);
print "person=$p3\n";
// $p3 and $p2 are two different objects having the same values
print "same object: " . var_export($p3 === $p2, true) . "\n";
print "same values: " . var_export($p3 == $p2, true) . "\n";
// end
exit;

Results:

1
2
3
4
5
6
> php classes-06.php
person=[Paul,Langevin,48]
person=[Laure,Adeline,67]
person=[Laure,Adeline,67]
same object: false
same values: true

Comments

  • Line 17, line 26, line 33: It is common that, after an exception, you must stop the execution of a console script if the error encountered is unrecoverable. This is not the case in a web script: you do not stop the script’s execution but instead display an error page. If you are inside a function, the [exit] statement will not be used; instead, [return] will be used: the script’s execution is not halted ([exit]), but the function is exited ([return]) after an error has been set;
  • line 39: call to the named constructor;
  • line 42: the [===] operator applied to two objects evaluates to true if they are the same object (same address). [$p3] and [$p2] are two distinct objects (line 5 of the results);
  • line 43: the operator [==], when applied to two objects of the same class, evaluates to true if their attributes have the same values (line 6 of the results). The function [var_export] returns a PHP representation of its parameter, in this case [true] or [false].

5.9. Validity checks using property hooks (PHP 8.4)

The previous class, [Person], protects its attributes by making them private and requiring external code to use the [get] and [set] methods. PHP 8.4 offers another solution: property hooks. A public attribute can be accompanied by a [set] hook—the code executed each time the attribute is written—and a [get] hook—the code executed each time the attribute is read. This preserves the simplicity of writing [$p->age = 49] while maintaining control over the values. This is demonstrated by the script [classes-23.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Person
{
    // public attribute with a [set] hook: validation is done on every write
    public string $firstName {
        set(string $value) {
            $value = trim($value);
            if ($value === "") {
                throw new \InvalidArgumentException("The first name must not be empty");
            }
            $this->firstName = $value;
        }
    }

    public string $lastName {
        set(string $value) {
            $value = trim($value);
            if ($value === "") {
                throw new \InvalidArgumentException("The last name must not be empty");
            }
            $this->lastName = $value;
        }
    }

    // without an explicit parameter, the received value is called [$value]
    public int $age {
        set {
            if ($value < 0) {
                throw new \InvalidArgumentException("The age must be a positive or zero integer");
            }
            $this->age = $value;
        }
    }

    // virtual attribute: it only has a [get] hook and takes no space in the object
    public string $identity {
        get => "[$this->firstName,$this->lastName,$this->age]";
    }

    // constructor: the assignments go through the [set] hooks
    public function __construct(string $firstName, string $lastName, int $age)
    {
        $this->firstName = $firstName;
        $this->lastName = $lastName;
        $this->age = $age;
    }
}

// test
$p = new Person(" Paul ", "Langevin", 48);
print "person=$p->identity\n";
// the attributes are public: they are read and written directly
$p->age = 49;
print "age=$p->age\n";
// but writing always goes through the [set] hook
foreach (["firstName" => "  ", "age" => -4] as $attribute => $value) {
    try {
        $p->$attribute = $value;
    } catch (\InvalidArgumentException $e) {
        print "Error: " . $e->getMessage() . "\n";
    }
}
// a virtual attribute without a [set] hook cannot be written
try {
    $p->identity = "?";
} catch (\Error $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// the object has remained valid
print "person=$p->identity\n";
var_dump($p);

Results:

> php classes-23.php
person=[Paul,Langevin,48]
age=49
Error: The first name must not be empty
Error: The age must be a positive or zero integer
Error: Property Person::$identity is read-only
person=[Paul,Langevin,49]
object(Person)#1 (3) {
  ["firstName"]=>
  string(4) "Paul"
  ["lastName"]=>
  string(8) "Langevin"
  ["age"]=>
  int(49)
}

Comments

  • lines 9–17: The public attribute [firstName] is followed by a block enclosed in curly braces that contains its hooks. Here, there is only one hook, [set];
  • line 10: The hook [set] receives the value to be assigned to the attribute. It validates it using [trim], checks it, and throws an exception if it is incorrect;
  • line 15: Inside its hooks, [$this->firstName] specifies the memory location of the attribute: the value is stored there. Everywhere else, [$this->firstName] goes through the hooks;
  • line 13: an exception of type [\InvalidArgumentException] is thrown; this is a predefined class derived from [\Exception] and more specific than it, signaling an invalid argument. We will cover the predefined PHP exceptions in the chapter on exceptions;
  • line 31: when the hook [set] does not declare a parameter, the received value is available in the implicit variable [$value], and its type is that of the attribute;
  • Lines 40–42: An attribute that has only one hook, [get], and never references its own memory location is a virtual attribute: it is calculated on each read and occupies no space in the object. The notation [get => expression] is a shorthand for a hook [get] that evaluates to the value of the expression;
  • lines 45–50: the constructor assigns the attributes; these assignments are performed via the hooks [set]. Validation is therefore performed during construction as well as during any subsequent modification, without using the methods [set];
  • line 54: the first name [" Paul "] is cleared by the hook [set];
  • line 57: the attribute [age] is modified directly, but via its hook [set];
  • Lines 60–66: An attempt is made to assign invalid values. The notation [$p->$attribute] uses the contents of the variable [$attribute] as the attribute name. The [set] hooks throw their exceptions (lines 4–5 of the results);
  • line 69: the virtual attribute [identity] does not have a hook [set]: it is read-only (line 6 of the results);
  • line 75: the object retained valid values. [var_dump] shows only three attributes: the virtual attribute [identity] does not exist in memory.

Property hooks have long existed in other languages (C#, Kotlin, Swift). They eliminate the need to write getters and setters “just in case”: you start with a simple public attribute and add a hook when validation becomes necessary, without changing the code that uses the class.

5.10. Asymmetric Visibility (PHP 8.4)

It’s often the case that a property needs to be readable by everyone but modifiable only by the class itself. In the past, we would have written a private property and a public getter. PHP 8.4 allows you to set different visibility levels for reading and writing a property: this is called asymmetric visibility. The script [classes-24.php] demonstrates this using a bank account:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class BankAccount
{
    // asymmetric visibility: public read, private write
    public private(set) float $balance = 0.0;

    // parameter promotion also accepts asymmetric visibility
    public function __construct(public private(set) string $holder)
    {
    }

    // only the methods of the class can modify the balance
    public function deposit(float $amount): void
    {
        if ($amount <= 0) {
            throw new \InvalidArgumentException("The amount must be positive");
        }
        $this->balance += $amount;
    }

    public function withdraw(float $amount): void
    {
        if ($amount <= 0 || $amount > $this->balance) {
            throw new \InvalidArgumentException("Withdrawal of $amount impossible");
        }
        $this->balance -= $amount;
    }
}

// test
$account = new BankAccount("Paul Langevin");
$account->deposit(100);
$account->withdraw(30);
// direct reading of the attributes, without a getter
print "{$account->holder}: {$account->balance} euros\n";
// writing from outside the class is forbidden
try {
    $account->balance = 1_000_000;
} catch (\Error $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// the class keeps control of the validity of the balance
try {
    $account->withdraw(500);
} catch (\InvalidArgumentException $e) {
    print "Error: " . $e->getMessage() . "\n";
}
print "{$account->holder}: {$account->balance} euros\n";

Results:

1
2
3
4
5
> php classes-24.php
Paul Langevin: 70 euros
Error: Cannot modify private(set) property BankAccount::$balance from global scope
Error: Withdrawal of 500 impossible
Paul Langevin: 70 euros

Comments

  • Line 9: The attribute [balance] is public for reading ([public]) and private for writing ([private(set)]). External code can read [$account->balance], but only the class’s methods can modify it. You can also write [protected(set)]: in this case, derived classes can modify the attribute;
  • line 12: asymmetric visibility is also possible for a promoted parameter;
  • lines 17–25: The methods [deposit] and [withdraw] are the only ways to modify the balance. They verify the validity of the operations;
  • line 39: attributes are read directly, without getters. The notation [{$expression}] inside a string enclosed in quotation marks allows for the insertion of an expression more complex than a simple variable;
  • line 42: Writing to the balance from outside the class causes an error (line 3 of the results). The character [_] can be used as a thousands separator in a literal number (PHP 7.4);
  • Line 48: The withdrawal is rejected by the [withdraw] method (line 4 of the results). The balance has not changed (line 5).

5.11. Read-Only Attributes and Classes ([readonly])

An object whose state cannot change after it is created is called immutable. Immutable objects greatly simplify programming: they can be shared among multiple parts of the code without fear that any of those parts will modify them. PHP 8.1 introduced read-only attributes (keyword [readonly]): such an attribute can be initialized only once, from within the class. PHP 8.2 introduced read-only classes: all their attributes are then [readonly]. The script [classes-25.php] presents an immutable class [Person]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// read-only class: all its attributes are [readonly]
final readonly class Person
{
    public function __construct(
        public string $firstName,
        public string $lastName,
        public int $age,
    ) {
        if ($age < 0) {
            throw new \InvalidArgumentException("The age must be a positive or zero integer");
        }
    }

    // "modifying" an immutable object = creating a modified copy
    public function withAge(int $age): static
    {
        // clone with modification of attributes (PHP 8.5)
        return clone($this, ["age" => $age]);
    }

    public function __toString(): string
    {
        return "[$this->firstName,$this->lastName,$this->age]";
    }
}

// test
$p1 = new Person("Paul", "Langevin", 48);
print "p1=$p1\n";
// a [readonly] attribute is initialized once and for all
try {
    $p1->age = 49;
} catch (\Error $e) {
    print "Error 1: " . $e->getMessage() . "\n";
}
// nor can attributes be added to a [readonly] class
try {
    $p1->address = "Paris";
} catch (\Error $e) {
    print "Error 2: " . $e->getMessage() . "\n";
}
// a new person, copy of the first one with another age
$p2 = $p1->withAge(49);
print "p1=$p1\n";
print "p2=$p2\n";

Results:

1
2
3
4
5
6
> php classes-25.php
p1=[Paul,Langevin,48]
Error 1: Cannot modify readonly property Person::$age
Error 2: Cannot create dynamic property Person::$address
p1=[Paul,Langevin,48]
p2=[Paul,Langevin,49]

Comments

  • line 7: the class is declared as [readonly]: its three promoted attributes are read-only. The keyword [final], which we will examine shortly, prevents the class from being subclassed;
  • Lines 10–12: The attributes are public: they can be read directly, without getters. They cannot be modified: a getter would serve no purpose;
  • line 14: The constructor checks the validity of the age. An immutable object must be valid upon construction since it will never change;
  • lines 20–24: “Modifying” an immutable object involves creating a modified copy of that object. This is what the [withAge] method does. Its return type, [static] (PHP 8.0), means “an object of the class on which the method was called.” The difference from [self] only becomes apparent with derived classes: if a derived class inherits the method, the type [static] refers to the derived class;
  • Line 23: The [clone] operator creates a copy of the [$this] object. Starting with PHP 8.5, [clone] can be used as a function and accept, as its second parameter, an array of attributes to be modified in the copy (clone with). Prior to PHP 8.5, you had to use the "magic" method [__clone] or rebuild the object with [new];
  • line 37: Modifying a [readonly] attribute causes an error (line 3 of the results);
  • Line 43: A class [readonly] also does not accept dynamic attributes: this is an error, not a simple deprecation warning (line 4 of the results);
  • line 48: [$p2] is a new person; [$p1] has not changed (lines 5–6 of the results).

In the following chapters, we will often use [readonly] classes to represent data (tax data from the government, the results of a tax calculation, etc.).

5.12. An array of [Person] objects

The following example, [classes-07.php], shows that arrays of objects are possible:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

require_once __DIR__ . "/Person.php";
use Examples\Person;

// test
// creating an array of Person objects
// to make the code easier to understand, the possible exception is not handled
$group = [new Person("Paul", "Langevin", 48), new Person("Sylvie", "Lefur", 70)];

// identity of these persons
for ($i = 0; $i < count($group); $i++) {
    print "group[$i]=$group[$i]\n";
}

// end
exit;

Results:

1
2
3
> php classes-07.php
group[0]=[Paul,Langevin,48]
group[1]=[Sylvie,Lefur,70]

Comments

  • line 12: creation of an array of 2 people;
  • line 15: iterating through the array;
  • Line 16: [$group[$i]] is an object of type [Person]. The [Person::__toString] method is used to display it.

5.13. Creating a class derived from the Person class

Create the following class, [Teacher], in a file named [Teacher.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// namespace
namespace Examples;

// a class derived from Person
class Teacher extends Person
{
    // attribute: subject taught
    private string $subject;

    // getter and setter
    public function getSubject(): string
    {
        return $this->subject;
    }

    public function setSubject(string $subject): void
    {
        $this->subject = $subject;
    }

    // constructor
    public function __construct(string $firstName, string $lastName, int $age, string $subject)
    {
        // parent attributes
        parent::__construct($firstName, $lastName, $age);
        // other attributes
        $this->setSubject($subject);
    }

    // redefinition of the __toString method of the parent class
    #[\Override]
    public function __toString(): string
    {
        return "[" . parent::__toString() . ",$this->subject]";
    }
}

Comments

  • line 7: The class [Teacher] is also part of the [Examples] namespace;
  • Line 10: The class [Teacher] is derived ([extends]) from the class [Person]. The derived class [Teacher] inherits the attributes and methods of its parent class;
  • Line 13: The class [Teacher] adds a new attribute, [subject], that is unique to it;
  • line 27: the constructor of the [Teacher] class takes 4 parameters:
    • 3 to initialize its parent class (first name, last name, age), line 30;
    • 1 for its own initialization (subject), line 32;
  • line 30: The derived class has access to the methods and constructors of its parent class via the keyword [parent::]. Here, the parameters (first name, last name, age) are passed to the parent class’s constructor;
  • line 36: [#[\Override]] is a PHP attribute (PHP 8.3). An attribute, written as [#[] and []], is information associated with a class, a method, a parameter, etc., that PHP or other tools can use. This attribute declares that the following method overrides a method of the parent class. If this is not the case (e.g., a typo in the method name or a method that has been removed from the parent class), PHP reports a fatal error. We’ll see an example of this a little later;
  • lines 37–40: The [__toString] method of the derived class uses the [__toString] method of the parent class.

The class [Teacher] is used by the following script, [classes-08.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// inclusion of the definition of the two classes
require_once __DIR__ . "/Person.php";
require_once __DIR__ . "/Teacher.php";
// declaration of the two classes used
use Examples\Person;
use Examples\Teacher;

// test
// creating an array of Person objects and derived objects
// for the simplicity of the example, exceptions are not handled
$group = [new Teacher("Paul", "Langevin", 48, "English"), new Person("Sylvie", "Lefur", 70)];

// identity of these persons
for ($i = 0; $i < count($group); $i++) {
    print "group[$i]=$group[$i]\n";
}
// end
exit;

Results:

1
2
3
> php classes-08.php
group[0]=[[Paul,Langevin,48],English]
group[1]=[Sylvie,Lefur,70]

Comments

  • Lines 7–8: We need to tell the PHP interpreter where the two [Teacher, Person] classes are located. The order is important: the parent class [Person] must be defined before the class [Teacher] is defined;
  • lines 10–11: declaration of the full names of the two classes. This will allow us to refer to them in the code simply by their names, without the namespace prefix;
  • line 16: we create an array containing an object of type [Teacher] and an object of type [Person];
  • lines 19–21: display the elements of the array;
  • line 20: The [__toString] method of each [$group[$i]] element will be called. The [Person] class has a [__toString] method. The [Teacher] class has two: the one from its parent class and its own. One might wonder which one will be called. The execution shows that the one from the [Teacher] class was called. This is always the case: when a method is called on an object, it is searched for in the following order: in the object’s class, in its parent class if it has one, then in the parent class of the parent class, and so on. The search stops as soon as the method is found.

5.14. Creating a Second Class Derived from the Person Class

The following example creates a class named [Student] derived from the class [Person], in the file [Student.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// namespace
namespace Examples;

// a second class derived from Person
class Student extends Person
{
    // attribute: course of study
    private string $course;

    // getter and setter
    public function getCourse(): string
    {
        return $this->course;
    }

    public function setCourse(string $course): void
    {
        $this->course = $course;
    }

    // constructor
    public function __construct(string $firstName, string $lastName, int $age, string $course)
    {
        // parent attributes
        parent::__construct($firstName, $lastName, $age);
        // other attributes
        $this->setCourse($course);
    }

    // redefinition of the __toString method of the parent class
    #[\Override]
    public function __toString(): string
    {
        return "[" . parent::__toString() . ",$this->course]";
    }
}

This class is used by the following script, [classes-09.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// inclusion and declaration of the classes used by the script
require_once __DIR__ . "/Person.php";
require_once __DIR__ . "/Teacher.php";
require_once __DIR__ . "/Student.php";
use Examples\{Person, Teacher, Student};

// test
// creating an array of Person objects and derived objects
// to make the example easier to understand, exceptions are not handled
$group = [
    new Teacher("Paul", "Langevin", 48, "English"),
    new Person("Sylvie", "Lefur", 70),
    new Student("Steve", "Boer", 23, "iup2 quality"),
];

// identity of these persons
foreach ($group as $i => $person) {
    print "group[$i]=$person (" . $person::class . ")\n";
}
// end
exit;

Results:

1
2
3
4
> php classes-09.php
group[0]=[[Paul,Langevin,48],English] (Examples\Teacher)
group[1]=[Sylvie,Lefur,70] (Examples\Person)
group[2]=[[Steve,Boer,23],iup2 quality] (Examples\Student)

Comments

  • Line 10: A single [use] statement can declare multiple classes from the same namespace by enclosing them in curly braces;
  • Line 22: The array is iterated over using [foreach], which returns both the index [$i] and the element [$person];
  • line 23: the notation [$object::class] (PHP 8.0) returns the full name of the object’s class. It replaces the [get_class] function, which has been deprecated when used without parameters since PHP 8.3.

5.15. Relationship Between the Constructor of a Derived Class and That of the Parent Class

In some object-oriented languages, the constructor of a derived class automatically calls that of its parent class. The following code [classes-16.php] shows that with PHP this is not the case:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Class1
{
    // constructor
    public function __construct()
    {
        print "constructor of the Class1 class\n";
    }
}

class Class2 extends Class1
{
    // constructor
    public function __construct()
    {
        // the constructor of the parent class is not called implicitly
        print "constructor of the Class2 class\n";
    }
}

class Class3 extends Class1
{
    // constructor
    public function __construct()
    {
        // explicit call of the constructor of the parent class
        parent::__construct();
        // code specific to Class3
        print "constructor of the Class3 class\n";
    }
}

class Class4 extends Class1
{
    // no constructor: the one of the parent class is used
}

// tests
print "test1---------\n";
new Class2();
print "test2---------\n";
new Class3();
print "test3---------\n";
new Class4();

Results:

1
2
3
4
5
6
7
8
> php classes-16.php
test1---------
constructor of the Class2 class
test2---------
constructor of the Class1 class
constructor of the Class3 class
test3---------
constructor of the Class1 class

Comments

  • lines 15–23: The constructor of [Class2] does not call that of its parent class; the parent class’s constructor is not executed (line 3 of the results);
  • line 31: the constructor of [Class3] explicitly calls that of its parent class (lines 5–6 of the results);
  • lines 37–40: The class [Class4] has no constructor. It therefore inherits the constructor from its parent class, which is executed (line 8 of the results).

5.16. Overriding a Method from the Parent Class

We have already seen that a method of the parent class can be overridden in a child class. Thus, the method [__toString] of the class [Person] has been overridden in the child classes [Teacher] and [Student]. The script [classes-13.php] illustrates this concept once again:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// main class
class Class1
{
    public function f(): int
    {
        return 1;
    }

    public function g(): int
    {
        return 2;
    }
}

// derived class
class Class2 extends Class1
{
    // we redefine the function f of the parent class
    #[\Override]
    public function f(): int
    {
        return parent::f() + 10;
    }
}

// code
$c2 = new Class2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1 = new Class1();
print $c1->f() . "\n";

Results:

1
2
3
4
> php classes-13.php
11
2
1

Comments

  • lines 7–18: The class [Class1] defines two methods, f and g;
  • lines 21–29: The class [Class2] extends the class [Class1] and redefines its method f. The attribute [#[\Override]] documents this redefinition;
  • line 32: creates a [$c2] object of type [Class2];
  • line 33: calls the f method of the [$c2] object. Since this method exists in the [Class2] class, it is executed. It, in turn, calls the f method of the parent class using the notation [parent::f()];
  • line 34: calls the g method of the [$c2] object. Since this method does not exist in the [Class2] class, it is looked up in the parent class, where it is found and executed;
  • line 35: creates a [$c1] object of type [Class1];
  • line 36: calls the f method of the [$c1] object. It is the f method of the [Class1] class that is executed.

Now let’s look at the purpose of the [#[\Override]] attribute. In the [classes-10.php] script, the developer intended to redefine the f method but used the wrong name:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// main class
class Class1
{
    public function f(): int
    {
        return 1;
    }
}

// derived class
class Class2 extends Class1
{
    // we think we are redefining the method f of the parent class
    // but we got the name wrong: the [Override] attribute detects it
    #[\Override]
    public function F2(): int
    {
        return parent::f() + 10;
    }
}

// code
$c2 = new Class2();
print $c2->f() . "\n";

Results:

1
2
3
4
5
> php classes-10.php

Fatal error: Class2::F2() has #[\Override] attribute, but no matching parent method exists in .../php8-oct-2026/scripts-console/classes/classes-10.php on line 21
Stack trace:
#0 {main}

Without the [#[\Override]] attribute, the [F2] method would simply be a new method of [Class2], and the script would display [1] without flagging any anomalies: the error would go unnoticed. With the attribute, PHP rejects the class as soon as it is loaded (line 3 of the results). In the rest of this course, we will consistently use [#[\Override]] on redefined methods.

5.17. Passing an object as a function parameter

Let's consider the following [classes-14.php] script:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// main class
class Class1
{
    public function f(): int
    {
        return 1;
    }

    public function g(): int
    {
        return 2;
    }
}

// derived class
class Class2 extends Class1
{
    // we redefine the function f of the parent class
    #[\Override]
    public function f(): int
    {
        return parent::f() + 10;
    }
}

// the parameter of the function is of type Class1 or derived
function doSomething(Class1 $c1): void
{
    print $c1->f() + $c1->g() . "\n";
}

// code
// we create an object of type Class2 derived from Class1
$c2 = new Class2();
// we call doSomething with an object of type Class2
doSomething($c2);
// an object of another type is rejected
try {
    doSomething(new \stdClass());
} catch (\TypeError $e) {
    print "Error: " . $e->getMessage() . "\n";
}

Results:

1
2
3
> php classes-14.php
13
Error: doSomething(): Argument #1 ($c1) must be of type Class1, stdClass given, called in .../php8-oct-2026/scripts-console/classes/classes-14.php on line 44

Comments

  • lines 7–18: the class [Class1];
  • lines 21–29: a class [Class2] derived from [Class1];
  • line 32: a function that expects a parameter of type [Class1]. When the expected type is a class, the actual parameter can be an object of the expected type or a derived type;
  • line 41: the function [doSomething] is called with a parameter of type [Class2], even though the expected type is [Class1]. This is accepted because [Class2] is derived from [Class1] (line 2 of the results);
  • line 44: an object of type [stdClass] is not of type [Class1]: the call causes an error of type [TypeError] (line 3 of the results).

5.18. Abstract Classes

An abstract class is an incomplete class that cannot be instantiated. It must be derived from in order to be usable.

What is the purpose of an abstract class? Sometimes we have classes that share one or more methods but differ in other methods or attributes. In such cases, it is desirable to group everything they have in common into a parent class. For now, we don’t need an abstract class. But suppose the child classes differ only in one method, M: the method’s signature would be the same in all child classes, but its implementation would differ. To require the child classes to implement method M:

  • we will declare the signature of method M in the parent class. Since the parent class does not know how to implement it, we prefix the method with the keyword [abstract]: this means that the implementation of method M is deferred to the child classes;
  • because the parent class is not fully implemented, it is also declared abstract using the same keyword [abstract]. This means the class can no longer be instantiated. A child class must be created to define the implementation of method M so that the body of the parent class can be used.

Here is an example [classes-15.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// abstract main class
abstract class Class1
{
    // method known to all the derived classes
    public function f(): int
    {
        return 1;
    }

    // abstract method g - will be defined by the derived classes
    abstract public function g(): int;
}

// derived class
class Class2 extends Class1
{
    // the method g of the parent class must be defined
    #[\Override]
    public function g(): int
    {
        return parent::f() + 10;
    }
}

// derived class
class Class3 extends Class1
{
    // the method g of the parent class must be defined
    #[\Override]
    public function g(): int
    {
        return parent::f() + 20;
    }

    // we can redefine the method f of the parent class
    #[\Override]
    public function f(): int
    {
        return 2;
    }
}

// code
$c2 = new Class2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Class3();
print $c3->f() . "\n";
print $c3->g() . "\n";
// an abstract class cannot be instantiated
try {
    new Class1();
} catch (\Error $e) {
    print "Error: " . $e->getMessage() . "\n";
}

Results:

1
2
3
4
5
6
> php classes-15.php
1
11
2
21
Error: Cannot instantiate abstract class Class1

Comments

  • lines 7–17: The class [Class1] is abstract because it does not implement the g method on line 16. It must therefore be derived from another class to be usable;
  • lines 20–28: The class [Class2] extends the class [Class1] and defines the g method of its parent class. The attribute [#[\Override]] also applies to the implementation of an abstract method;
  • lines 31–46: The class [Class3] extends the class [Class1] and defines the method g of its parent class;
  • lines 40–44: The class [Class3] also redefines the method f of its parent class;
  • lines 49–54: two objects of types [Class2] and [Class3] are created, and their methods f and g are called;
  • line 57: An abstract class cannot be instantiated; an error occurs (line 6 of the results).

5.19. Final Classes

A final class is a class that cannot be derived from. Consider the following script, [classes-11.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// namespace
namespace Examples;

// class that cannot be derived
final class Class1
{
}

// derived class - causes an error
class Class2 extends Class1
{
}

// code
new Class2();

Results:

1
2
3
4
5
> php classes-11.php

Fatal error: Class Examples\Class2 cannot extend final class Examples\Class1 in .../php8-oct-2026/scripts-console/classes/classes-11.php on line 15
Stack trace:
#0 {main}

Comments

  • line 10: the keyword [final] makes the class [Class1] a final class that cannot be derived from;
  • line 15: the class [Class2] extends the final class [Class1], which is an error;
  • The error is reported as soon as the PHP interpreter loads the definition of the [Class2] class: this is a fatal error that cannot be intercepted by a [try / catch]. The script is not executed.

Declaring a class as [final] is a best practice when it was not designed to be derived from; this prevents other developers from relying on the class's internal details.

5.20. Final Methods

A final method is a method that cannot be overridden by a subclass. Here is an example, [classes-12.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// namespace
namespace Examples;

// main class
class Class1
{
    // this method cannot be redefined in a derived class
    final public function f(): int
    {
        return 1;
    }
}

// derived class
class Class2 extends Class1
{
    // forbidden redefinition - causes an error
    public function f(): int
    {
        return 2;
    }
}

// code
new Class2();

Results:

1
2
3
4
5
> php classes-12.php

Fatal error: Cannot override final method Examples\Class1::f() in .../php8-oct-2026/scripts-console/classes/classes-12.php on line 23
Stack trace:
#0 {main}

Comments

  • line 13: The method f of the class [Class1] is declared final using the keyword ***[final]***;
  • line 20: the class [Class2] extends the class [Class1];
  • line 23: The method f of the parent class [Class1] is redefined. This causes a fatal error, which is reported as soon as the class [Class2] is loaded.

5.21. Static Methods and Attributes

A static method is a method associated with the class in which it is defined, rather than with the class’s instance objects. Thus, if class C declares a static method M, to use it from outside the class, one would write:

  • [C::M] if you are outside the class;
  • [self::M] if you are inside the class.

Here is an example: [classes-17.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Class1
{
    // static method
    public static function say(string $message): void
    {
        print "$message\n";
    }

    // static method using another static method of the class
    public static function sayTwice(string $message): void
    {
        self::say($message);
        self::say($message);
    }
}

// test -------------------
Class1::say("hello");
Class1::sayTwice("good morning");
// first-class callable syntax: [Class1::say(...)] is a function
$speak = Class1::say(...);
$speak("hi");
// that can be passed to another function
array_map($speak, ["one", "two"]);

Results:

1
2
3
4
5
6
7
> php classes-17.php
hello
good morning
good morning
hi
one
two

Comments

  • line 9: the method [say] is declared static using the keyword [static];
  • line 17: Inside the class, the static method [say] is called using the notation [self::say];
  • line 23: the static method [say] is called using the notation [Class1::say];
  • line 26: first-class callable syntax (PHP 8.1): writing [(...)] in place of the arguments of a call does not make the call but returns the function itself, in the form of a [Closure] object. The variable [$speak] then contains a function that is called just like an ordinary function (line 27). The same syntax applies to functions ([strlen(...)]) and to object methods ([$p->identity(...)]);
  • line 29: such a function can be passed as an argument to another function. The function [array_map] applies the function [$speak] to each element of the array (lines 6–7 of the results).

Now let’s consider the following code, [classes-18.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Class1
{
    // static attribute
    private static int $nbObjects = 0;

    public function __construct()
    {
        print "Class1 constructor\n";
        self::$nbObjects++;
    }

    // static method
    public static function say(): void
    {
        print self::$nbObjects . " objects of type [Class1] have been built\n";
    }
}

// test -------------------
new Class1();
new Class1();
Class1::say();

Results:

1
2
3
4
> php classes-18.php
Class1 constructor
Class1 constructor
2 objects of type [Class1] have been built

Comments

  • Line 9: A static attribute is declared to count the number of instances of the [Class1] class that have been created. This is not an attribute that can belong to an instance of the class. In fact, if two objects, O1 and O2, are created, neither is aware of the other. Having a counter in the instance doesn’t make sense: when a new object is created, in which instance would the counter be incremented? A static attribute is a class attribute, not an instance attribute of the class;
  • lines 11–15: the constructor is where we count the created objects, since the creation of each new object triggers the execution of the constructor;
  • line 14: Note the notation [self::$nbObjects], which indicates that we are referring to a static attribute of the class in which the executed code resides;
  • lines 18–21: The static method [say] is responsible for displaying the number of objects created;
  • Lines 25–27: Two objects are created, and the object counter is displayed.

5.22. Visibility Between Parent and Child Classes

Let’s examine the following script, [classes-19.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class SomeParent
{
    // attribute
    private int $attributeOfParent = 4;

    // method
    public function doTest(): void
    {
        // who is calling?
        print "parent:\n";
        var_dump($this);
        // parent display
        print "parent: attributeOfParent={$this->attributeOfParent}\n";
        print "parent: attributeOfChild={$this->attributeOfChild}\n";
    }
}

class SomeChild extends SomeParent
{
    // attribute
    private int $attributeOfChild = 14;

    // method
    #[\Override]
    public function doTest(): void
    {
        // child display
        print "child: attributeOfParent={$this->attributeOfParent}\n";
        print "child: attributeOfChild={$this->attributeOfChild}\n";
        // parent
        parent::doTest();
    }
}

// main script
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();

Comments

  • Lines 6–21: the class [SomeParent];
  • Lines 23–38: the child class [SomeChild]. We can see that it extends the class [SomeParent];
  • line 9: the class [SomeParent] has only one attribute;
  • Lines 12–20: The [SomeParent::doTest] method is designed to display two attributes:
    • [$attributeOfParent], which belongs to the class [SomeParent];
    • [$attributeOfChild], which belongs to the class [SomeChild];
  • Lines 15–16: The caller’s identity is displayed; the method will be called in two different ways:
    • from the parent class [SomeParent];
    • from the child class [SomeChild];
  • lines 18–19: display the two attributes;
  • line 26: the class [SomeChild] has only one attribute;
  • line 30: the [SomeChild::doTest] method is designed to display two attributes:
    • [$attributeOfParent], which belongs to the class [SomeParent];
    • [$attributeOfChild], which belongs to the class [SomeChild];
  • Lines 33–34: display of the two attributes;
  • line 36: calls the [doTest] method of the parent class, which in turn displays both attributes;
  • line 42: the method [SomeParent::doTest] is called;
  • line 44: the [SomeChild::doTest] method is called.

In the first test, the visibility of both attributes is [private]. We can therefore expect that the child class will not see the attribute of its parent class. That attribute would need to have at least the visibility [protected]. But what about the child class’s attribute? Is it visible in the parent class?

Here are the results of this first test:

> php classes-19.php
---------------------------test1
parent:
object(SomeParent)#1 (1) {
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
}
parent: attributeOfParent=4

Warning: Undefined property: SomeParent::$attributeOfChild in .../php8-oct-2026/scripts-console/classes/classes-19.php on line 19
parent: attributeOfChild=
---------------------------test2

Warning: Undefined property: SomeChild::$attributeOfParent in .../php8-oct-2026/scripts-console/classes/classes-19.php on line 33
child: attributeOfParent=
child: attributeOfChild=14
parent:
object(SomeChild)#1 (2) {
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
}
parent: attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in .../php8-oct-2026/scripts-console/classes/classes-19.php:19
Stack trace:
#0 .../php8-oct-2026/scripts-console/classes/classes-19.php(36): SomeParent->doTest()
#1 .../php8-oct-2026/scripts-console/classes/classes-19.php(44): SomeChild->doTest()
#2 {main}
  thrown in .../php8-oct-2026/scripts-console/classes/classes-19.php on line 19

Comments

  • lines 2–11: results of the first test where the [SomeParent::doTest] method is called;
  • lines 3–7: we can see that the object calling the method is of type [SomeParent];
  • line 8: the [$attributeOfParent] attribute is displayed;
  • lines 10–11: We can see that the attribute [SomeParent::$attributeOfChild] does not exist. Therefore, it is not displayed. In PHP 8, reading a nonexistent attribute triggers a [Warning]-level warning (it was a simple [Notice] in PHP 7);
  • lines 12–31: results of the second test, in which the [SomeChild::doTest] method is called;
  • lines 14–15: We can see that the attribute [SomeChild::$attributeOfParent] does not exist. Therefore, it is not displayed. This is normal: the attribute [SomeParent::$attributeOfParent] is [private] and is therefore not recognized in the child class;
  • line 16: the attribute [$attributeOfChild] is displayed;
  • Lines 17–31: We are in the [SomeParent::doTest] method called by the child class;
  • lines 18–23: we see that [$this] is of type [SomeChild] with two private attributes;
  • line 24: Surprisingly, [$this]—which is of type [SomeChild]—can see the private attribute of its parent, [$attributeOfParent];
  • Lines 26–31: Equally surprising, [$this], which is of type [SomeChild], cannot access its attribute [$attributeOfChild]: attempting to access it causes a fatal error.

This result is very surprising: although lines 18–23 indicate that [$this] is of type [SomeChild], this [$this] inside the [SomeParent::doTest] method behaves as if it were an instance of the [SomeParent] class rather than the [SomeChild] class. In fact, visibility is checked relative to the class in which the code is written (here, [SomeParent]) and not relative to the object’s class.

Let’s run a new test with the script [classes-20.php]. The attribute [$attributeOfParent] now has visibility [protected]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class SomeParent
{
    // attribute
    protected int $attributeOfParent = 4;

    // method
    public function doTest(): void
    {
        // who is calling?
        print "parent:\n";
        var_dump($this);
        // parent display
        print "parent: attributeOfParent={$this->attributeOfParent}\n";
        print "parent: attributeOfChild={$this->attributeOfChild}\n";
    }
}

class SomeChild extends SomeParent
{
    // attribute
    private int $attributeOfChild = 14;

    // method
    #[\Override]
    public function doTest(): void
    {
        // child display
        print "child: attributeOfParent={$this->attributeOfParent}\n";
        print "child: attributeOfChild={$this->attributeOfChild}\n";
        // parent
        parent::doTest();
    }
}

// main script
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();

Results:

> php classes-20.php
---------------------------test1
parent:
object(SomeParent)#1 (1) {
  ["attributeOfParent":protected]=>
  int(4)
}
parent: attributeOfParent=4

Warning: Undefined property: SomeParent::$attributeOfChild in .../php8-oct-2026/scripts-console/classes/classes-20.php on line 19
parent: attributeOfChild=
---------------------------test2
child: attributeOfParent=4
child: attributeOfChild=14
parent:
object(SomeChild)#1 (2) {
  ["attributeOfParent":protected]=>
  int(4)
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
}
parent: attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in .../php8-oct-2026/scripts-console/classes/classes-20.php:19
Stack trace:
#0 .../php8-oct-2026/scripts-console/classes/classes-20.php(36): SomeParent->doTest()
#1 .../php8-oct-2026/scripts-console/classes/classes-20.php(44): SomeChild->doTest()
#2 {main}
  thrown in .../php8-oct-2026/scripts-console/classes/classes-20.php on line 19

Comments

  • Line 13: The class [SomeChild] can now see the attribute of its parent, [$attributeOfParent]. This is normal since the parent now has a scope of [protected];
  • In the [SomeParent::doTest] method, the [$this] object is of type [SomeChild] (lines 16–21). It can see the attribute of its parent, [$attributeOfParent] (line 22), but still cannot see its own private attribute, [$attributeOfChild] (lines 24–29).

In the third attempt, [classes-21.php], the attribute [$attributeOfChild] also has a scope of [protected]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class SomeParent
{
    // attribute
    protected int $attributeOfParent = 4;

    // method
    public function doTest(): void
    {
        // who is calling?
        print "parent:\n";
        var_dump($this);
        // parent display
        print "parent: attributeOfParent={$this->attributeOfParent}\n";
        print "parent: attributeOfChild={$this->attributeOfChild}\n";
    }
}

class SomeChild extends SomeParent
{
    // attribute
    protected int $attributeOfChild = 14;

    // method
    #[\Override]
    public function doTest(): void
    {
        // child display
        print "child: attributeOfParent={$this->attributeOfParent}\n";
        print "child: attributeOfChild={$this->attributeOfChild}\n";
        // parent
        parent::doTest();
    }
}

// main script
print "---------------------------test1\n";
(new SomeParent())->doTest();
print "---------------------------test2\n";
(new SomeChild())->doTest();

The results of the execution are as follows:

> php classes-21.php
---------------------------test1
parent:
object(SomeParent)#1 (1) {
  ["attributeOfParent":protected]=>
  int(4)
}
parent: attributeOfParent=4

Warning: Undefined property: SomeParent::$attributeOfChild in .../php8-oct-2026/scripts-console/classes/classes-21.php on line 19
parent: attributeOfChild=
---------------------------test2
child: attributeOfParent=4
child: attributeOfChild=14
parent:
object(SomeChild)#1 (2) {
  ["attributeOfParent":protected]=>
  int(4)
  ["attributeOfChild":protected]=>
  int(14)
}
parent: attributeOfParent=4
parent: attributeOfChild=14
  • Line 23: This time, inside the parent, [$this], which is of type [SomeChild] (lines 16–21), sees the protected attribute [$attributeOfChild] of its own class, [SomeChild].

What can we learn from these tests?

  • That the [$this] instance of a parent class, used in a method of the parent class, can see:
    • the attributes and methods of the parent class, regardless of their visibility;
    • does not see any of the attributes or methods of its child classes;
  • this is the expected behavior;
  • that the [$this] instance of a child class, used in a method of the child class, sees:
    • the attributes and methods of the parent class if they have at least [protected] visibility. Those with [private] visibility are not visible;
    • the attributes and methods of the child class, regardless of their visibility;
  • this is the expected behavior;
  • the [$this] instance of a child class, used in a method of the parent class, sees:
    • the attributes and methods of the parent class, regardless of their visibility;
    • the attributes and methods of its own class only if they have at least visibility [protected]. Those with visibility [private] are not visible;
  • This is unexpected behavior, which can be explained by the rule stated above: visibility depends on the class in which the code is written.

5.23. Enumerations (PHP 8.1)

A person has a gender: female or male. How can this information be represented? In PHP 7, a string (["F"], ["M"]) or class constants were used. At that time, there was nothing to prevent passing an incorrect value (["X"], ["female"]...). PHP 8.1 introduced enumerations (keyword [enum]): an enumeration is a type that has only a finite number of possible values, called cases. We define the enumeration [Gender] in the file [Gender.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// namespace
namespace Examples;

// an enumeration backed by character strings (backed enum)
enum Gender: string
{
    case Female = "F";
    case Male = "M";

    // an enumeration can have methods
    public function label(): string
    {
        return match ($this) {
            Gender::Female => "Madam",
            Gender::Male => "Sir",
        };
    }
}

Comments

  • Line 10: the enumeration [Gender]. The type [string] following its name makes it a “backed” enumeration: each case is associated with a scalar value, in this case a string. This value is useful for storing the case in a file or database. Without this type, it is a pure enumeration whose cases have no associated values;
  • lines 12–13: the two cases of the enumeration and their values;
  • lines 16–22: An enumeration can have methods (as well as constants and static methods). Within a method, ***[$this]*** refers to the case on which the method is called;
  • lines 18–21: the expression [match] (PHP 8.0) compares its parameter to the values of each branch using the operator [===] and returns the value of the first matching branch. If no branch matches, a [UnhandledMatchError] error is thrown. Here, all branches are defined.

The enumeration is used by the script [classes-26.php]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

require_once __DIR__ . "/Gender.php";
use Examples\Gender;

// a person with a gender
final readonly class Person
{
    public function __construct(
        public string $firstName,
        public string $lastName,
        public Gender $gender,
    ) {
    }

    public function __toString(): string
    {
        return "{$this->gender->label()} $this->firstName $this->lastName";
    }
}

// test
$p1 = new Person("Paul", "Langevin", Gender::Male);
$p2 = new Person("Sylvie", "Lefur", Gender::Female);
print "$p1\n$p2\n";
// an enumeration case has a name and a value
print "name={$p2->gender->name}, value={$p2->gender->value}\n";
// the cases are unique objects: they are compared with ===
print "p1 is a man: " . var_export($p1->gender === Gender::Male, true) . "\n";
// the list of the cases
foreach (Gender::cases() as $gender) {
    print "$gender->name ($gender->value): {$gender->label()}\n";
}
// from a value (read from a file, a database...) to a case
print "from('F'): " . Gender::from("F")->name . "\n";
print "tryFrom('X'): " . var_export(Gender::tryFrom("X"), true) . "\n";
try {
    Gender::from("X");
} catch (\ValueError $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// the [Gender] type only accepts the cases of the enumeration
try {
    new Person("Steve", "Boer", "M");
} catch (\TypeError $e) {
    print "Error: " . $e->getMessage() . "\n";
}

Results:

> php classes-26.php
Sir Paul Langevin
Madam Sylvie Lefur
name=Female, value=F
p1 is a man: true
Female (F): Madam
Male (M): Sir
from('F'): Female
tryFrom('X'): NULL
Error: "X" is not a valid backing value for enum Examples\Gender
Error: Person::__construct(): Argument #3 ($gender) must be of type Examples\Gender, string given, called in .../php8-oct-2026/scripts-console/classes/classes-26.php on line 47

Comments

  • line 15: the attribute [gender] of class [Person] is of type [Gender];
  • line 21: call to the [label] method of the instance stored in the [gender] attribute;
  • line 26: a case is denoted by the notation [Gender::Male];
  • line 30: each case has an attribute [name], its name, and for a backed enumeration, an attribute [value], its value (line 4 of the results);
  • line 32: each case is a unique object: cases are compared using the operator [===];
  • line 34: the static method [cases] returns the list of cases in the enumeration (lines 6–7 of the results);
  • line 38: the static method [from] returns the case associated with a value. This is the reverse process of [value]: ["F"] is read from a database, and the case [Gender::Female] is obtained;
  • line 39: the method [tryFrom] returns [null] if the value does not match any case;
  • line 41: the method [from], on the other hand, throws an error [ValueError] (line 10 of the results);
  • line 47: the type [Gender] accepts only the enum cases, not their values: ["M"] is rejected (line 11 of the results). This is the whole point of enumerations: an incorrect value is detected immediately.

5.24. JSON Encoding of a Class

In a class, the [__toString] method is often present: it is supposed to return a string representing the object that calls it. It may be tempting to make this string a JSON string. We’ll explore this approach now. The scripts are in the [scripts-console/classes/json] folder:

1
2
3
4
5
6
7
8
9
php8-oct-2026/scripts-console/classes/json
├── json-01.php
├── json-02.php
├── json-03.php
├── json-04.php
├── Person.php
├── Person2.php
├── Person3.php
└── Person4.php

We will use the following [Person] class:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

class Person
{
    // attributes
    private ?string $lastName = null;
    private ?string $firstName = null;
    private ?int $age = null;
    private ?array $children = null;

    // global setter
    public function setFromArray(array $arrayOfAttributes): static
    {
        // initialization of some attributes of the class
        foreach ($arrayOfAttributes as $attribute => $value) {
            // attributes not declared in the class are rejected
            if (!property_exists($this, $attribute)) {
                throw new \InvalidArgumentException("Unknown attribute [$attribute]");
            }
            $this->$attribute = $value;
        }
        // we return the object
        return $this;
    }

    // getters
    public function getLastName(): ?string
    {
        return $this->lastName;
    }

    public function getFirstName(): ?string
    {
        return $this->firstName;
    }

    public function getAge(): ?int
    {
        return $this->age;
    }

    public function getChildren(): ?array
    {
        return $this->children;
    }


    // __toString
    public function __toString(): string
    {
        // we identify the object
        var_dump($this);
        // we retrieve its attributes
        $attributes = \get_object_vars($this);
        var_dump($attributes);
        // we return the jSON string of the attributes
        return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
    }
}

Comments

  • lines 9–12: the four attributes of the class. They are nullable types ([?string] means either [string] or [null]) and are initially set to [null];
  • lines 30–45: the getters that allow you to retrieve the values of these attributes;
  • lines 15–27: a global setter that initializes the attributes from an associative array ***[$arrayOfAttributes],*** whose keys match the names of the class’s attributes;
  • line 20: the function [property_exists] verifies that the attribute is declared in the class. Without this verification, an unknown key would create a dynamic attribute, which has been deprecated since PHP 8.2;
  • Line 23: The notation [$this->$attribute] refers to the attribute whose name is the value of the variable [$attribute];
  • line 15: The method returns the current object [$this]. Its return type is [static]: “an object of the class on which the method was called.” This allows for chained calls: [(new Person())->setFromArray([...])];
  • Lines 52–61: the [__toString] method of the class;
  • line 57: the PHP function [get_object_vars] retrieves the values of the object’s attributes in the form of an associative array [['lastName'=>'lastName1', 'firstName'=>'firstName1', 'age'=>age1, 'children'=>[...]]]. When called from within the class, it also returns the private attributes;
  • line 60: the JSON string from this array of attributes is returned. The option [JSON_UNESCAPED_UNICODE] specifies that accented characters should not be converted to sequences [\u00e9].

Let’s examine the script [json-01.php], which uses the class [Person]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// Person class
require_once __DIR__ . "/Person.php";

// instantiation of the father
$father = new Person();
// initialization of the father
$father->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Dieudonné",
    "age" => 58,
]);
// instantiation and initialization of child1
$child1 = (new Person())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Sylvain",
    "age" => 17,
]);
// instantiation and initialization of child2
$child2 = (new Person())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Géraldine",
    "age" => 12,
]);
// initialization of the father's children
$father->setFromArray([
    "children" => [$child1, $child2],
]);

// display of the father's elements
$child1 = $father->getChildren()[0];
$child2 = $father->getChildren()[1];
print "------------------------child1\n";
print "child1=$child1\n";
print "------------------------child2\n";
print "child2=$child2\n";
print "------------------------father\n";
print "father=$father\n";

Comments

  • lines 10–16: an object of type [Person] [$father] is initialized using the [Person::setFromArray] method;
  • lines 18–22: an object [Person] [$child1] is initialized in the same way;
  • lines 24–28: initialize a [Person] [$child2] object;
  • line 31: the parent’s [children] attribute is initialized with an array containing the two children;
  • lines 35–36: retrieve the parent’s two children;
  • line 38: the [print] operation attempts to convert the [$child1] object into a string. To do this, it uses the [__toString] method of the object. We therefore expect to see the object’s JSON string;
  • line 42: we do the same with the parent.

The results are as follows:

> php json-01.php
------------------------child1
object(Person)#2 (4) {
  ["lastName":"Person":private]=>
  string(11) "Bertholomé"
  ["firstName":"Person":private]=>
  string(7) "Sylvain"
  ["age":"Person":private]=>
  int(17)
  ["children":"Person":private]=>
  NULL
}
array(4) {
  ["lastName"]=>
  string(11) "Bertholomé"
  ["firstName"]=>
  string(7) "Sylvain"
  ["age"]=>
  int(17)
  ["children"]=>
  NULL
}
child1={"lastName":"Bertholomé","firstName":"Sylvain","age":17,"children":null}
------------------------child2
object(Person)#3 (4) {
  ["lastName":"Person":private]=>
  string(11) "Bertholomé"
  ["firstName":"Person":private]=>
  string(10) "Géraldine"
  ["age":"Person":private]=>
  int(12)
  ["children":"Person":private]=>
  NULL
}
array(4) {
  ["lastName"]=>
  string(11) "Bertholomé"
  ["firstName"]=>
  string(10) "Géraldine"
  ["age"]=>
  int(12)
  ["children"]=>
  NULL
}
child2={"lastName":"Bertholomé","firstName":"Géraldine","age":12,"children":null}
------------------------father
object(Person)#1 (4) {
  ["lastName":"Person":private]=>
  string(11) "Bertholomé"
  ["firstName":"Person":private]=>
  string(10) "Dieudonné"
  ["age":"Person":private]=>
  int(58)
  ["children":"Person":private]=>
  array(2) {
    [0]=>
    object(Person)#2 (4) {
      ["lastName":"Person":private]=>
      string(11) "Bertholomé"
      ["firstName":"Person":private]=>
      string(7) "Sylvain"
      ["age":"Person":private]=>
      int(17)
      ["children":"Person":private]=>
      NULL
    }
    [1]=>
    object(Person)#3 (4) {
      ["lastName":"Person":private]=>
      string(11) "Bertholomé"
      ["firstName":"Person":private]=>
      string(10) "Géraldine"
      ["age":"Person":private]=>
      int(12)
      ["children":"Person":private]=>
      NULL
    }
  }
}
array(4) {
  ["lastName"]=>
  string(11) "Bertholomé"
  ["firstName"]=>
  string(10) "Dieudonné"
  ["age"]=>
  int(58)
  ["children"]=>
  array(2) {
    [0]=>
    object(Person)#2 (4) {
      ["lastName":"Person":private]=>
      string(11) "Bertholomé"
      ["firstName":"Person":private]=>
      string(7) "Sylvain"
      ["age":"Person":private]=>
      int(17)
      ["children":"Person":private]=>
      NULL
    }
    [1]=>
    object(Person)#3 (4) {
      ["lastName":"Person":private]=>
      string(11) "Bertholomé"
      ["firstName":"Person":private]=>
      string(10) "Géraldine"
      ["age":"Person":private]=>
      int(12)
      ["children":"Person":private]=>
      NULL
    }
  }
}
father={"lastName":"Bertholomé","firstName":"Dieudonné","age":58,"children":[{},{}]}

Comments

  • lines 3–12: the object [$child1];
  • Lines 13–22: the attribute array for the object [$child1]. We have them all;
  • line 23: we have the JSON string from the object [$child1];
  • lines 24–45: same for the object [$child2];
  • lines 46–113: for the parent, it’s a little different since its attribute [children] is not [null], as it was for the children;
  • line 113: we see that in the father’s JSON string, the children are empty objects [{}];
  • Lines 80–112: We see that in the father’s attribute table, child 1 has remained an object, as has child 2. In short, the expression [\get_object_vars($this)]—where [$this] represents the father—is not recursive: if an attribute of the class [Person] is itself an object, the expression [\get_object_vars($this)] does not attempt to retrieve its attribute array. However, the function [json_encode] encodes only the public attributes of an object: the children, all of whose attributes are private, become empty JSON objects.

We can improve this. We modify the [Person] class to the following [Person2] class:

    // __toString
    public function __toString(): string
    {
        // we retrieve the attributes of the object
        $attributes = $this->getAttributes();
        $children = $attributes["children"];
        if ($children !== null) {
            $attributes["children"] = [$children[0]->getAttributes(), $children[1]->getAttributes()];
        }
        // we return the jSON string of the attributes
        return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
    }

    public function getAttributes(): array
    {
        return \get_object_vars($this);
    }
}

Comments

  • lines 64–67: The [getAttributes] method returns the attribute array of the object that calls it;
  • lines 52–62: the [__toString] method;
  • Line 55: We retrieve the object's attributes from the array [$attributes];
  • line 56: Based on the previous example, we know that [$attributes["children"]] is an array of two objects of type [Person2];
  • Lines 57–59: The two objects are replaced by their array of attributes;
  • line 61: all that remains is to encode the constructed array of attributes as JSON.

The [json-02.php] script uses the [Person2] class as follows:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// Person2 class
require_once __DIR__ . "/Person2.php";

// instantiation of the father
$father = new Person2();
// initialization of the father
$father->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Dieudonné",
    "age" => 58,
]);
// instantiation and initialization of child1
$child1 = (new Person2())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Sylvain",
    "age" => 17,
]);
// instantiation and initialization of child2
$child2 = (new Person2())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Géraldine",
    "age" => 12,
]);
// initialization of the father's children
$father->setFromArray([
    "children" => [$child1, $child2],
]);

// display of the father
print "------------------------father\n";
print "father=$father\n";

The script [json-02.php] is identical to the script [json-01.php], except that the class [Person2] has replaced the class [Person] and only the parent is displayed. The results of the execution are as follows:

1
2
3
> php json-02.php
------------------------father
father={"lastName":"Bertholomé","firstName":"Dieudonné","age":58,"children":[{"lastName":"Bertholomé","firstName":"Sylvain","age":17,"children":null},{"lastName":"Bertholomé","firstName":"Géraldine","age":12,"children":null}]}

This time, the children are correctly displayed along with the parent.

The previous solution is unsatisfactory because the children may have children of their own. This brings us back to the previous problem. The class [Person3] resolves this problem as follows:

    // __toString
    public function __toString(): string
    {
        // we return the jSON string of the attributes
        $attributes = [];
        $this->getRecursiveAttributes($attributes, $this, []);
        // jSON string of the attributes
        return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
    }

    public function getAttributes(): array
    {
        return \get_object_vars($this);
    }

    private function getRecursiveAttributes(array &$attributes, mixed $value, array $keys): void
    {
        // analysis of the value [$value]
        // $keys is an array [key1, key2, .., keyn]
        // $value=$attributes[key1][key2]...[keyn]
        // if [$value] is an object we use its [getAttributes] method
        if (\is_object($value)) {
            // attributes of the object [$value]
            $objectAttributes = $value->getAttributes();
            // are there keys?
            if ($keys) {
                // in [$attributes], we are going to replace $value with the array of its attributes
                // we must build the element $attributes[key1][key2]...[keyn]
                // where $keys is the array [key1, key2, .., keyn]
                // we take the reference of the array [$attributes]
                $attribute = &$attributes;
                // we scan the array of keys
                foreach ($keys as $key) {
                    // we take the reference of the attribute
                    $attribute = &$attribute[$key];
                }
                // here $attribute and $attributes[key1][key2]...[keyn] are identical
                // they share the same memory location
                // the object [$value] is replaced by its array of attributes:
                // we must write $attributes[key1][key2]...[keyn]=$objectAttributes
                // which is equivalent to $attribute = $objectAttributes
                $attribute = $objectAttributes;
            } else {
                // no keys - we are at the beginning of the exploration of the object
                // $objectAttributes represents the 1st-level attributes of the class
                $attributes += $objectAttributes;
            }
            // maybe there are still objects in [$objectAttributes]
            // we explore the attributes of [$objectAttributes]
            $this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
        } elseif (\is_array($value)) {
            // we have an array - we analyze each of its elements
            foreach ($value as $key => $element) {
                // we add the current key to the array $keys
                $keys[] = $key;
                // we analyze $element
                $this->getRecursiveAttributes($attributes, $element, $keys);
                // we remove from the array $keys the key that has just been analyzed
                \array_pop($keys);
            }
        }
    }
}

Comments

  • line 56: this time, the method [__toString] retrieves the attributes of its class and requires that this be done recursively: if an attribute is an object or an array of objects, then each object must be replaced by its array of attributes in the class’s final array of attributes;
  • Lines 66–112: The [getRecursiveAttributes] method performs this task. We have commented out its code. Writing a recursive function is often complex. That is the case here. The reader will not miss anything if they do not understand it: we will see shortly that PHP offers a much simpler solution. The recursive call occurs at lines 100 and 107;
  • the advantage of this code is that it wasn’t written solely for the [Person3] class. It applies to any class with attributes whose values are objects of different types, as long as the classes used by the main class also have, like the main class itself, the [getAttributes] method found on lines 61–64.

The script [json-03.php] uses the class [Person3] as follows:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// Person3 class
require_once __DIR__ . "/Person3.php";

// instantiation of the father
$father = new Person3();
// initialization of the father
$father->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Dieudonné",
    "age" => 58,
]);
// instantiation and initialization of child1
$child1 = (new Person3())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Sylvain",
    "age" => 27,
]);
// instantiation and initialization of child2
$child2 = (new Person3())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Géraldine",
    "age" => 12,
]);
// initialization of the father's children
$father->setFromArray([
    "children" => [$child1, $child2],
]);
// instantiation and initialization of child11
$child11 = (new Person3())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Gaëtan",
    "age" => 2,
]);
// instantiation and initialization of child12
$child12 = (new Person3())->setFromArray([
    "lastName" => "Bertholomé",
    "firstName" => "Mathilde",
    "age" => 1,
]);
// initialization of the children of child1
$child1->setFromArray([
    "children" => [$child11, $child12],
]);
// display of the father
print "------------------------father\n";
print "father=$father\n";
  • lines 34–47: [$child1] is given two children.

The results of the execution are as follows:

1
2
3
> php json-03.php
------------------------father
father={"lastName":"Bertholomé","firstName":"Dieudonné","age":58,"children":[{"lastName":"Bertholomé","firstName":"Sylvain","age":27,"children":[{"lastName":"Bertholomé","firstName":"Gaëtan","age":2,"children":null},{"lastName":"Bertholomé","firstName":"Mathilde","age":1,"children":null}]},{"lastName":"Bertholomé","firstName":"Géraldine","age":12,"children":null}]}

The JSON strings have been successfully retrieved from all [Person3] objects that make up the parent.

5.24.1. The [JsonSerializable] interface

There is a much simpler solution, provided by PHP: the predefined interface [JsonSerializable]. We will examine interfaces in the next chapter. For now, let’s just note that a class that “implements” the [JsonSerializable] interface must define a [jsonSerialize(): mixed] method. When the function [json_encode] encounters an object of this class, it calls its method [jsonSerialize] and encodes the returned value in place of the object. It does this at every level of the data tree. The class [Person4] uses this interface:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// the class implements the predefined interface [JsonSerializable]
class Person4 implements \JsonSerializable
{
    // constructor with parameter promotion
    public function __construct(
        private string $lastName,
        private string $firstName,
        private int $age,
        // array of [Person4] objects
        private array $children = [],
    ) {
    }

    // adds a child and returns the current object to chain the calls
    public function addChild(Person4 $child): static
    {
        $this->children[] = $child;
        return $this;
    }

    // method of the [JsonSerializable] interface:
    // returns the value that [json_encode] must encode instead of the object
    #[\Override]
    public function jsonSerialize(): mixed
    {
        return \get_object_vars($this);
    }

    // __toString: the jSON string of the object
    public function __toString(): string
    {
        return \json_encode($this, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
    }
}

Comments

  • line 7: the class implements the [JsonSerializable] interface;
  • lines 10–16: A constructor with parameter promotion replaces the global setter. The array of children is empty by default;
  • lines 20–24: Adds a child and returns the current object (return type [static]) to allow for chained calls;
  • line 29: the method required by the interface. Its return type is [mixed] (PHP 8.0), which means “any type.” Here, it returns the object’s attribute array. The children remain [Person4] objects: [json_encode] will call their own method, [jsonSerialize]. The recursion is handled by PHP;
  • line 28: the attribute [#[\Override]] also applies to a method imposed by an interface;
  • line 37: [json_encode] is called directly on the object. Three options are combined with the [|] operator:
    • [JSON_UNESCAPED_UNICODE]: accented characters are written as-is;
    • [JSON_PRETTY_PRINT]: The JSON string is formatted across multiple lines, with indentation;
    • [JSON_THROW_ON_ERROR] (PHP 7.3): in the event of an error, [json_encode] throws a [JsonException] exception instead of returning [false]. This is important here: the [__toString] method must return a string and cannot return [false].

The script [json-04.php] uses the class [Person4]:

<?php

// strict respect of the declared types of function parameters
declare(strict_types=1);

// Person4 class
require_once __DIR__ . "/Person4.php";

// the father, his children and his grandchildren
$child1 = (new Person4("Bertholomé", "Sylvain", 27))
    ->addChild(new Person4("Bertholomé", "Gaëtan", 2))
    ->addChild(new Person4("Bertholomé", "Mathilde", 1));
$father = (new Person4("Bertholomé", "Dieudonné", 58))
    ->addChild($child1)
    ->addChild(new Person4("Bertholomé", "Géraldine", 12));

// display of the father: [json_encode] calls [jsonSerialize] on each object encountered
print "------------------------father\n";
print "father=$father\n";
// a compact jSON string, without escaping accented characters
print "------------------------json_encode\n";
print json_encode($child1, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR) . "\n";
// decoding: we get an associative array (2nd parameter set to true), not a [Person4] object
print "------------------------json_decode\n";
$data = json_decode((string) $father, true, flags: JSON_THROW_ON_ERROR);
print "first name of the 1st grandchild: {$data['children'][0]['children'][0]['firstName']}\n";
// an incorrect jSON string causes a [JsonException] exception
print "------------------------error\n";
try {
    json_decode('{"lastName": "Bertholomé"', flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
    print "Error: " . $e->getMessage() . "\n";
}
// without [JSON_THROW_ON_ERROR], the error is silent
var_dump(json_decode('{"lastName": "Bertholomé"'));
print "json_last_error_msg: " . json_last_error_msg() . "\n";

Results:

> php json-04.php
------------------------father
father={
    "lastName": "Bertholomé",
    "firstName": "Dieudonné",
    "age": 58,
    "children": [
        {
            "lastName": "Bertholomé",
            "firstName": "Sylvain",
            "age": 27,
            "children": [
                {
                    "lastName": "Bertholomé",
                    "firstName": "Gaëtan",
                    "age": 2,
                    "children": []
                },
                {
                    "lastName": "Bertholomé",
                    "firstName": "Mathilde",
                    "age": 1,
                    "children": []
                }
            ]
        },
        {
            "lastName": "Bertholomé",
            "firstName": "Géraldine",
            "age": 12,
            "children": []
        }
    ]
}
------------------------json_encode
{"lastName":"Bertholomé","firstName":"Sylvain","age":27,"children":[{"lastName":"Bertholomé","firstName":"Gaëtan","age":2,"children":[]},{"lastName":"Bertholomé","firstName":"Mathilde","age":1,"children":[]}]}
------------------------json_decode
first name of the 1st grandchild: Gaëtan
------------------------error
Error: Syntax error
NULL
json_last_error_msg: Syntax error

Comments

  • lines 10–12: construction of [$child1] and its two children through chained calls;
  • line 19: the parent’s [__toString] method produces a formatted JSON string that contains all descendants (lines 3–34 of the results). We did not write any recursive code;
  • line 22: without [JSON_PRETTY_PRINT], the JSON string is compact (line 36 of the results);
  • line 25: the function [json_decode] performs the inverse operation: it transforms a JSON string into a PHP value. With a second parameter set to [true], the JSON objects become associative arrays. Note that this returns arrays rather than [Person4] objects: reconstructing objects from a JSON string requires additional code (or a library such as Symfony’s [Serializer] component). The named argument [flags:] eliminates the need to pass the third parameter [depth];
  • line 30: the JSON string is incorrect (missing closing curly brace). With [JSON_THROW_ON_ERROR], a [JsonException] exception is thrown (line 40 of the results);
  • line 35: without [JSON_THROW_ON_ERROR], [json_decode] returns [null] without reporting any errors (line 41). You should then check the [json_last_error_msg] function to see if an error occurred. That is why we will consistently use the [JSON_THROW_ON_ERROR] option for the remainder of this course.