Skip to content

5. Classes

Vocabulário: Uma classe é um tipo PHP. Uma variável deste tipo é chamada de objeto. Um objeto é uma instância (instância) de uma classe.

5.1. A hierarquia de scripts

Image

5.2. Qualquer variável pode tornar-se um objeto com atributos

O script [classes-01.php] é o seguinte:


<?php
 
// a generic object
// $obj1=new stdClass();
// any variable can have attributes by construction
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// displays the
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// modify object
$obj1->attr2 += 100;
// displays the
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// copies the value of object1 (address of the object pointed to) to object2
// the two variables are then different but point to the same object
$obj2 = $obj1;
// modify obj2
$obj2->attr2 = 0;
// displays both objects
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$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 (address) from 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);
// modify obj3
$obj3->attr2 = 10;
// displays both objects
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// changes the object pointed to by obj2
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$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($attribut, $valeur) = each($obj3)) {
  print "obj3[$attribut]=$valeur\n";
}
// end
exit;

Resultados:


Warning: Creating default object from empty value in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_14.php on line 6
objet1=[un,100]
objet1=[un,200]
objet1=[un,0]
objet2=[un,0]
obj1 :
stdClass Object
(
)
obj2 :
stdClass Object
(
    [attr1] => un
    [attr2] => 0
)
obj2 :
stdClass Object
(
    [attr1] => un
    [attr2] => 0
)
obj3 :
stdClass Object
(
    [attr1] => un
    [attr2] => 0
)
objet2=[un,10]
objet3=[un,10]
obj2 :
stdClass Object
(
    [attr3] => deux
    [attr4] => 20
)
obj3 :
stdClass Object
(
    [attr3] => deux
    [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]=deux
obj3[attr4]=20

Comentários

  • linha 6: a notação $obj->attr refere-se ao atributo attr da variável $obj. Se não existir, é criado, tornando assim a variável $obj um objeto com atributos. Vimos que o PHP cria então um objeto stdClass por predefinição;
  • linha 16: a expressão $obj2=$obj1, quando $obj1 é um objeto, é uma cópia de objetos por referência: $obj2 e $obj1 são referências (endereços) ao mesmo objeto. O próprio objeto pode ser modificado por qualquer uma das referências;
  • linhas 23–27: visam mostrar que $obj1 e $obj2 são duas variáveis diferentes: não estão no mesmo endereço de memória:
    • $obj2 = $obj1 copia o valor de $obj1 para a variável $obj2 (passo 1 acima). O valor de $obj1 é o endereço de um objeto. Assim, $obj1 e $obj2 apontam para o mesmo objeto. Ao manipular uma variável $obj que aponta para um objeto, o PHP manipula o objeto apontado pela variável $obj. De acordo com o diagrama abaixo, podemos ver que o objeto apontado pode ser modificado tanto através de $obj1 como através de $obj2. É isto que mostram as linhas 4 e 5 dos resultados;

Image

  • linha 30: a expressão $obj3=&$obj2 faz com que $obj2 e $obj3 tenham o mesmo endereço [1 abaixo]. Poderíamos dizer que as duas variáveis são aliases para o mesmo local de memória. Ambas apontam para um objeto, o Objeto A abaixo [2];
    • A operação $obj2=new stdClass() cria um novo objeto, o Objeto B [3 abaixo], e o endereço desse novo objeto é atribuído à variável $obj2. Uma vez que $obj2 e $obj3 são dois aliases para a mesma localização na memória, $obj3 também aponta para o novo objeto, o Objeto B. Isto é demonstrado nas linhas 16–27 e 30–41 dos resultados;

Image

  • linhas 52–54: mostram que um objeto pode ser percorrido como um dicionário. As chaves do dicionário são os nomes dos atributos, e os valores do dicionário são os valores desses mesmos atributos;
  • linha 51: a função count pode ser aplicada a um objeto (com um aviso), mas não retorna, como seria de esperar, o número de atributos. Assim, um objeto partilha semelhanças com um dicionário, mas não é um;

5.3. Uma classe Pessoa sem atributos declarados

O script [classes-02.php] é o seguinte:


<?php
 
class Personne {
 
  // class attributes
  // undeclared - can be created dynamically
  // method
  function identite() {
    // a priori, uses non-existent attributes
    return "[$this->prenom,$this->nom,$this->age]";
  }
 
}
 
// test
// attributes are public and can be created dynamically
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// method call
print "personne=" . $p->identite() . "\n";
// end
exit;

Resultados:

personne=[Paul,Langevin,48]

Comentários

  • linhas 3–13: definem uma classe Person. Uma classe é um modelo utilizado para criar objetos. Contém atributos e funções denominadas métodos. Os atributos não precisam de ser declarados;
  • linhas 8–11: o método identity exibe os valores de três atributos não declarados na classe. A palavra-chave $this refere-se ao objeto ao qual o método é aplicado;
  • linha 17: criamos um objeto $p do tipo Pessoa. A palavra-chave new é utilizada para criar um novo objeto. A operação devolve uma referência ao objeto criado (ou seja, um endereço). São possíveis várias notações: new Pessoa(), new Pessoa, new pessoa. O nome da classe não distingue maiúsculas de minúsculas;
  • linhas 18–20: os três atributos exigidos pelo método identity são criados no objeto $p;
  • linha 22: o método identity da classe Person é aplicado ao objeto $p. No código (linhas 8–11) do método identity, $this refere-se ao mesmo objeto que $p;

5.4. A classe Person com atributos declarados

O script [classes-03.php] é o seguinte:


<?php
 
class Personne {
 
  // class attributes
  var $prenom;
  var $nom;
  var $age;
 
  // method
  function identite() {
    return "[$this->prenom,$this->nom,$this->age]";
  }
 
}
 
// test
// attributes are public
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// method call
print "personne=" . $p->identite() . "\n";
// end
exit;

Resultados:

personne=[Paul,Langevin,48]

Comentários

  • linhas 6–8: os atributos da classe são declarados explicitamente utilizando a palavra-chave var;

5.5. A classe Person com um construtor

Os exemplos anteriores mostraram classes Person pouco comuns, tal como poderiam ter sido encontradas no PHP 4. Não é recomendável seguir estes exemplos. Apresentamos agora uma classe Person [classes-04.php] que segue as melhores práticas do PHP 7:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
class Personne {
// class attributes
  private $prenom;
  private $nom;
  private $age;
 
// getters and setters
  public function getPrenom(): string {
    return $this->prenom;
  }
 
  public function getNom(): string {
    return $this->nom;
  }
 
  public function getAge(): int {
    return $this->age;
  }
 
  public function setPrenom(string $prenom): void {
    $this->prenom = $prenom;
  }
 
  public function setNom(string $nom): void {
    $this->nom = $nom;
  }
 
  public function setAge(int $age): void {
    $this->age = $age;
  }
 
// manufacturer
  public function __construct(string $prenom, string $nom, int $age) {
    // we go through sets
    $this->setPrenom($prenom);
    $this->setNom($nom);
    $this->setAge($age);
  }
 
// method toString
  public function __toString(): string {
    return "[$this->prenom,$this->nom,$this->age]";
  }
 
}
 
// test
// creation of a Personne object
$p = new Personne("Paul", "Langevin", 48);
// identity of this person
print "personne=$p\n";
// we change the age
$p->setAge(14);
// identity of the person
print "personne=$p\n";
// end
exit;

Resultados:

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

Comentários

  • linhas 6–50: a classe Pessoa;
  • linhas 7-9: os atributos privados da classe. Estes atributos só são visíveis dentro da classe. Outras palavras-chave que podem ser utilizadas são:
  • public: torna o atributo um atributo público visível a partir do exterior da classe,
  • protected: torna o atributo um atributo protegido, visível de dentro da classe e das suas classes derivadas;
  • uma vez que os atributos são privados, não podem ser acedidos a partir do exterior da classe. Por conseguinte, não podemos escrever o seguinte código:
$p=new Personne() ;
$p->nom="Landru" ;

Aqui, estamos fora da classe Person. Como o atributo name é privado, a linha 2 está incorreta. Para inicializar os campos privados do objeto $p, existem duas maneiras:

  • utilizar os métodos públicos set e get (os nomes destes métodos podem ser quaisquer) nas linhas 12–34. Podemos então escrever:
$p=new Personne() ;
$p->setNom("Landru") ;
  • utilize o construtor nas linhas 37–42. Escreveríamos então:
$p=new Personne("Michel","Landru",44) ;

O código acima chama automaticamente o método da classe Person chamado __construct;

  • Linha 59: Esta linha apresenta a pessoa $p como uma cadeia de caracteres. Para tal, é utilizado o método da classe Person denominado __toString (linhas 45–47);
  • Todos os métodos da classe (funções) foram prefixados com a palavra-chave public, o que indica que a função é visível fora da classe. As outras palavras-chave que podem ser utilizadas são, tal como acontece com os atributos e com o mesmo significado, private e protected. Sem um atributo de visibilidade explícito, a função tem visibilidade pública implícita;

5.6. A classe Person com verificações de validade no construtor

O construtor de uma classe é o local adequado para verificar se os valores de inicialização do objeto estão corretos. No entanto, um objeto também pode ser inicializado através dos seus métodos de definição ou equivalentes. Para evitar a duplicação das mesmas verificações em dois locais diferentes, estas verificações podem ser colocadas dentro dos métodos de definição. Se se verificar que o valor de inicialização de um objeto está incorreto, é lançada uma exceção. Aqui está um exemplo.

Primeiro, movemos a definição da classe Person para o seu próprio ficheiro [Person.php]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace;
namespace Exemples;
 
// person class
class Personne {
// class attributes
  private $prenom;
  private $nom;
  private $age;
 
// getters and setters
  public function getPrenom(): string {
    return $this->prenom;
  }
 
  public function getNom(): string {
    return $this->nom;
  }
 
  public function getAge(): int {
    return $this->age;
  }
 
  public function setPrenom(string $prénom): void {
    // first name must be non-empty
    $prénom = trim($prénom);
    if ($prénom === "") {
      throw new \Exception("Le prénom doit être non vide");
    } else {
      $this->prenom = $prénom;
    }
  }
 
  public function setNom(string $nom): void {
    // name must be non-empty
    $nom = trim($nom);
    if ($nom === "") {
      throw new \Exception("Le nom doit être non vide");
    } else {
      $this->nom = $nom;
    }
  }
 
  public function setAge(int $âge): void {
    // age must be valid
    if ($âge < 0) {
      throw new \Exception("L'âge doit être un entier positif ou nul");
    } else {
      $this->age = $âge;
    }
  }
 
// manufacturer
  public function __construct(string $prenom, string $nom, int $age) {
    // we go through sets
    $this->setPrenom($prenom);
    $this->setNom($nom);
    $this->setAge($age);
  }
 
  // method
  public function initWithPersonne(Personne $p): void {
    // initializes the current object with a person $p
    $this->__construct($p->prenom, $p->nom, $p->age);
  }
 
  // method toString
  function __toString(): string {
    return "[$this->prenom,$this->nom,$this->age]";
  }
 
}

Comentários

  • linha 4: especifica que o tipo dos parâmetros da função deve ser respeitado quando declarados;
  • linha 7: define um namespace. O nome completo (ou qualificado) da classe Person é, então, \Examples\Person. Repare no caractere \ no início do nome qualificado: isto resulta num nome qualificado absoluto. Se este caractere estiver ausente, o nome é relativo (relativo ao namespace atual). Assim, se duas classes A e B fazem parte do mesmo namespace E, no código da classe A, a classe B pode ser acedida utilizando a notação relativa B. Se a classe A faz parte do namespace E1 e B faz parte do namespace E2, no código de A, B será acedida utilizando a notação absoluta \E2\B. Definir uma classe dentro de um namespace não é obrigatório, mas o NetBeans emite um aviso se não o fizer. Por isso, vamos fazê-lo. Além disso, os namespaces devem corresponder à estrutura de diretórios dos ficheiros. Assim, a classe A no namespace E1 deve estar num ficheiro chamado E1/A.php. Isto não é obrigatório, mas, mais uma vez, o NetBeans emite um aviso se não o fizer. No exemplo da classe [\Exemples\Personne], o NetBeans emite um aviso porque o caminho do ficheiro de [Personne.php] é [exemples/classes/Personne.php] e, portanto, não corresponde ao namespace. Não confunda o caminho do ficheiro com o namespace. O nome totalmente qualificado de uma classe utiliza um namespace e não tem nada a ver com o caminho do ficheiro PHP da classe. A relação entre o caminho do ficheiro e o namespace é opcional e pode ser ignorada, como fizemos aqui;
  • linhas 12–14: os três atributos privados da classe;
  • linhas 29–37: inicialização do atributo first_name e verificação do valor de inicialização;
  • linha 31: a função trim($string) remove os espaços no início e no fim de $string. Assim, trim("abcd") é a string "abcd" e trim(" ") é a string vazia;
  • linha 32: se o primeiro nome estiver vazio, é lançada uma exceção (linha 33); caso contrário, o primeiro nome é armazenado (linha 35). Para lançar uma exceção, utilizámos aqui a classe predefinida [Exception]. Aqui, somos obrigados a utilizar o seu nome absoluto [\Exception]. Se utilizarmos o seu nome relativo [Exception], então esta classe será procurada no namespace atual, ou seja, o namespace [Examples] da classe Person. Assim, o interpretador PHP irá procurar uma classe com o nome absoluto [\Examples\Exception], que não existe;

A classe [Person] é utilizada pelo seguinte script [classes-05.php]:


<?php
 
// strict adherence to function parameter types
declare(strict_types=1);
 
// including definition of the Person class
require_once __DIR__."/Personne.php";
 
// qualified name of the Person class
use \Exemples\Personne;
 
// test
// creation of a Personne object
$p = new Personne("Paul", "Langevin", 48);
// identity of this person
print "Exemple1, personne=$p\n";
// creation of an erroneous Person object
try {
  $p = new Personne("xx", "yy", "zz");
} catch (\Exception $e1) {
  print "Exemple2, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
  print "Exemple2, erreur : " . $e2->getMessage() . "\n";
}
// creation of an erroneous Person object
try {
  $p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
  print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
  print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}
 
// end
exit;

Comentários

  • linha 7: o script irá utilizar a classe [Person]. Por isso, temos de indicar ao interpretador PHP onde pode encontrar a definição desta classe. É essa a função das instruções [include file] e [require file]. Aqui, utilizámos a instrução [include]. A diferença entre as duas instruções é a seguinte: se a instrução [include file] encontrar erros ao carregar o [file], é emitido um erro [E_WARNING], mas a execução continua; por outro lado, [require], no mesmo caso, gera um erro fatal e a execução do script é interrompida. Cada uma destas duas instruções tem uma variante: [include_once] e [require_once]. Estas duas variantes permitem-lhe lidar com casos em que o mesmo ficheiro é incluído várias vezes. Considere um projeto composto por vários scripts PHP, alguns dos quais fazem referência à classe [Person]. A sua execução fará com que o ficheiro [Person.php] seja incluído várias vezes, resultando num erro porque uma classe não pode ser definida duas vezes. A solução é utilizar as variantes [_once], que garantem que o ficheiro é incluído apenas uma vez no script global do projeto;
  • linha 7: a constante [__DIR__] é uma constante PHP que se refere ao caminho completo do diretório que contém o script com a constante [__DIR__]. Assim, a expressão na linha 17:

require_once __DIR__."/Personne.php";

será equivalente a algo como:


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

No caminho do ficheiro, pode utilizar / ou \;

  • Linha 14: Utilizamos a classe [Person] que acabámos de definir. O script [classes-05.php] não tem namespaces. A linha 14 utiliza o nome relativo da classe [Person] sem um namespace. Uma vez que a classe [Person] não tem namespace, é procurada dentro do próprio script [classes-05.php] e, por isso, não será encontrada. Existem duas soluções para este problema:
    • utilizar o nome completo da classe [\Examples\Person];
    • utilizar a instrução `use` na linha 10. Isto indica que o código que se segue utiliza a classe `[Examples\Person]`;
  • Linha 10: A instrução `use` indica ao interpretador que a classe [Person] referenciada na linha 14 é, na verdade, a classe [\Examples\Person]. Dito isto, onde é que o interpretador irá encontrar o código para esta classe? A linha 7 indica-lhe. Esta linha indica que, para executar o script atual, o script [Person.php] também deve ser carregado. Aqui, é utilizado o nome de ficheiro relativo. Por conseguinte, será procurado na pasta que contém o script [classes-05.php]. Os scripts [Person.php] e [classes-05.php] devem, portanto, estar na mesma pasta. É o que acontece aqui, onde ambos se encontram na pasta [examples/classes]. A instrução na linha 10 é equivalente a:

use \Exemples\Personne as Personne;

A instrução [use] acima especifica que o alias [Person] se refere à classe [\Exemples\Personne];

  • linha 14: é criado um objeto [Person]. O método [__construct] da classe [Person] será executado implicitamente aqui;
  • linha 16: exibe a Pessoa $p. Para ser exibido, o valor da variável $p deve ser convertido numa cadeia de caracteres. Implicitamente, o método [Person.__toString] é então executado. Este método deve, portanto, devolver uma cadeia de caracteres;
  • Vimos que o construtor da classe [Person] pode lançar uma exceção do tipo [\Exception]. Por isso, é necessário tratá-la. Consequentemente, o código na linha 14 está incompleto. Temos de utilizar o código nas linhas 18–24 para tratar adequadamente a exceção que possa ocorrer. Aqui, provocamos intencionalmente uma exceção passando uma idade que não é um número inteiro. Neste caso específico, a exceção gerada — — é lançada pelo interpretador PHP e não pelo código da classe [Person]. De facto, a assinatura do método [Person.__construct] é a seguinte:

function __construct(string $prenom, string $nom, int $age)

Portanto, o parâmetro [age] passado ao construtor deve ser do tipo inteiro. Se não for esse o caso, o interpretador PHP lança um [TypeError]. Além disso, os métodos [set] da classe [Person] lançam uma [\Exception]. Uma vez que o construtor que os chama não possui um bloco try/catch, a exceção propaga-se um nível acima para o código que chamou o construtor, ou seja, o código no script [classes-05.php]. Em última análise, o script [classes-05.php] pode receber dois tipos de exceções: \Exception ou \TypeError. Note-se que, quando o programador tem a certeza de que determinadas exceções não podem ocorrer, não utilizará os blocos catch correspondentes. Aqui, estes são utilizados sistematicamente apenas para fins de demonstração. No entanto, os blocos catch serão utilizados para todas as exceções possíveis, mesmo que improváveis;

Por esta razão, o bloco try nas linhas 18–24 tem dois blocos catch para tratar os dois tipos de exceção separadamente;

  • Linha 20: Pode escrever [Exception] ou [\Exception]:
    • A primeira versão utiliza o nome relativo da classe, relativo ao namespace do script. O script não possui namespace. O seu namespace é, portanto, a raiz de todos os namespaces: \. Assim, escrever [Exception] aqui é equivalente a escrever [\Exception]. E a classe [Exception] está, de facto, localizada no namespace [\];

É preferível utilizar os nomes absolutos das exceções predefinidas do PHP num script que não tenha um namespace próprio. Assim, se decidir atribuir um namespace a este script, escrever os nomes absolutos das classes continua a ser válido, enquanto que, no outro caso, alterar o namespace causará erros com os nomes relativos das classes;

  • Linha 21: Quando ocorre uma exceção, o método [Exception→getMessage] recupera a mensagem de erro da exceção. O mesmo se aplica a um [TypeError]. No método [Person.setFirstName], escrevemos:

  public function setPrenom(string $prénom) {
    // le prénom doit être non vide
    $prénom = trim($prénom);
    if ($prénom === "") {
      throw new \Exception("Le prénom doit être non vide");
    } else {
      $this->prenom = $prénom;
    }
}

Na linha 5, é lançada uma exceção com a mensagem de erro [O nome próprio não pode estar vazio]. É isto que o método [Exception→getMessage] na linha 29 do script [classes-05.php] irá recuperar.

Resultados:


Exemple1, personne=[Paul,Langevin,48]
Exemple2, erreur : Argument 3 passed to Exemples\Personne::__construct() must be of the type integer, string given, called in C:\Data\st-2019\dev\php7\php5-exemples\exemples\exemple_18.php on line 19
Exemple3, erreur : Le prénom doit être non vide

5.7. Adicionar um método que atua como um construtor secundário

No PHP 7, não é possível ter múltiplos construtores com parâmetros diferentes que permitam que um objeto seja construído de várias maneiras. Podemos, portanto, usar métodos que atuam como construtores:


  // méthode
  public function initWithPersonne(Personne $p) {
    // initialise l'objet courant avec une personne $p
    $this->__construct($p->prenom, $p->nom, $p->age);
}

Comentários

  • linhas 2-5: o método initWithPerson permite que os valores dos atributos de outro objeto Person sejam atribuídos ao objeto atual. Aqui, ele chama o construtor __construct, mas isso não é obrigatório. Poderia inicializar os atributos da própria classe [Person];

A nova classe [Person] é utilizada pelo seguinte script [classes-06.php]:


<?php
 
// including definition of the Person class
require_once __DIR__."/Personne.php";
// declaration of the qualified name of the Personne class
use \Exemples\Personne;
 
// test
// creation of a Personne object
try {
  $p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
// identity of this person
print "personne=$p\n";
// creation of a second person
try {
  $p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
// initialization of the first with the values of the second
try {
  $p->initWithPersonne($p2);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
 
// check
print "personne=$p\n";
// end
exit;
  • Linhas 14, 23, 30: É comum que, após uma exceção, seja necessário interromper a execução de um script de consola se o erro encontrado for irrecuperável. Este não é o caso num script web: não interrompemos a execução do script, mas exibimos uma página de erro. Se estivermos dentro de uma função, não usamos a instrução exit, mas sim return: não interrompemos a execução do script (exit), mas saímos da função (return) após definir um erro;

Resultados:

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

5.8. Uma matriz de objetos [Person]

O exemplo seguinte [classes-07.php] mostra que é possível ter matrizes de objetos.


<?php
 
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
 
// test
// create an array of Personne objects
// to make the code easier to understand, we don't handle the possible exception
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];
 
// identity of these persons
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
 
// end
exit;

Resultados:

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

Comentários

  • linha 9: criação de uma matriz com 2 pessoas;
  • linha 12: iteração pelo array;
  • linha 13: $group[$i] é um objeto do tipo Pessoa. O método [Pessoa.__toString] é usado para o exibir;

5.9. Criação de uma classe derivada da classe Pessoa

Criamos a seguinte classe [Teacher] num ficheiro chamado [Teacher.php]:


<?php
 
// strict adherence to declared function parameter types
declare (strict_types=1);
 
// namespace
namespace Exemples;
 
// a class derived from personne
class Enseignant extends Personne {
  // attributes
  private $discipline;   // subject taught
 
  // getter and setter
 
  public function getDiscipline(): string {
    return $this->discipline;
  }
 
  public function setDiscipline(string $discipline): void {
    $this->discipline = $discipline;
  }
 
  // manufacturer
  public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
    // parent attributes
    parent::__construct($prénom, $nom, $âge);
    // other attributes
    $this->setDiscipline($discipline);
  }
 
  // overloading the __toString function of the parent class
  public function __toString(): string {
    return "[" . parent::__toString() . ",$this->discipline]";
  }
 
}

Comentários

  • linha 7: a classe [Teacher] também faz parte do namespace [Examples];
  • linha 10: a classe Teacher estende a classe Person. A classe derivada Teacher herda os atributos e métodos da sua classe pai;
  • linha 12: a classe Teacher adiciona um novo atributo, subject, que é específico dela;
  • linha 25: o construtor da classe Teacher recebe 4 parâmetros:
    • 3 para inicializar a sua classe pai (first_name, last_name, age), linha 27;
    • 1 para a sua própria inicialização (subject), linha 29;
  • linha 27: a classe derivada tem acesso aos métodos e construtores da sua classe pai através da palavra-chave parent::. Aqui, passamos os parâmetros (first_name, last_name, age) para o construtor da classe pai;
  • linhas 33–35: o método __toString da classe derivada utiliza o método __toString da classe pai;

A classe [Teacher] é utilizada pelo seguinte script [classes-08.php]:


<?php
 
// inclusion of the definition of the two classes
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// declaration of the two classes used
use \Exemples\Personne;
use \Exemples\Enseignant;
 
// test
// creation of an array of Personne and derived objects
// for the simplicity of the example, we don't handle exceptions
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));
 
