Skip to content

5. Classes

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

5.1. The script hierarchy

Image

5.2. Any variable can become an object with attributes

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


<?php

// a generic object
// $obj1=new stdClass();
// any variable can have attributes by default
$obj1->attr1 = "one";
$obj1->attr2 = 100;
// display the object
print "object1=[$obj1->attr1,$obj1->attr2]\n";
// Modify the object
$obj1->attr2 += 100;
// display the object
print "object1=[$obj1->attr1,$obj1->attr2]\n";
// copies the value of object1 (address of the pointed-to object) into object2
// the two variables are now different but point to the same object
$obj2 = $obj1;
// modifies obj2
$obj2->attr2 = 0;
// displays both objects
print "object1=[$obj1->attr1,$obj1->attr2]\n";
print "object2=[$obj2->attr1,$obj2->attr2]\n";
// change the object pointed to by obj1
$obj1 = new stdClass();
print "obj1:\n";
print_r($obj1);
print "obj2:\n";
print_r($obj2);
// assigns the reference (address) of object2 to object3
// $obj2 and $obj3 are now the same variable
$obj3 = &$obj2;
print "obj2:\n";
print_r($obj2);
print "obj3:\n";
print_r($obj3);
// modifies obj3
$obj3->attr2 = 10;
// displays both objects
print "object2=[$obj2->attr1,$obj2->attr2]\n";
print "object3=[$obj3->attr1,$obj3->attr2]\n";
// change 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?
print count($obj3) . "\n";
while (list($attribute, $value) = each($obj3)) {
  print "obj3[$attribute] = $value\n";
}
// end
exit;

Results:


Warning: Creating default object from empty value in C:\Data\st-2019\dev\php7\php5-examples\examples\example_14.php on line 6
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
)

Warning: count(): Parameter must be an array or an object that implements Countable in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 50
1

Deprecated: The each() function is deprecated. This message will be suppressed on further calls in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 51
obj3[attr3]=two
obj3[attr4]=20

Comments

  • line 6: the notation $obj->attr refers to the attr attribute of the $obj variable. If it does not exist, it is created, thereby making the $obj variable an object with attributes. We have seen that PHP then creates a stdClass object by default;
  • line 16: the expression $obj2=$obj1, when $obj1 is an object, is a copy of objects by reference: $obj2 and $obj1 are references (addresses) to the same object. The object itself can be modified by either reference;
  • lines 23–27: aim to show that $obj1 and $obj2 are two different variables: they are not at the same memory address:
    • $obj2 = $obj1 copies the value of $obj1 into the variable $obj2 (step 1 above). The value of $obj1 is the address of an object. Thus, $obj1 and $obj2 point to the same object. When manipulating a variable $obj that points to an object, PHP manipulates the object pointed to by the variable $obj. According to the diagram below, we can see that the pointed-to object can be modified either via $obj1 or via $obj2. This is what lines 4 and 5 of the results show;

Image

  • line 30: the expression $obj3=&$obj2 causes $obj2 and $obj3 to have the same address [1 below]. We could say that the two variables are aliases for the same memory location. They both point to an object, Object A below [2];
    • 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. Since $obj2 and $obj3 are two aliases for the same memory location, $obj3 also points to the new object, Object B. This is shown in lines 16–27 and 30–41 of the results;

Image

  • lines 52–54: show that an object can be traversed like a dictionary. The keys of the dictionary are the names of the attributes, and the values of the dictionary are the values of those same attributes;
  • line 51: the count function can be applied to an object (with a warning) but does not, as one might expect, return the number of attributes. Thus, an object shares similarities with a dictionary but is not one;

5.3. A Person class with no declared attributes

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


<?php

class Person {

  // class attributes
  // not declared - can be created dynamically
  // method
  function identity() {
    // initially, uses non-existent attributes
    return "[$this->firstName,$this->lastName,$this->age]";
  }

}

// test
// attributes are public and can be created dynamically
$p = new Person();
$p->firstName = "Paul";
$p->lastName = "Langevin";
$p->age = 48;
// Calling a method
print "person=" . $p->identity() . "\n";
// end
exit;

Results:

person=[Paul,Langevin,48]

Comments

  • lines 3–13: define a Person class. A class is a template used to create objects. It contains attributes and functions called methods. Attributes do not need to be declared;
  • lines 8–11: the identity method displays the values of three attributes not declared in the class. The keyword $this refers to the object to which the method is applied;
  • line 17: we create an object $p of type Person. 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 18–20: the three attributes required by the identity method are created in the $p object;
  • line 22: the identity method of the Person class is applied to the $p object. In the code (lines 8–11) of the identity method, $this refers to the same object as $p;

5.4. The Person class with declared attributes

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


<?php

class Person {

  // class attributes
  var $firstName;
  var $lastName;
  var $age;

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

}