// identity of these persons
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
// end
exit;

Comentários

  • linhas 4-5: precisamos indicar ao interpretador PHP onde as duas classes [Teacher, Person] estão localizadas;
  • linhas 7-8: declaração dos nomes completos das duas classes. Isto permitirá que nos referimos a elas no código simplesmente pelos seus nomes, sem o sufixo do namespace;
  • linha 13: criamos uma matriz contendo um tipo [Person] e um tipo [Teacher];
  • linhas 16-18: exibimos os elementos da matriz;
  • linha 17: o método __toString de cada elemento $group[$i] será chamado. A classe Person tem um método __toString. A classe Teacher tem dois: o da sua classe pai e o seu próprio. Poder-se-á perguntar qual deles será chamado. A execução mostra que foi o da classe Teacher que foi chamado. É sempre assim: quando um método é chamado num objeto, ele é procurado na seguinte ordem: no próprio objeto, na sua classe pai, se tiver uma, depois na classe pai da classe pai, e assim por diante… A pesquisa pára assim que o método é encontrado.

Resultados:

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

5.10. Criação de uma segunda classe derivada da classe Pessoa

O exemplo seguinte cria uma classe Student derivada da classe Person no ficheiro [Student.php]:


<?php
 
// strict adherence to declared types of function parameters
declare (strict_types=1);
 