// test
// attributes are public
$p = new Person();
$p->last_name = "Paul";
$p->last_name = "Langevin";
$p->age = 48;
// call a method
print "person=" . $p->identity() . "\n";
// end
exit;

Results:

person=[Paul,Langevin,48]

Comments

  • lines 6–8: the class attributes are explicitly declared using the var keyword;

5.5. The Person class with a constructor

The previous examples showed exotic Person classes as they might have been found in PHP 4. It is not recommended to follow these examples. We now present a Person class [classes-04.php] that follows PHP 7 best practices:


<?php

// Strict adherence to the declared types of function parameters
declare (strict_types=1);
class Person {
// class attributes
  private $first_name;
  private $last_name;
  private $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 use the set methods
    $this->setFirstName($firstName);
    $this->setLastName($lastName);
    $this->setAge($age);
  }

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

}

// test
// Create a Person object
$p = new Person("Paul", "Langevin", 48);
// identity of this person
print "person=$p\n";
// change the age
$p->setAge(14);
// person's identity
print "person=$p\n";
// end
exit;

Results:

person=[Paul,Langevin,48]
person=[Paul,Langevin,14]

Comments

  • lines 6–50: the Person class;
  • lines 7-9: the class's private attributes. These attributes are only visible 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 the following code:
$p = new Personne();
$p->name = "Landru";

Here, we are outside the Person class. Since the name attribute is private, line 2 is incorrect. To initialize the private fields of the $p object, there are two ways:

  • use the public set and get methods (the names of these methods can be anything) in lines 12–34. We can then write:
$p = new Personne();
$p->setName("Landru");
  • use the constructor in lines 37–42. We would then write:
$p = new Person("Michel", "Landru", 44);

The code above automatically calls the Person class method called __construct;

  • Line 59: This line displays the person $p as a string. To do this, the Person class method called __toString (lines 45–47) is used;
  • All methods of the class (functions) have been prefixed with the keyword public, which indicates that the function 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 attribute, the function has implicit public visibility;

5.6. The Person class with validity checks in the constructor

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

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


<?php

// Strict adherence to the declared types of function parameters
declare (strict_types=1);

// namespace;
namespace Examples;

// Person class
class Person {
// class attributes
  private $firstName;
  private $lastName;
  private $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");
    } else {
      $this->first_name = $first_name;
    }
  }

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

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

// constructor
  public function __construct(string $first_name, string $last_name, int $age) {
    // we use the set methods
    $this->setFirstName($firstName);
    $this->setLastName($lastName);
    $this->setAge($age);
  }

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

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

}

Comments

  • line 4: specifies that the type of the function parameters must be respected when declared;
  • line 7: defines a namespace. The full (or qualified) name of the Person class is then \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 relative (relative to the current namespace). Thus, if two classes A and B are part of the same namespace E, in the code of class A, class B can be accessed using the relative notation B. If class A is part of namespace E1 and B is part of namespace E2, in the code of A, B will be accessed using the absolute notation \E2\B. Defining a class within a namespace is not mandatory, but NetBeans issues a warning if you do not do so. Therefore, we will do it. Furthermore, namespaces should correspond to the file directory structure. Thus, class A in namespace E1 should be in a file named E1/A.php. This is not mandatory, but again, NetBeans issues a warning if you do not do so. In the example of the class [\Exemples\Personne], NetBeans issues a warning because the file path of [Personne.php] is [exemples/classes/Personne.php] and therefore does not match the namespace. Do not confuse the file path with the namespace. The fully qualified name of a class uses a namespace and has nothing to do with the file path of the class’s PHP file. The file path/namespace relationship is optional and may be ignored, as we have done here;
  • lines 12–14: the class’s three private attributes;
  • lines 29–37: initialization of the first_name attribute and verification of the initialization value;
  • line 31: the trim($string) function removes spaces at the beginning and end of $string. Thus, trim("abcd") is the string "abcd" and trim(" ") is the empty string;
  • line 32: if the first name is empty, then an exception is thrown (line 33); otherwise, the first name is stored (line 35). To throw an exception, we used the predefined class [Exception] 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., the [Examples] namespace of the Person class. Thus, the PHP interpreter will search for a class with the absolute name [\Examples\Exception], which does not exist;

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


<?php

// Strict adherence to function parameter types
declare(strict_types=1);

// include the Person class definition
require_once __DIR__."/Person.php";

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

// test
// creation of a Person object
$p = new Person("Paul", "Langevin", 48);
// identity of this person
print "Example1, person=$p\n";
// incorrect creation of a 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 "Example 3, error: " . $e2->getMessage() . "\n";
}

// end
exit;

Comments

  • line 7: the script will use the [Person] class. We must therefore tell the PHP interpreter where it can find the definition of this class. This is the role of the [include file] and [require file] statements. Here we have used the [include] statement. The difference between the two statements is as follows: if the [include file] statement encounters errors while loading [file], an [E_WARNING] error is issued but execution continues, whereas [require] in the same case generates a fatal error and script execution stops. Each of these two statements has a variant: [include_once] and [require_once]. These two variants allow you to handle cases where the same file is included multiple times. Consider a project consisting of several PHP scripts, some of which reference the [Person] class. Their execution will cause the [Person.php] file 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 overall script;
  • line 7: the constant [__DIR__] is a PHP constant that refers to the full path of the directory containing the script with the [__DIR__] constant. Thus, the expression on line 17:

require_once __DIR__."/Personne.php";

will be equivalent to something like:


require_once ‘C:\Data\st-2019\dev\php7\php5-examples\examples\classes/People.php’

In the file path, you can use either / or \;

  • Line 14: We use the [Person] class that we just defined. The [classes-05.php] script has no namespaces. Line 14 uses the relative name of the [Person] class without a namespace. Since the [Person] class has no namespace, it is searched for within the [classes-05.php] script itself and will therefore not be found. There are two solutions to this problem:
    • use the full class name [\Examples\Person];
    • use the `use` statement on line 10. This indicates that the code that follows uses the class `[Examples\Person]`;
  • Line 10: The use statement lets the interpreter know that the [Person] class referenced on line 14 is actually the [\Examples\Person] class. That said, where will the interpreter find the code for this class? Line 7 tells it. This line indicates that to execute the current script, the script [Person.php] must also be loaded. Here, the relative file name is used. It will therefore be searched for in the folder containing the script [classes-05.php]. The scripts [Person.php] and [classes-05.php] must therefore be in the same folder. This is the case here, where they are both in the [examples/classes] folder. The statement on line 10 is equivalent to:

use \Examples\Person as Person;

The [use] statement above specifies that the alias [Person] refers to the class [\Exemples\Personne];

  • line 14: a [Person] object is created. The [__construct] method of the [Person] class will be implicitly executed here;
  • line 16: displays the Person $p. To be displayed, the value of the variable $p must be converted to a string. Implicitly, the [Person.__toString] method is then executed. This method must therefore return a string;
  • We have seen that the constructor of the [Person] class can throw an exception of type [\Exception]. This must therefore be handled. Consequently, the code on line 14 is incomplete. We must use the code in lines 18–24 to properly handle the exception that may occur. Here, we intentionally trigger one by passing an age that is not an integer. In this specific case, the exception that is generated— —is thrown by the PHP interpreter and not by the code of the [Person] class. Indeed, the signature of the [Person.__construct] method is as follows:

function __construct(string $first_name, string $last_name, 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 a [TypeError]. Furthermore, the [set] methods of the [Person] class throw an [\Exception]. Since the constructor that calls them does not have a try/catch block, the exception propagates up one level to the code that called the constructor, i.e., the code in the [classes-05.php] script. Ultimately, the [classes-05.php] script can receive two types of exceptions: \Exception or \TypeError. Note that when the developer is certain certain exceptions cannot occur, they will not use the corresponding catch blocks. Here, they are used systematically solely for demonstration purposes. However, catch blocks will be used for every possible exception, even if unlikely;

For this reason, the try block in lines 18–24 has two catch blocks to handle the two exception types separately;

  • Line 20: You can write either [Exception] or [\Exception]:
    • The first version uses the relative class name, relative to the script’s namespace. The script has no namespace. Its namespace is therefore the root of all namespaces: \. Thus, writing [Exception] here is equivalent to writing [\Exception]. And the [Exception] class is indeed located in the [\] namespace;

It is preferable to use the absolute names of PHP’s predefined exceptions in a script that does not have a namespace itself. Thus, if you decide to give this script a namespace, writing the absolute class names remains valid, whereas in the other case, changing the namespace will cause errors with the relative class names;

  • Line 21: When an exception occurs, the [Exception→getMessage] method retrieves the exception’s error message. The same applies to a [TypeError]. In the [Person.setFirstName] method, we wrote:

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

On line 5, an exception is thrown with the error message [The first name must not be empty]. This is what the [Exception→getMessage] method on line 29 of the [classes-05.php] script will retrieve.

Results:


Example 1, person=[Paul,Langevin,48]
Example 2, error: Argument 3 passed to Examples\Person::__construct() must be of type integer, string given, called in C:\Data\st-2019\dev\php7\php5-examples\examples\example_18.php on line 19
Example 3, error: The first name must not be empty

5.7. Adding a method acting as a secondary constructor

In PHP 7, it is not possible to have multiple constructors with different parameters that would allow an object to be constructed in various ways. We can therefore use methods acting as constructors:


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

Comments

  • lines 2-5: the initWithPerson method allows the values of the attributes of another Person object to be assigned to the current object. Here, it calls the __construct constructor, but this is not mandatory. It could initialize the attributes of the [Person] class itself;

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


<?php

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

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

// verification
print "person=$p\n";
// end
exit;
  • Lines 14, 23, 30: 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: we do not stop the script’s execution but display an error page. If we are inside a function, we do not use the exit statement but return: we do not stop the script’s execution (exit) but exit the function (return) after setting an error;

Results:

person=[Paul,Langevin,48]
person=[Laure,Adeline,67]

5.8. An array of objects [Person]

The following example [classes-07.php] shows that you can have arrays of objects.


<?php

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

// test
// creating an array of Person objects
// to make the code easier to understand, we do not handle any exceptions
$group = [new Person("Paul", "Langevin", 48), new Person("Sylvie", "Lefur", 70)];

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

// end
exit;

Results:

group[0]=[Paul,Langevin,48]
group[1] = [Sylvie, Lefur, 70]

Comments

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

5.9. Creating a class derived from the Person class

We create the following [Teacher] class in a file named [Teacher.php]:


<?php

// Strict adherence to the declared types of function parameters
declare (strict_types=1);

// namespace
namespace Examples;

// a class derived from Person
class Teacher extends Person {
  // attributes
  private $subject;    // subject taught

  // getter and setter

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

  public function setDiscipline(string $discipline): void {
    $this->discipline = $discipline;
  }

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

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

}

Comments

  • line 7: the [Teacher] class is also part of the [Examples] namespace;
  • line 10: the Teacher class extends the Person class. The derived Teacher class inherits the attributes and methods of its parent class;
  • line 12: the Teacher class adds a new attribute, subject, which is specific to it;
  • line 25: the Teacher class constructor takes 4 parameters:
    • 3 to initialize its parent class (first_name, last_name, age), line 27;
    • 1 for its own initialization (subject), line 29;
  • line 27: the derived class has access to the methods and constructors of its parent class via the keyword parent::. Here, we pass the parameters (first_name, last_name, age) to the parent class’s constructor;
  • lines 33–35: the __toString method of the derived class uses the __toString method of the parent class;

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


<?php

// Include the definitions 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
// creation of an array of Person objects and derived classes
// for simplicity, we do not handle exceptions
$group = array(new Teacher("Paul", "Langevin", 48, "English"), new Person("Sylvie", "Lefur", 70));

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

Comments

  • lines 4-5: we need to tell the PHP interpreter where the two classes [Teacher, Person] are located;
  • lines 7-8: 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 suffix;
  • line 13: we create an array containing a [Person] type and a [Teacher] type;
  • lines 16-18: display the elements of the array;
  • line 17: the __toString method of each element $group[$i] will be called. The Person class has a __toString method. The Teacher class has two: that of its parent class and its own. One might wonder which one will be called. The execution shows that it was the one from the Teacher class that 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 itself, 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.

Results:

group[0]=[[Paul,Langevin,48],English]
group[1]=[Sylvie,Lefur,70]

5.10. Creating a second class derived from the Person class

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


<?php

// Strict adherence to the declared types of function parameters
declare (strict_types=1);

// namespace
namespace Examples;

class Student extends Person {
  // attributes
  private $education;    // education

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

  public function setFormation(string $training): void {
    $this->training = $training;
  }

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

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

}

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


<?php

// Include and define the classes used by the script
require_once __DIR__."/Person.php";
use \Examples\Person;
require_once __DIR__."/Teacher.php";
use \Examples\Teacher;
require_once __DIR__."/Student.php";
use \Examples\Student;

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

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

Results:


group[0]=[[Paul,Langevin,48],English]
group[1]=[Sylvie,Lefur,70]
group[2]=[[Steve,Boer,23],iup2 quality]

5.11. 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 this is not the case with PHP 7:


<?php

class Class1 {

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

}

class Class2 extends Class1 {

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

}

class Class3 extends Class1 {

  // constructor
  public function __construct() {
    // Explicit call to the parent class constructor
    parent::__construct();
    // Code specific to Class3
    print "Class3 constructor\n";
  }

}

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

Results

1
2
3
4
5
test1---------
Class2 constructor
test2---------
constructor of the Class1 class
constructor of the Class3 class

5.12. 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 [__toString] method of the [Person] class (see link) has been overridden in the child classes [Teacher] (see link) and [Student] (see link). The script [classes-13.php] illustrates the concept again:


<?php

// Strict adherence to function parameter types
declare(strict_types=1);

// main class
class Class1 {

  public function f(): int {
    return 1;
  }

  function g(): int {
    return 2;
  }

}

// derived class
class Class2 extends Class1 {

  // we redefine the f function of the parent class
  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";

Comments

  • lines 7–17: the class [Class1] defines two methods, f and g;
  • lines 20–27: the class [Class2] extends the class [Class1] and redefines its method f;

Results

1
2
3
11
2
1

Comments

  • Line 30 of the code creates an object $c2 of type [Class2];
  • Line 31 of the code calls the f method of the $c2 object. Since this method exists, it is executed;
  • Line 32 of the code calls the g method of the $c2 object. Since this method does not exist, it is looked up in the parent class, where it is found and executed;
  • Line 33 of the code creates an object $c1 of type [Class1];
  • Line 34 of the code calls the f method of the $c1 object. Since this method exists, it is executed;

5.13. Passing an object as a function parameter

Consider the following script [classes-14.php]:


<?php

// Strict adherence to function parameter types
declare(strict_types=1);

// main class
class Class1 {

  public function f(): int {
    return 1;
  }

  function g(): int {
    return 2;
  }

}

// derived class
class Class2 extends Class1 {

  // we redefine the f function of the parent class
  public function f(): int {
    return parent::f() + 10;
  }

}

// The function parameter is of type Class1 or a derived class
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();
// call doSomething with
doSomething($c2);

Comments

  • lines 7–17: the [Class1] class;
  • lines 20–27: a class [Class2] derived from [Class1];
  • line 30: 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;
  • lines 35–38: the function [doSomething] is called with a parameter of type [Class2], even though the expected type is [Class1];

Results

13

5.14. 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 an abstract class used for? Sometimes we have classes that share one or more methods but differ in other methods or attributes. It is then desirable to group everything that is 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 abstract keyword: 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 abstract keyword. 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 adherence to function parameter types
declare(strict_types=1);

// main abstract class
abstract Class Class1 {

  // method known to all derived classes
  public function f(): int {
    return 1;
  }

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

// derived class
Class Class2 extends Class1 {

  // The g method of the parent class must be defined
  public function g(): int {
    return parent::f() + 10;
  }

}

// derived class
Class Class3 extends Class1 {

  // The g method of the parent class must be defined
  public function g(): int {
    return parent::f() + 20;
  }

  // We can override the f method of the parent class
  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";

Comments

  • lines 7–16: the [Class1] class is abstract (line 7) because it does not know how to implement the g method on line 15. It must therefore be derived to be usable;
  • lines 19–26: the class [Class2] extends the class [Class1] and redefines the g method of its parent class (lines 22–24);
  • lines 29–41: the class [Class3] extends the class [Class1] and redefines the method g of its parent class (lines 32–34);
  • lines 37–39: the class [Class3] redefines the method f of its parent class;
  • lines 44–49: two objects of types [Class2] and [Class3] are created, and their methods f and g are called;

Results

1
2
3
4
1
11
2
21

5.15. Final classes

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


<?php

// namespace
namespace Examples;

// non-derivable class
final Class Class1 {
  
}

// derived class
Class Class2 extends Class1 {
  
}

// code - should cause an error
new Class2();

Comments

  • lines 7–9: the final keyword makes the [Class1] class a final class that cannot be derived from;
  • lines 12-14: the class [Class2] extends the final class [Class1], which is an error;
  • line 17: the error will only be reported when the script is executed and an attempt is made to manipulate an object of type [Class2];

Results

Fatal error: Class Examples\Class2 may not inherit from a final class (Examples\Class1) in C:\Data\st-2019\dev\php7\php5-examples\examples\classes\classes-11.php on line 14

5.16. Final Methods

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


<?php

// Strict adherence to function parameter types
declare(strict_types=1);

// namespace
namespace Examples;

// main class
Class Class1 {

  // This method cannot be overridden in a derived class
  public final function f(): int {
    return 1;
  }

}

// derived class
Class Class2 extends Class1 {

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

}

// code - should cause an error
new Class2();

Comments

  • line 13: the f method of class [Class1] is declared final using the final keyword;
  • line 20: the class [Class2] extends the class [Class1];
  • lines 22-23: the function f of the parent class [Class1] is redefined. This should cause an error;
  • line 29: an object of type [Class2] is created to force the PHP interpreter to inspect the [Class2] class;

Results

Fatal error: Cannot override final method Examples\Class1::f() in C:\Data\st-2019\dev\php7\php5-examples\examples\classes\classes-12.php on line 26

5.17. Static methods and attributes

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

  • C::M if you are outside the class;
  • self::M if we are in the class;

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


<?php

class Class1 {

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

}

// test -------------------
Class1::say("hello");

Comments

  • line 6: the [say] method is declared static using the static keyword;
  • line 13: call to the static method [say] using the syntax: Class1::say;

Results

hello

Now consider the following code [classes-18.php]:


<?php

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

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

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

}

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

Comments

  • Line 5: We declare a static attribute that will count the number of instances of class [Class1] 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 within the instance makes no sense: when a new object is created, in which instance would we increment a counter? We would be forced to increment the counter of a specific object, ignoring the counters of the other instances. A static attribute is a class attribute, not an instance attribute of the class;
  • lines 7–10: it is in the constructor that we will count the created objects, since the creation of each new object triggers the execution of the constructor;
  • line 14: Note the notation `self::$nbObjects` to indicate that we are referring to a static attribute of the class in which the executed code resides;
  • lines 13–15: the static method [say] is responsible for displaying the number of objects created;
  • lines 20–22: we create two objects and display the object counter;

Results


Class1 constructor
Class1 constructor
2 objects of type [Class1] have been constructed

5.18. Visibility between Parent Class and Child Class

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


<?php

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

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

}

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

  // method
  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 3–17: the [SomeParent] class;
  • lines 19–32: the child class [SomeChild]. We can see that it extends the class [SomeParent] (line 19);
  • line 5: the [SomeParent] class has only one attribute;
  • lines 8–17: the method [SomeParent::doTest] is intended to display two attributes:
    • [$attributeOfParent], which belongs to the [SomeParent] class;
    • [$attributeOfChild], which belongs to the [SomeChild] class (line 21);
  • lines 10–11: 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 13–14: display of the two attributes;
  • lines 19–32: the child class [SomeChild] that extends the class [SomeParent] (line 19);
  • line 21: the class [SomeChild] has only one attribute;
  • line 24: the method [SomeChild::doTest] is intended to display two attributes:
    • [$attributeOfParent], which belongs to the [SomeParent] class;
    • [$attributeOfChild], which belongs to the [SomeChild] class;
  • lines 26–27: display of the two attributes;
  • line 30: calls the [doTest] method of the parent class, which in turn displays the two attributes;
  • line 36: the [SomeParent::doTest] method is called;
  • line 38: 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. This attribute would need to have at least [protected] visibility. But what about the child class’s attribute? Is it visible in the parent class?

Here are the results of this first test:

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

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14
parent: attributeOfChild=
---------------------------test2

Notice: Undefined property: SomeChild::$attributeOfParent in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 26
child: attributeOfParent=
child: attributeOfChild=14
parent:
object(SomeChild)#1 (2) {
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
  ["attributeOfParent":"SomeParent":private]=>
  int(4)
}
parent: attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php(39): SomeChild->doTest()
#2 {main}
  thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-19.php on line 14

Comments

  • lines 1-10: results of the first test where the method [SomeParent::doTest] is called;
  • lines 3-6: we see that the object calling the method is of type [SomeParent];
  • line 7: display of the [$attributeOfParent] attribute;
  • lines 9-10: we see that the attribute [SomeParent::$attributeOfChild] does not exist. It is therefore not displayed;
  • lines 11–30: results of the second test where the method [SomeChild::doTest] is called;
  • lines 13–14: we see that the attribute [SomeChild::$attributeOfParent] does not exist. It is therefore not displayed. This is normal: the attribute [SomeParent::$attributeOfParent] is [private] and therefore not known in the child class;
  • line 15: display of the [$attributeOfChild] attribute;
  • lines 16–30: we are in the [SomeParent::doTest] method called by the child class;
  • lines 17–22: we see that [$this] is of type [SomeChild] with two private attributes;
  • line 23: surprisingly, [$this] of type [SomeChild] can see the parent’s attribute [$attributeOfParent] here;
  • lines 25–30: just as surprisingly, [$this] of type [SomeChild] does not see its own attribute [$attributeOfChild];

This result is very surprising: although lines 17–21 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 and not of the [SomeChild] class.

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


<?php

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

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

}

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

  // method
  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

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

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14
parent: attributeOfChild=
---------------------------test2
child: attributeOfParent=4
child: attributeOfChild=14
parent:
object(SomeChild)#1 (2) {
  ["attributeOfChild":"SomeChild":private]=>
  int(14)
  ["attributeOfParent":protected]=>
  int(4)
}
parent: attributeOfParent=4