// namespace
namespace Exemples;
 
class Etudiant extends Personne {
  // attributes
  private $formation;   // training pursued
 
  // getter and setter
  public function getFormation(): string {
    return $this->formation;
  }
 
  public function setFormation(string $formation): void {
    $this->formation = $formation;
  }
 
  // manufacturer
  public function __construct(string $prénom, string $nom, int $âge, string $formation) {
    // parent attributes
    parent::__construct($prénom, $nom, $âge);
    // other attributes
    $this->setFormation($formation);
  }
 
  // overloading the __toString function of the parent class
  public function __toString(): string {
    return "[" . parent::__toString() . ",$this->formation]]";
  }
 
}

Esta classe é utilizada pelo seguinte script [classes-09.php]:


<?php
 
// inclusion and definition of classes used by the script
require_once __DIR__."/Personne.php";
use \Exemples\Personne;
require_once __DIR__."/Enseignant.php";
use \Exemples\Enseignant;
require_once __DIR__."/Etudiant.php";
use \Exemples\Etudiant;
 
// test
// creation of a table of person objects and derivatives
// to make the example easier to understand, we don't handle exceptions
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));
 
// identity of these persons
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
// end
exit;

Resultados:


groupe[0]=[[Paul,Langevin,48],anglais]
groupe[1]=[Sylvie,Lefur,70]
groupe[2]=[[Steve,Boer,23],iup2 qualité]

5.11. Relação entre o construtor de uma classe derivada e o da classe pai

Em algumas linguagens orientadas para objetos, o construtor de uma classe derivada chama automaticamente o da sua classe pai. O código seguinte [classes-16.php] mostra que este não é o caso no PHP 7:


<?php
 
class Classe1 {
 
  // manufacturer
  public function __construct() {
    print "constructeur de la classe Classe1\n";
  }
 
}
 
class Classe2 extends Classe1 {

  // manufacturer
  public function __construct() {
    // the constructor of the parent class is not called implicitly
    print "constructeur de la classe Classe2\n";
  }
 
}
 
class Classe3 extends Classe1 {
 
  // manufacturer
  public function __construct() {
    // explicit call to parent class constructor
    parent::__construct();
    // code specific to Classe3
    print "constructeur de la classe Classe3\n";
  }
 
}
 
// tests
print "test1---------\n";
new Classe2();
print "test2---------\n";
new Classe3();

Resultados

1
2
3
4
5
test1---------
constructeur de la classe Classe2
test2---------
constructeur de la classe Classe1
constructeur de la classe Classe3

5.12. Sobrescrever um método da classe pai

Já vimos que um método da classe pai pode ser reescrito numa classe filha. Assim, o método [__toString] da classe [Person] (ver link) foi reescrito nas classes filhas [Teacher] (ver link) e [Student] (ver link). O script [classes-13.php] ilustra novamente este conceito:


<?php
 
// strict adherence to function parameter types
declare(strict_types=1);
 
// main class
class Classe1 {
 
  public function f(): int {
    return 1;
  }
 
  function g(): int {
    return 2;
  }
 
}
 
// derived class
class Classe2 extends Classe1 {
 
  // redefine the f function of the parent class
  public function f(): int {
    return parent::f() + 10;
  }
 
}
 
// code
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c1=new Classe1();
print $c1->f()."\n";