Fatal error: Uncaught Error: Cannot access private property SomeChild::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php:14
Stack trace:
#0 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(30): SomeParent->doTest()
#1 C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php(39): SomeChild->doTest()
#2 {main}
  thrown in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-20.php on line 14

Comments

  • line 12: the [SomeChild] class now sees its parent’s attribute [$attributeOfParent]. This is normal since the attribute now has a [protected] scope;
  • in the method [someParent::doTest], the object [$this] is of type [SomeChild] (lines 15–20). It sees its parent’s attribute [$attributeOfParent] (line 21) but still not its own attribute [$attributeOfChild] (lines 23–28);

In the third test, the [$attributeOfChild] attribute also has a [protected] scope:


<?php

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

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

}

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

  // method
  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:

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

Notice: Undefined property: SomeParent::$attributeOfChild in C:\Data\st-2019\dev\php7\poly\scripts-console\classes\classes-21.php on line 14
parent : attributeOfChild=
---------------------------test2
child: attributeOfParent=4
child: attributeOfChild=14
parent:
object(SomeChild)#1 (2) {
  ["attributeOfChild":protected]=>
  int(14)
  ["attributeOfParent":protected]=>
  int(4)
}
parent: attributeOfParent=4
parent: attributeOfChild=14
  • Line 22: This time, inside the parent, [$this] of type [SomeChild] (lines 15–20) 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, sees:
    • the attributes and methods of the parent class regardless of their visibility;
    • does not see any of the attributes and 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 seen;
    • the attributes and methods of the child class, regardless of their visibility;