Comentários

  • linhas 7–17: a classe [Class1] define dois métodos, f e g;
  • linhas 20–27: a classe [Class2] estende a classe [Class1] e redefine o seu método f;

Resultados

1
2
3
11
2
1

Comentários

  • A linha 30 do código cria um objeto $c2 do tipo [Class2];
  • A linha 31 do código chama o método f do objeto $c2. Como este método existe, é executado;
  • A linha 32 do código chama o método g do objeto $c2. Como este método não existe, é procurado na classe pai, onde é encontrado e executado;
  • A linha 33 do código cria um objeto $c1 do tipo [Class1];
  • A linha 34 do código chama o método f do objeto $c1. Como este método existe, é executado;

5.13. Passar um objeto como parâmetro de função

Considere o seguinte script [classes-14.php]:


<?php
 
// strict adherence to function parameter types
declare(strict_types=1);
 
// main class
class Classe1 {
 
  public function f(): int {
    return 1;
  }
 
  function g(): int {
    return 2;
  }
 
}
 
// derived class
class Classe2 extends Classe1 {
 
  // redefine the f function of the parent class
  public function f(): int {
    return parent::f() + 10;
  }
 
}
 
// the function parameter is of type Class1 or derived
function doSomething(Classe1 $c1): void {
  print $c1->f() + $c1->g() . "\n";
}
 
// code
// create a Class2 object derived from Class1
$c2 = new Classe2();
// we call doSomething with
doSomething($c2);

Comentários

  • linhas 7–17: a classe [Class1];
  • linhas 20–27: uma classe [Class2] derivada de [Class1];
  • linha 30: uma função que espera um parâmetro do tipo [Class1]. Quando o tipo esperado é uma classe, o parâmetro real pode ser um objeto do tipo esperado ou de um tipo derivado;
  • linhas 35–38: a função [doSomething] é chamada com um parâmetro do tipo [Class2], mesmo que o tipo esperado seja [Class1];

Resultados

13

5.14. Classes abstratas

Uma classe abstrata é uma classe incompleta que não pode ser instanciada. Para ser utilizável, deve ser derivada de outra classe.

Para que serve uma classe abstrata? Por vezes, temos classes que partilham um ou mais métodos, mas diferem noutros métodos ou atributos. Nesse caso, é aconselhável agrupar tudo o que é comum numa classe pai. Por enquanto, não precisamos de uma classe abstrata. Mas suponhamos que as classes filhas diferem apenas num método, M: a assinatura do método seria a mesma em todas as classes filhas, mas a sua implementação seria diferente. Para exigir que as classes filhas implementem o método M:

  • Vamos declarar a assinatura do método M na classe pai. Uma vez que a classe pai não sabe como implementá-lo, antepomos a palavra-chave «abstract» ao método: isto significa que a implementação do método M é adiada para as classes filhas;
  • como a classe pai não está totalmente implementada, ela também é declarada como abstract utilizando a mesma palavra-chave abstract. Isto significa que a classe já não pode ser instanciada. É necessário criar uma classe filha para definir a implementação do método M, para que o corpo da classe pai possa ser utilizado;

Aqui está um exemplo [classes-15.php]:


<?php
 
// strict adherence to function parameter types
declare(strict_types=1);
 
// abstract main class
abstract Class Classe1 {
 
  // method known to all derived classes
  public function f(): int {
    return 1;
  }
 
  // abstract g method - will be defined by derived classes
  abstract function g(): int;
}
 
// derived class
Class Classe2 extends Classe1 {
 
  // the g method of the parent class must be defined
  public function g(): int {
    return parent::f() + 10;
  }

}
 
// derived class
Class Classe3 extends Classe1 {
 
  // the g method of the parent class must be defined
  public function g(): int {
    return parent::f() + 20;
  }
 
  // we can redefine the f method of the parent class
  public function f(): int {
    return 2;
  }
 
}
 
// code
$c2 = new Classe2();
print $c2->f() . "\n";
print $c2->g() . "\n";
$c3 = new Classe3();
print $c3->f() . "\n";
print $c3->g() . "\n";

Comentários

  • linhas 7–16: a classe [Class1] é abstrata (linha 7) porque não sabe como implementar o método g na linha 15. Por isso, deve ser derivada para poder ser utilizada;
  • linhas 19–26: a classe [Class2] estende a classe [Class1] e redefine o método g da sua classe pai (linhas 22–24);
  • linhas 29–41: a classe [Class3] estende a classe [Class1] e redefine o método g da sua classe pai (linhas 32–34);
  • linhas 37–39: a classe [Class3] redefine o método f da sua classe pai;
  • linhas 44–49: são criados dois objetos dos tipos [Class2] e [Class3], e os seus métodos f e g são chamados;

Resultados

1
2
3
4
1
11
2
21

5.15. Classes finais

Uma classe final é uma classe da qual não é possível derivar outras classes. Considere o seguinte script [classes-11.php]:


<?php
 
// namespace
namespace Exemples;
 
// non-derivable class
final Class Classe1 {
  
}
 
// derived class
Class Classe2 extends Classe1 {
  
}
 
// code - must cause an error
new Classe2();

Comentários

  • linhas 7–9: a palavra-chave final torna a classe [Class1] uma classe final da qual não é possível derivar;
  • linhas 12-14: a classe [Class2] estende a classe final [Class1], o que constitui um erro;
  • linha 17: o erro só será reportado quando o script for executado e for feita uma tentativa de manipular um objeto do tipo [Class2];

Resultados

Fatal error: Class Exemples\Classe2 may not inherit from final class (Exemples\Classe1) in C:\Data\st-2019\dev\php7\php5-exemples\exemples\classes\classes-11.php on line 14

5.16. Métodos finais

Um método final é um método que não pode ser sobreposto por subclasses. Aqui está um exemplo [classes-12.php]:


<?php
 
// strict adherence to function parameter types
declare(strict_types=1);
 
// namespace
namespace Exemples;
 
// main class
Class Classe1 {
 
  // this method cannot be redefined in a derived class
  public final function f(): int {
    return 1;
  }
 
}
 
// derived class
Class Classe2 extends Classe1 {
 
  public function f(): int {
    return 2;
  }
 
}
 
// code - must cause an error
new Classe2();

Comentários

  • linha 13: o método f da classe [Class1] é declarado como final utilizando a palavra-chave final;
  • linha 20: a classe [Class2] estende a classe [Class1];
  • linhas 22-23: a função f da classe pai [Class1] é redefinida. Isto deve causar um erro;
  • linha 29: é criado um objeto do tipo [Class2] para forçar o interpretador PHP a inspecionar a classe [Class2];

Resultados

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

5.17. Métodos e atributos estáticos

Um método estático é um método associado à classe na qual está definido, e não aos objetos de instância da classe. Assim, se a classe C declarar um método estático M, para o utilizar escreveríamos:

  • C::M se estiver fora da classe;
  • self::M se estivermos dentro da classe;

Aqui está um exemplo [classes-17.php]:


<?php
 
class Classe1 {
 
  // static method
  static function say(string $message): void {
    print "$message\n";
  }
 
}
 
// test -------------------
Classe1::say("hello");

Comentários

  • linha 6: o método [say] é declarado como estático utilizando a palavra-chave static;
  • linha 13: chamada ao método estático [say] utilizando a sintaxe: Class1::say;

Resultados

hello

Agora considere o seguinte código [classes-18.php]:


<?php
 
class Classe1 {
  // static attribute
  private static $nbObjects = 0;
 
  public function __construct() {
    print "constructeur Classe1\n";
    self::$nbObjects++;
  }
 
  // static method
  static function say(): void {
    print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
  }
 
}
 
// test -------------------
new Classe1();
new Classe1();
Classe1::say();

Comentários

  • Linha 5: Declaramos um atributo estático que contará o número de instâncias da classe [Class1] que foram criadas. Este não é um atributo que possa pertencer a uma instância da classe. Na verdade, se forem criados dois objetos, O1 e O2, nenhum deles tem conhecimento do outro. Ter um contador dentro da instância não faz sentido: quando um novo objeto é criado, em qual instância incrementaríamos o contador? Seríamos obrigados a incrementar o contador de um objeto específico, ignorando os contadores das outras instâncias. Um atributo estático é um atributo da classe, não um atributo de instância da classe;
  • linhas 7–10: é no construtor que contaremos os objetos criados, uma vez que a criação de cada novo objeto desencadeia a execução do construtor;
  • linha 14: Observe a notação `self::$nbObjects` para indicar que nos estamos a referir a um atributo estático da classe na qual o código executado reside;
  • linhas 13–15: o método estático [say] é responsável por exibir o número de objetos criados;
  • linhas 20–22: criamos dois objetos e exibimos o contador de objetos;

Resultados


constructeur Classe1
constructeur Classe1
2 objets de type [Classe1] ont été construits

5.18. Visibilidade entre a classe pai e a classe filha

Vamos examinar o seguinte script [classes-19.php]:


<?php
 
class SomeParent {
  // attribute
  private $attributeOfParent = 4;
 