This is the expected behavior.

  • that 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 [protected] visibility. Those with [private] visibility are not visible;

This is unexpected behavior.

5.19. 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 for this string to be a JSON string. We will explore this path now.

Image

We will use the following [Person] class:


<?php

class Person {
  // attributes
  private $lastName;
  private $lastName;
  private $age;
  private $children;

  // global setter
  public function setFromArray(array $arrayOfAttributes): Person {
    // initialization of certain class attributes
    foreach ($arrayOfAttributes as $attribute => $value) {
      $this->$attribute = $value;
    }
    // return the object
    return $this;
  }

  // getters
  public function getName() {
    return $this->name;
  }

  public function getLastName() {
    return $this->firstName;
  }

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

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

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

}

Comments

  • lines 5–8: the four attributes of the class;
  • lines 20–35: the getters that allow you to retrieve the values of these attributes;
  • lines 11–18: a global setter that initializes the attributes from an associative array [$arrayOfAttributes] whose keys match the class attributes;
  • lines 38–46: the class’s [__toString] method;
  • line 42: the PHP function [get_object_vars] retrieves the values of the class attributes as an associative array [‘name’=>’name1’, ‘first_name’=>’first_name1’, ‘age’=>’age1’, ‘children’=>[]];
  • line 45: we return the JSON string of this array of attributes;

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