  // method
  public function doTest(): void {
    // who's 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 $attributeOfChild = 14;
 
  // method
  public function doTest(): void {
    // children's 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();

Comentários

  • linhas 3–17: a classe [SomeParent];
  • linhas 19–32: a classe filha [SomeChild]. Podemos ver que ela estende a classe [SomeParent] (linha 19);
  • linha 5: a classe [SomeParent] tem apenas um atributo;
  • linhas 8–17: o método [SomeParent::doTest] destina-se a exibir dois atributos:
    • [$attributeOfParent], que pertence à classe [SomeParent];
    • [$attributeOfChild], que pertence à classe [SomeChild] (linha 21);
  • linhas 10–11: a identidade do chamador é exibida; o método será chamado de duas maneiras diferentes:
    • a partir da classe pai [SomeParent];
    • a partir da classe filha [SomeChild];
  • linhas 13–14: exibição dos dois atributos;
  • linhas 19–32: a classe filha [SomeChild] que estende a classe [SomeParent] (linha 19);
  • linha 21: a classe [SomeChild] tem apenas um atributo;
  • linha 24: o método [SomeChild::doTest] destina-se a exibir dois atributos:
    • [$attributeOfParent], que pertence à classe [SomeParent];
    • [$attributeOfChild], que pertence à classe [SomeChild];
  • linhas 26–27: exibição dos dois atributos;
  • linha 30: chama o método [doTest] da classe pai, que, por sua vez, exibe os dois atributos;
  • linha 36: o método [SomeParent::doTest] é chamado;
  • linha 38: o método [SomeChild::doTest] é chamado;

No primeiro teste, a visibilidade de ambos os atributos é [private]. Podemos, portanto, esperar que a classe filha não veja o atributo da sua classe pai. Este atributo teria de ter, pelo menos, visibilidade [protected]. Mas e quanto ao atributo da classe filha? É visível na classe pai?

Eis os resultados deste primeiro teste:

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

Comentários

  • linhas 1-10: resultados do primeiro teste em que o método [SomeParent::doTest] é chamado;
  • linhas 3-6: vemos que o objeto que chama o método é do tipo [SomeParent];
  • linha 7: exibição do atributo [$attributeOfParent];
  • linhas 9-10: vemos que o atributo [SomeParent::$attributeOfChild] não existe. Por isso, não é exibido;
  • linhas 11–30: resultados do segundo teste em que o método [SomeChild::doTest] é chamado;
  • linhas 13–14: vemos que o atributo [SomeChild::$attributeOfParent] não existe. Por isso, não é exibido. Isto é normal: o atributo [SomeParent::$attributeOfParent] é [private] e, portanto, não é conhecido na classe filha;
  • linha 15: exibição do atributo [$attributeOfChild];
  • linhas 16–30: estamos no método [SomeParent::doTest] chamado pela classe filha;
  • linhas 17–22: vemos que [$this] é do tipo [SomeChild] com dois atributos privados;
  • linha 23: surpreendentemente, [$this] do tipo [SomeChild] consegue ver aqui o atributo do pai [$attributeOfParent];
  • linhas 25–30: igualmente surpreendente, [$this] do tipo [SomeChild] não vê o seu próprio atributo [$attributeOfChild];

Este resultado é muito surpreendente: embora as linhas 17–21 indiquem que [$this] é do tipo [SomeChild], este [$this] dentro do método [SomeParent::doTest] comporta-se como se fosse uma instância da classe [SomeParent] e não da classe [SomeChild].

Vamos executar um novo teste com o script [classes-20.php]. O atributo [$attributeOfParent] tem agora visibilidade [protected] (linha 5):


<?php
 
class SomeParent {
  // attribute
  protected $attributeOfParent = 4;
 
  // method
  public function doTest(): void {
    // who's 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 $attributeOfChild = 14;
 
  // method
  public function doTest(): void {
    // children's 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();

Resultados

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

Comentários

  • linha 12: a classe [SomeChild] agora acede ao atributo do seu pai [$attributeOfParent]. Isto é normal, uma vez que o atributo tem agora um âmbito [protected];
  • no método [someParent::doTest], o objeto [$this] é do tipo [SomeChild] (linhas 15–20). Ele vê o atributo do seu pai [$attributeOfParent] (linha 21), mas ainda não o seu próprio atributo [$attributeOfChild] (linhas 23–28);

No terceiro teste, o atributo [$attributeOfChild] também tem um escopo [protected]:


<?php
 
class SomeParent {
  // attribute
  protected $attributeOfParent = 4;
 
  // method
  public function doTest(): void {
    // who's 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 $attributeOfChild = 14;
 
  // method
  public function doTest(): void {
    // children's 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();

Os resultados da execução são os seguintes:

---------------------------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
  • Linha 22: Desta vez, dentro do pai, [$this] do tipo [SomeChild] (linhas 15–20) vê o atributo protegido [$attributeOfChild] da sua própria classe [SomeChild].

O que podemos aprender com estes testes?

  • que a instância [$this] de uma classe pai, utilizada num método da classe pai, vê:
    • os atributos e métodos da classe pai, independentemente da sua visibilidade;
    • não vê nenhum dos atributos e métodos das suas classes filhas;

Este é o comportamento esperado.

  • que a instância [$this] de uma classe filha, utilizada num método da classe filha, vê:
    • os atributos e métodos da classe pai, se estes tiverem, pelo menos, visibilidade [protected]. Aqueles com visibilidade [private] não são visíveis;
    • os atributos e métodos da classe filha, independentemente da sua visibilidade;

Este é o comportamento esperado.

  • que a instância [$this] de uma classe filha, utilizada num método da classe pai, veja:
    • os atributos e métodos da classe pai, independentemente da sua visibilidade;
    • os atributos e métodos da sua própria classe apenas se tiverem, no mínimo, visibilidade [protected]. Aqueles com visibilidade [private] não são visíveis;

Este é um comportamento inesperado.

5.19. Codificação JSON de uma classe

Numa classe, o método [__toString] está frequentemente presente: é suposto que devolva uma cadeia de caracteres que represente o objeto que o invoca. Pode ser tentador que esta cadeia de caracteres seja uma cadeia JSON. Vamos explorar esta possibilidade agora.

Image

Vamos utilizar a seguinte classe [Person]:


<?php
 
class Personne {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;
 
  // setter global
  public function setFromArray(array $arrayOfAttributes): Personne {
    // initialization of certain class attributes
    foreach ($arrayOfAttributes as $attribute => $value) {
      $this->$attribute = $value;
    }
    // object is returned
    return $this;
  }
 
  // getters
  public function getNom() {
    return $this->nom;
  }
 
  public function getPrénom() {
    return $this->prénom;
  }
 
  public function getÂge() {
    return $this->âge;
  }
 
  public function getEnfants() {
    return $this->enfants;
  }
 
  // __toString
  public function __toString(): string {
    // we identify the object
    var_dump($this);
    // retrieve its attributes
    $attributes = \get_object_vars($this);
    var_dump($attributes);
    // render the jSON attribute string
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }
 
}

Comentários

  • linhas 5–8: os quatro atributos da classe;
  • linhas 20–35: os getters que permitem recuperar os valores desses atributos;
  • linhas 11–18: um setter global que inicializa os atributos a partir de um array associativo [$arrayOfAttributes] cujas chaves correspondem aos atributos da classe;
  • linhas 38–46: o método [__toString] da classe;
  • linha 42: a função PHP [get_object_vars] recupera os valores dos atributos da classe como uma matriz associativa [‘name’=>’name1’, ‘first_name’=>’first_name1’, ‘age’=>’age1’, ‘children’=>[]];
  • linha 45: devolvemos a cadeia JSON desta matriz de atributos;

Vamos examinar o script [json-01.php] que utiliza a classe [Person]:


<?php
 
// person class
require "Personne.php";
 
// father instantiation
$père = new Personne();
// father initialization
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// instantiation and initialization child1
$enfant1 = (new Personne())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 17
  ]);
// instantiation and initialization child2
$enfant2 = (new Personne())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// father's children initialization
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);
 
// display father elements
$enfant1=($père->getEnfants())[0];
$enfant2=($père->getEnfants())[1];
print "------------------------enfant1\n";
print "enfant1=$enfant1\n";
print "------------------------enfant2\n";
print "enfant2=$enfant2\n";
print "------------------------père\n";
print "père=$père\n";

Comentários

  • linhas 6-13: inicializamos um objeto [Person] [$father] utilizando o método [Person::setFromArray], que nos permite inicializar um objeto [Person] com uma matriz cujas chaves correspondem aos atributos da classe [Person];
  • linhas 14-19: inicializamos um objeto [Person] [$child1] da mesma forma;
  • linhas 21–25: inicializamos um objeto [Person] [$child2];
  • linhas 27–29: o atributo [$father→children] é inicializado com uma matriz dos dois filhos;
  • linhas 32–33: atribuímos os dois filhos ao pai;
  • linha 35: a operação [print] tenta converter o objeto [$child1] numa string. Para isso, utiliza o método [__toString] do objeto. Esperamos, portanto, ver a string JSON do objeto;
  • linhas 38–39: fazemos o mesmo com o pai;

Os resultados são os seguintes:


------------------------enfant1
object(Personne)#2 (4) {
  ["nom":"Personne":private]=>
  string(11) "Bertholomé"
  ["prénom":"Personne":private]=>
  string(7) "Sylvain"
  ["âge":"Personne":private]=>
  int(17)
  ["enfants":"Personne":private]=>
  NULL
}
array(4) {
  ["nom"]=>
  string(11) "Bertholomé"
  ["prénom"]=>
  string(7) "Sylvain"
  ["âge"]=>
  int(17)
  ["enfants"]=>
  NULL
}
enfant1={"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null}
------------------------enfant2
object(Personne)#3 (4) {
  ["nom":"Personne":private]=>
  string(11) "Bertholomé"
  ["prénom":"Personne":private]=>
  string(10) "Géraldine"
  ["âge":"Personne":private]=>
  int(12)
  ["enfants":"Personne":private]=>
  NULL
}
array(4) {
  ["nom"]=>
  string(11) "Bertholomé"
  ["prénom"]=>
  string(10) "Géraldine"
  ["âge"]=>
  int(12)
  ["enfants"]=>
  NULL
}
enfant2={"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}
------------------------père
object(Personne)#1 (4) {
  ["nom":"Personne":private]=>
  string(11) "Bertholomé"
  ["prénom":"Personne":private]=>
  string(10) "Dieudonné"
  ["âge":"Personne":private]=>
  int(58)
  ["enfants":"Personne":private]=>
  array(2) {
    [0]=>
    object(Personne)#2 (4) {
      ["nom":"Personne":private]=>
      string(11) "Bertholomé"
      ["prénom":"Personne":private]=>
      string(7) "Sylvain"
      ["âge":"Personne":private]=>
      int(17)
      ["enfants":"Personne":private]=>
      NULL
    }
    [1]=>
    object(Personne)#3 (4) {
      ["nom":"Personne":private]=>
      string(11) "Bertholomé"
      ["prénom":"Personne":private]=>
      string(10) "Géraldine"
      ["âge":"Personne":private]=>
      int(12)
      ["enfants":"Personne":private]=>
      NULL
    }
  }
}
array(4) {
  ["nom"]=>
  string(11) "Bertholomé"
  ["prénom"]=>
  string(10) "Dieudonné"
  ["âge"]=>
  int(58)
  ["enfants"]=>
  array(2) {
    [0]=>
    object(Personne)#2 (4) {
      ["nom":"Personne":private]=>
      string(11) "Bertholomé"
      ["prénom":"Personne":private]=>
      string(7) "Sylvain"
      ["âge":"Personne":private]=>
      int(17)
      ["enfants":"Personne":private]=>
      NULL
    }
    [1]=>
    object(Personne)#3 (4) {
      ["nom":"Personne":private]=>
      string(11) "Bertholomé"
      ["prénom":"Personne":private]=>
      string(10) "Géraldine"
      ["âge":"Personne":private]=>
      int(12)
      ["enfants":"Personne":private]=>
      NULL
    }
  }
}
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{},{}]}

Comentários

  • linhas 2–11: o objeto [$child1];
  • linhas 12-21: o conjunto de atributos do objeto [$child1]. Temos todos eles;
  • linha 22: temos a cadeia JSON para o objeto [$child1];
  • linhas 23–44: o mesmo para o objeto [$child2];
  • Linhas 45–112: Para o pai, é um pouco diferente porque o seu atributo [children] não é NULL, como era para os filhos;
  • linha 112: vemos que os filhos estão em falta na cadeia JSON do pai;
  • linhas 79–111: vemos que, na matriz de atributos do pai, o filho 1 (linhas 89–98) continua a ser um objeto, tal como o filho 2 (linhas 99–110). Em resumo, a expressão [\get_object_vars($this)], onde [$this] representa o pai, não é recursiva: se um atributo da classe [Person] for ele próprio um objeto, a expressão [\get_object_vars($this)] não tenta recuperar a sua matriz de atributos;

Podemos melhorar isto. Modificamos a classe [Person] para a seguinte classe [Person2]:


<?php
 
class Personne2 {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;
 
  // setter global
  public function setFromArray(array $arrayOfAttributes): Personne2 {

    // object is returned
    return $this;
  }
 
  // getters
  public function getNom() {
    return $this->nom;
  }
 

 
  // __toString
  public function __toString(): string {
    // retrieve the object's attributes
    $attributes = $this->getAttributes($this);
    $enfants = $attributes["enfants"];
    if ($enfants != NULL) {
      $attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
    }
    // render the JSON attribute string
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }
 
  public function getAttributes(): array {
    return \get_object_vars($this);
  }
 
}

Comentários

  • linhas 36–38: a função [getAttributes] devolve o conjunto de atributos do objeto que a invoca;
  • linhas 25–34: a função [__toString];
  • Linha 27: recuperamos os atributos da classe [Person] para a matriz [$attributes];
  • linha 28: com base no exemplo anterior, sabemos que [$attributes["children"]] é uma matriz de dois objetos do tipo [Person];
  • linhas 29–31: os dois objetos são substituídos pela sua matriz de atributos;
  • linha 33: resta apenas codificar a matriz de atributos construída em JSON;

O script [json-02.php] utiliza a classe [Person2] da seguinte forma:


<?php
 
// person2 class
require "Personne2.php";

// father instantiation
$père = new Personne2();
// initialization
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// instantiation and initialization child1
$enfant1 = (new Personne2())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 17
  ]);
// instantiation and initialization child2
$enfant2 = (new Personne2())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// father's children initialization
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);
 
// father display
print "------------------------père\n";
print "père=$père\n";

O script [json-02.php] é idêntico ao script [json-01.php], exceto que a classe [Person2] substituiu a classe [Person].

Os resultados da execução são os seguintes:

------------------------père
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{"nom":"Bertholomé","prénom":"Sylvain","âge":17,"enfants":null},{"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}]}

Desta vez, conseguimos obter os filhos juntamente com o pai.

A solução anterior é insatisfatória porque os filhos podem ter os seus próprios filhos. Encontramos então o problema anterior.

A classe [Person3] resolve este problema da seguinte forma:


<?php
 
class Personne3 {
  // attributes
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;
 
  // setter global
  public function setFromArray(array $arrayOfAttributes): Personne3 {

  }
 
  // getters

 
  // __toString
  public function __toString(): string {
    // render the JSON attribute string
    $attributes = [];
    $this->getRecursiveAttributes($attributes, $this, []);
    // string JSON 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 {
    // value analysis [$value]
    // $keys is an array [key1, key2, .., keyn]
    // $value=$attributes[key1][key2]….[keyn]
    // if [$value] is an object, we use its method [getAttributes]
    if (\is_object($value)) {
      // attributs de l'objet [$value]
      $objectAttributes = $value->getAttributes();
      // what do we do with the result?
      if ($keys) {
        // in [$attributes], we replace $value with the array of its attributes
        // element $attributes[key1][key2]...[keyn] must be constructed
        // where $keys is the array [key1, key2, .., keyn]
        // we take the table reference [$attributes]
        $attribute = &$attributes;
        // scan the key table
        foreach ($keys as $key) {
          // we take the reference of the
          $attribute = &$attribute[$key];
        }
        // here $attribut and $attributes[key1][key2]...[key(n)] are identical
        // they share the same memory location
        // object [$value] is replaced by its array of attributes;
        // write $attributes[key1][key2]...[keyn]=$objectAttributes
        // which is equivalent to $attribute = $objectAttributes
        $attribute = $objectAttributes;
      } else {
        // no keys - we're just beginning to explore the object
        // $objectAttributes represents the 1st level attributes of the class
        $attributes += $objectAttributes;
      }
      // maybe in [$objectAttributes] there are still objects
      // explore [$objectAttributes] attributes
      $this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
    } else {
      if (\is_array($value)) {
        // we have a table - we analyze each of its elements
        foreach ($value as $key => $élément) {
          // add the current key to the $keys array
          \array_push($keys, $key);
          // we analyze $élément
          $this->getRecursiveAttributes($attributes, $élément, $keys);
          // remove the key just analyzed from the $keys array
          \array_pop($keys);
        }
      }
    }
  }

Comentários

  • linhas 21–22: desta vez, o método [__toString] recupera os atributos da sua classe e solicita que isto seja feito de forma recursiva: se um atributo for um objeto ou uma matriz de objetos, então cada objeto deve ser substituído pela sua matriz de atributos na matriz final de atributos da classe;
  • linhas 31-78: a função [getRecursiveAttributes] realiza este trabalho. Comentámos o código. Escrever uma função recursiva é frequentemente complexo. É o caso aqui. O leitor não perderá nada se não compreender. Existem bibliotecas que tratam desta tarefa. As chamadas recursivas ocorrem nas linhas 64 e 72;
  • a vantagem deste código é que não foi escrito exclusivamente para a classe [Person3]. Aplica-se a qualquer classe com atributos cujos valores sejam de tipos de objeto diferentes, desde que as classes utilizadas pela classe principal tenham, tal como ela, o método [getAttributes] nas linhas 27–29

O script [json-03.php] utiliza a classe [Person3] da seguinte forma:


<?php
 
// person3 class
require "Personne3.php";
 
// father instantiation
$père = new Personne3();
// initialization
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// instantiation and initialization child1
$enfant1 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 27
  ]);
// instantiation and initialization child2
$enfant2 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// father's children initialization
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);
// instantiation and initialization child11
$enfant11 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Gaëtan",
  "âge" => 2
  ]);
// instantiation and initialization child12
$enfant12 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Mathilde",
  "âge" => 1
  ]);
// initialization children of child1
$enfant1->setFromArray([
  "enfants" => [$enfant11, $enfant12]
]);
// father display
print "------------------------père\n";
print "père=$père\n";
  • linhas 30-45: atribuímos dois filhos a [$child1];

Os resultados da execução são os seguintes:

------------------------père
père={"nom":"Bertholomé","prénom":"Dieudonné","âge":58,"enfants":[{"nom":"Bertholomé","prénom":"Sylvain","âge":27,"enfants":[{"nom":"Bertholomé","prénom":"Gaëtan","âge":2,"enfants":null},{"nom":"Bertholomé","prénom":"Mathilde","âge":1,"enfants":null}]},{"nom":"Bertholomé","prénom":"Géraldine","âge":12,"enfants":null}]}

Se formatarmos este resultado, obtemos o seguinte:


père={
    "nom": "Bertholomé",
    "prénom": "Dieudonné",
    "âge": 58,
    "enfants": [
        {
            "nom": "Bertholomé",
            "prénom": "Sylvain",
            "âge": 27,
            "enfants": [
                {
                    "nom": "Bertholomé",
                    "prénom": "Gaëtan",
                    "âge": 2,
                    "enfants": null
                },
                {
                    "nom": "Bertholomé",
                    "prénom": "Mathilde",
                    "âge": 1,
                    "enfants": null
                }
            ]
        },
        {
            "nom": "Bertholomé",
            "prénom": "Géraldine",
            "âge": 12,
            "enfants": null
        }
    ]
}

Recuperámos com sucesso as cadeias JSON para todos os objetos [Person] que compõem o pai.