<?php

// Person class
require "Person.php";

// instantiate the parent
$parent = new Person();
// Initializing the father
$father->setFromArray([
  "lastName" => "Bertholomé",
  "firstName" => "Dieudonné",
  "age" => 58
]);
// instantiate and initialize child1
$child1 = (new Person())->setFromArray([
  "last_name" => "Bertholomé",
  "first_name" => "Sylvain",
  "age" => 17
  ]);
// instantiate and initialize child2
$child2 = (new Person())->setFromArray([
  "last name" => "Bertholomé",
  "first name" => "Géraldine",
  "age" => 12
  ]);
// initialize father's children
$father->setFromArray([
  "children" => [$child1, $child2]
]);

// display the parent's children
$child1 = ($parent->getChildren())[0];
$child2 = ($parent->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 6-13: we initialize a [Person] object [$father] using the [Person::setFromArray] method, which allows us to initialize a [Person] object with an array whose keys match the attributes of the [Person] class;
  • lines 14-19: we initialize a [Person] object [$child1] in the same way;
  • lines 21–25: we initialize a [Person] object [$child2];
  • lines 27–29: the [$father→children] attribute is initialized with an array of the two children;
  • lines 32–33: we assign the two children to the father;
  • line 35: the [print] operation attempts to convert the [$child1] object into a string. To do this, it uses the object’s [__toString] method. We therefore expect to see the object’s JSON string;
  • lines 38–39: we do the same with the father;

The results are as follows:


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

Comments

  • lines 2–11: the object [$child1];
  • lines 12-21: the array of attributes for the object [$child1]. We have them all;
  • line 22: we have the JSON string for the object [$child1];
  • lines 23-44: same for the object [$child2];
  • Lines 45–112: For the parent, it’s a little different because its [children] attribute isn’t NULL, as it was for the children;
  • line 112: we see that the children are missing from the father’s JSON string;
  • lines 79–111: we see that in the father’s attribute array, child 1 (lines 89–98) remains an object, as does child 2 (lines 99–110). In short, the expression [\get_object_vars($this)], where [$this] represents the parent, is not recursive: if an attribute of the [Person] class is itself an object, the expression [\get_object_vars($this)] does not attempt to retrieve its attribute array;

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


<?php

class Person2 {
  // attributes
  private $last_name;
  private $lastName;
  private $age;
  private $children;

  // global setter
  public function setFromArray(array $arrayOfAttributes): Person2 {

    // return the object
    return $this;
  }

  // getters
  public function getName() {
    return $this->name;
  }



  // __toString
  public function __toString(): string {
    // retrieve the object's attributes
    $attributes = $this->getAttributes($this);
    $children = $attributes["children"];
    if ($children != NULL) {
      $attributes["children"] = [$children[0]->getAttributes(), $children[1]->getAttributes()];
    }
    // 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 36–38: the [getAttributes] function returns the array of attributes of the object that calls it;
  • lines 25–34: the [__toString] function;
  • Line 27: We retrieve the attributes of the [Person] class into the [$attributes] array;
  • line 28: based on the previous example, we know that [$attributes["children"]] is an array of two objects of type [Person];
  • lines 29–31: the two objects are replaced by their array of attributes;
  • line 33: all that remains is to encode the constructed array of attributes into JSON;

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


<?php

// Person2 class
require "Person2.php";

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

// display parent
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].

The results of the execution are as follows:

------------------------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, we successfully obtained the children along with the father.

The previous solution is unsatisfactory because the children may have children of their own. We then encounter the previous problem.

The [Person3] class solves this problem as follows:


<?php

class Person3 {
  // attributes
  private $lastName;
  private $last_name;
  private $age;
  private $children;

  // global setter
  public function setFromArray(array $arrayOfAttributes): Person3 {

  }

  // getters


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

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

  private function getRecursiveAttributes(array &$attributes, $value, $keys): void {
    // parse the value [$value]
    // $keys is an array [key1, key2, .., keyn]
    // $value = $attributes[key1][key2][keyn]
    // if [$value] is an object, use its [getAttributes] method
    if (\is_object($value)) {
      // attributes of the object [$value]
      $objectAttributes = $value->getAttributes();
      // What do we do with the result?
      if ($keys) {
        // In [$attributes], we'll replace $value with the array of its attributes
        // we need to construct the element $attributes[key1][key2][keyn]
        // where $keys is the array [key1, key2, .., keyn]
        // we take the reference to the array [$attributes]
        $attribute = &$attributes;
        // we iterate through the array of keys
        foreach ($keys as $key) {
          // get the reference to the attribute
          $attribute = &$attribute[$key];
        }
        // Here, $attribut and $attributes[key1][key2][key(n)] are identical
        // they share the same memory location
        // the object [$value] is replaced by its array of attributes;
        // you 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 object traversal
        // $objectAttributes represents the first-level attributes of the class
        $attributes += $objectAttributes;
      }
      // perhaps there are still objects in [$objectAttributes]
      // we explore the attributes of [$objectAttributes]
      $this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
    } else {
      if (\is_array($value)) {
        // we have an array - we iterate through each of its elements
        foreach ($value as $key => $element) {
          // add the current key to the $keys array
          \array_push($keys, $key);
          // process $element
          $this->getRecursiveAttributes($attributes, $element, $keys);
          // remove the key that was just parsed from the $keys array
          \array_pop($keys);
        }
      }
    }
  }

Comments

  • lines 21–22: this time, the [__toString] method retrieves the attributes of its class and requests 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 31-78: the [getRecursiveAttributes] function does this work. We have commented on the 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. There are libraries that handle this task. The recursive calls occur on lines 64 and 72;
  • the advantage of this code is that it was not written solely for the [Person3] class. It applies to any class with attributes whose values are of different object types, as long as the classes used by the main class have, like it, the [getAttributes] method in lines 27–29

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


<?php

// Person3 class
require "Person3.php";

// instantiate the parent
$parent = new Person3();
// initialization
$father->setFromArray([
  "lastName" => "Bertholomé",
  "firstName" => "Dieudonné",
  "age" => 58
]);
// instantiate and initialize child1
$child1 = (new Person3())->setFromArray([
  "lastName" => "Bertholomé",
  "lastName" => "Sylvain",
  "age" => 27
  ]);
// instantiate and initialize child2
$child2 = (new Person3())->setFromArray([
  "lastName" => "Bertholomé",
  "first_name" => "Géraldine",
  "age" => 12
  ]);
// initialize the father's children
$father->setFromArray([
  "children" => [$child1, $child2]
]);
// instantiate and initialize child11
$child11 = (new Person3())->setFromArray([
  "lastName" => "Bertholomé",
  "first_name" => "Gaëtan",
  "age" => 2
  ]);
// instantiate and initialize child12
$child12 = (new Person3())->setFromArray([
  "lastName" => "Bertholomé",
  "first_name" => "Mathilde",
  "age" => 1
  ]);
// initialize children of child1
$child1->setFromArray([
  "children" => [$child11, $child12]
]);
// display parent
print "------------------------parent\n";
print "father=$father\n";
  • lines 30-45: we assign two children to [$child1];

The results of the execution are as follows:

------------------------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}]}

If we format this result, we get the following:


father={
    "last_name": "Bertholomé",
    "first_name": "Dieudonné",
    "age": 58,
    "children": [
        {
            "last name": "Bertholomé",
            "first_name": "Sylvain",
            "age": 27,
            "children": [
                {
                    "last name": "Bertholomé",
                    "first_name": "Gaëtan",
                    "age": 2,
                    "children": null
                },
                {
                    "last name": "Bertholomé",
                    "first name": "Mathilde",
                    "age": 1,
                    "children": null
                }
            ]
        },
        {
            "name": "Bertholomé",
            "first name": "Géraldine",
            "age": 12,
            "children": null
        }
    ]
}

We have successfully retrieved the JSON strings for all [Person] objects that make up the parent.