Skip to content

5. As classes

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

5.1. A árvore de scripts

Image

5.2. Qualquer variável pode se tornar um objeto dotado de atributos

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


<?php

// um objeto genérico
// $obj1=new stdClass();
// qualquer variável pode ter atributos por definição
$obj1->attr1 = "un";
$obj1->attr2 = 100;
// exibe o objeto
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// altera o objeto
$obj1->attr2 += 100;
// exibe o objeto
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
// copia o valor de objeto1 (endereço do objeto apontado) para objeto2
// as duas variáveis são, então, diferentes, mas apontam para o mesmo objeto
$obj2 = $obj1;
// altera obj2
$obj2->attr2 = 0;
// exibe os dois objetos
print "objet1=[$obj1->attr1,$obj1->attr2]\n";
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
// altera o objeto apontado por obj1
$obj1 = new stdClass();
print "obj1 :\n";
print_r($obj1);
print "obj2 :\n";
print_r($obj2);
// atribui a referência (o endereço) de objeto2 a objeto3
// $obj2 e $obj3 passam a ser, então, uma única e mesma variável
$obj3 = &$obj2;
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);
// altera obj3
$obj3->attr2 = 10;
// exibe os dois objetos
print "objet2=[$obj2->attr1,$obj2->attr2]\n";
print "objet3=[$obj3->attr1,$obj3->attr2]\n";
// altera o objeto apontado por obj2
$obj2 = new stdClass();
$obj2->attr3 = "deux";
$obj2->attr4 = 20;
// exibe os dois objetos $obj2 e $obj3
print "obj2 :\n";
print_r($obj2);
print "obj3 :\n";
print_r($obj3);

// um objeto é um dicionário?
print count($obj3) . "\n";
while (list($attribut, $valeur) = each($obj3)) {
  print "obj3[$attribut]=$valeur\n";
}
// fim
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 designa o atributo attr da variável $obj. Se ele não existir, será criado, transformando assim a variável $obj em um objeto com atributos. Vimos que PHP cria, por padrão, um objeto do tipo stdClass;
  • 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) para o mesmo objeto. O próprio objeto pode ser modificado por qualquer uma das referências;
  • linhas 23-27: têm como objetivo mostrar que $obj1 e $obj2 são duas variáveis diferentes: elas não estão no mesmo endereço de memória:
    • $obj2=$obj1 copiou o valor de $obj1 para a variável $obj2 (operação 1 acima). O valor de $obj1 é o endereço de um objeto. Assim, $obj1 e $obj2 apontam para o mesmo objeto. Quando se manipula uma variável $obj e esta aponta para um objeto, PHP manipula o objeto apontado pela variável $obj. De acordo com o esquema abaixo, percebe-se que é possível modificar o objeto apontado tanto por meio de $obj1 quanto por meio de $obj2. É isso que mostram as linhas 4 e 5 dos resultados;

Image

  • linha 30: a expressão $obj3=&$obj2 faz com que $obj2 e $obj3 fiquem no mesmo endereço, [1 ci-dessous]. Pode-se dizer que as duas variáveis são aliases do mesmo local na memória. Ambas apontam para um objeto, o Objeto A abaixo, [2];
    • a operação $obj2=new stdClass() faz com que um novo objeto, Objeto B, seja criado ([3 ci-dessous]), e o endereço desse novo objeto é atribuído à variável $obj2. Como $obj2 e $obj3 são dois aliases do mesmo endereço de memória, $obj3 também aponta para o novo objeto Objeto B. É isso que mostram as 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, 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 se esperar, o número de atributos. Portanto, um objeto apresenta 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 {

  // atributos da classe
  // não declarados — podem ser criados dinamicamente
  // método
  function identite() {
    // a priori, utiliza atributos inexistentes
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

// teste
// os atributos são públicos e podem ser criados dinamicamente
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// chamada de um método
print "personne=" . $p->identite() . "\n";
// fim
exit;

Resultados:

personne=[Paul,Langevin,48]

Comentários

  • linhas 3-13: definem uma classe Personne. Uma classe é um modelo a partir do qual se criam objetos. Ela reúne atributos e funções chamadas métodos. Não é obrigatório declarar os atributos;
  • linhas 8-11: o método identité exibe o valor de três atributos não declarados na classe. A palavra-chave $this designa o objeto ao qual o método é aplicado;
  • linha 17: cria-se um objeto $p do tipo Personne. A palavra-chave `new` serve para criar um novo objeto. A operação retorna uma referência ao objeto criado (ou seja, um endereço). São possíveis várias formas de escrita: `new Pessoa()`, `new Pessoa`, `new pessoa`. O nome da classe não diferencia maiúsculas de minúsculas;
  • linhas 18-20: os três atributos necessários para o método identité são criados no objeto $p;
  • linha 22: o método identité da classe Personne é aplicado ao objeto $p. No código (linhas 8-11) do método identité, $this faz referência ao mesmo objeto que $p;

5.4. A classe Pessoa com atributos declarados

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


<?php

class Personne {

  // atributos da classe
  var $prenom;
  var $nom;
  var $age;

  // método
  function identite() {
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

// teste
// os atributos são públicos
$p = new Personne();
$p->prenom = "Paul";
$p->nom = "Langevin";
$p->age = 48;
// chamada de um método
print "personne=" . $p->identite() . "\n";
// fim
exit;

Resultados:

personne=[Paul,Langevin,48]

Comentários

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

5.5. A classe Pessoa com um construtor

Os exemplos anteriores mostravam classes Personne pouco comuns, como as encontradas no PHP 4. Não é recomendável seguir esses exemplos. Apresentamos agora uma classe Personne [classes-04.php] que segue as boas práticas do PHP 7:


<?php

// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);
class Personne {
// atributos da classe
  private $prenom;
  private $nom;
  private $age;

// getters e 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;
  }

// construtor
  public function __construct(string $prenom, string $nom, int $age) {
    // utiliza-se os set
    $this->setPrenom($prenom);
    $this->setNom($nom);
    $this->setAge($age);
  }

// método toString
  public function __toString(): string {
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

// teste
// criação de um objeto Pessoa
$p = new Personne("Paul", "Langevin", 48);
// identidade dessa pessoa
print "personne=$p\n";
// alteração da idade
$p->setAge(14);
// identidade da pessoa
print "personne=$p\n";
// fim
exit;

Resultados:

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

Comentários

  • linhas 6-50: a classe Pessoa;
  • linhas 7-9: os atributos privados (private) da classe. Esses atributos só são visíveis dentro da classe. Outras palavras-chave que podem ser utilizadas são:
  • public: torna o atributo visível de fora da classe,
  • protected: torna o atributo visível de dentro da classe e das classes derivadas dela;
  • como os atributos são privados, não é possível acessá-los de fora da classe. Portanto, não é possível escrever o código a seguir:
$p=new Personne() ;
$p->nom="Landru" ;

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

  • usar os métodos públicos set e get (o nome desses métodos pode ser qualquer um) das linhas 12 a 34. Assim, poderemos escrever:
$p=new Personne() ;
$p->setNom("Landru") ;
  • usar o construtor das linhas 37 a 42. Nesse caso, escrever-se-á:
$p=new Personne("Michel","Landru",44) ;

A sintaxe acima chama automaticamente o método da classe Personne chamado __construct;

  • linha 59: esta linha exibe a pessoa $p na forma de uma sequência de caracteres. Para isso, é utilizado o método da classe Personne, chamado __toString (linhas 45-47);
  • todos os métodos da classe (funções) foram prefixados pela palavra-chave public, que indica que a função é visível fora da classe. As outras palavras-chave utilizáveis são, assim como para os atributos e com o mesmo significado, private e protected. Sem um atributo explícito de visibilidade, a função tem visibilidade implícita public;

5.6. A classe Pessoa 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 por meio de seus métodos set ou equivalentes. Para evitar repetir as mesmas verificações em dois locais diferentes, é possível colocá-las nos métodos set. Se um valor de inicialização de um objeto for incorreto, será lançada uma exceção. Veja um exemplo.

Primeiramente, movemos a definição da classe Personne para seu próprio arquivo [Personne.php]:


<?php

// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes;
namespace Exemples;

// classe Pessoa
class Personne {
// atributos da classe
  private $prenom;
  private $nom;
  private $age;

// getters e 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 {
    // o nome deve ser diferente de vazio
    $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 {
    // o sobrenome deve ser diferente de vazio
    $nom = trim($nom);
    if ($nom === "") {
      throw new \Exception("Le nom doit être non vide");
    } else {
      $this->nom = $nom;
    }
  }

  public function setAge(int $âge): void {
    // a idade deve ser válida
    if ($âge < 0) {
      throw new \Exception("L'âge doit être un entier positif ou nul");
    } else {
      $this->age = $âge;
    }
  }

// construtor
  public function __construct(string $prenom, string $nom, int $age) {
    // utilizamos os set
    $this->setPrenom($prenom);
    $this->setNom($nom);
    $this->setAge($age);
  }

  // método
  public function initWithPersonne(Personne $p): void {
    // inicializa o objeto atual com uma pessoa $p
    $this->__construct($p->prenom, $p->nom, $p->age);
  }

  // método toString
  function __toString(): string {
    return "[$this->prenom,$this->nom,$this->age]";
  }

}

Comentários

  • linha 4: solicita-se que o tipo dos parâmetros das funções seja respeitado quando declarado;
  • linha 7: define um namespace. O nome completo (denominado qualifié) da classe Personne passa a ser, então, \Exemples\Personne. Observe o caractere \ no início do nome qualificado: temos, então, um nome qualificado absolu. Se esse caractere estiver ausente, temos um nome qualificado relatif (relativo ao namespace atual). Assim, se duas classes A e B pertencem ao mesmo espaço de nomes E, no código da classe A será possível acessar a classe B por meio da notação relative B. Se a classe A fizer parte do espaço de nomes E1 e a classe B do espaço de nomes E2, no código da classe A, a classe B será acessada pela notação absolue \E2\B. Definir uma classe dentro de um espaço de nomes não é obrigatório, mas o NetBeans emite um aviso se isso não for feito. Portanto, faremos isso. Além disso, os espaços de nomes devem corresponder à estrutura hierárquica dos arquivos. Assim, a classe A no namespace E1 deve estar no arquivo E1/A.php. Isso não é obrigatório, mas, mais uma vez, o NetBeans emite um aviso se não for feito. No exemplo da classe [\Exemples\Personne], o NetBeans emite um aviso porque a estrutura de diretórios do arquivo [Personne.php] é [exemples/classes/Personne.php] e, portanto, não corresponde ao namespace. Não se deve confundir estrutura de arquivos com espaço de nomes. O nome totalmente qualificado de uma classe utiliza um espaço de nomes e não tem nada a ver com a estrutura do arquivo PHP da classe. A relação entre estrutura de arquivos e espaço de nomes é opcional e pode não ser respeitada, como fizemos aqui;
  • linhas 12-14: os três atributos privados da classe;
  • linhas 29-37: inicialização do atributo prenom e verificação do valor de inicialização;
  • linha 31: a função trim($chaine) remove os espaços que se encontram no início e no final de $chaine. Assim, trim(“abcd ”) resulta na sequência “abcd” e trim“ ” resulta na sequência vazia;
  • linha 32: se o nome for vazio, lança-se uma exceção (linha 33); caso contrário, o nome é armazenado (linha 35). Para lançar uma exceção, utilizou-se aqui a classe predefinida [Exception]. Aqui, é obrigatório usar seu nome absoluto [\Exception]. Se for utilizado seu nome relativo [Exception], essa classe será procurada no espaço de nomes atual, ou seja, o espaço de nomes [Exemples] da classe Personne. Assim, o interpretador PHP procurará uma classe com o nome absoluto [\Exemples\Exception], que não existe;

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


<?php

// respeito estrito ao tipo dos parâmetros das funções
declare(strict_types=1);

// inclusão da definição da classe Pessoa
require_once __DIR__."/Personne.php";

// nome qualificado da classe Pessoa
use \Exemples\Personne;

// teste
// criação de um objeto Pessoa
$p = new Personne("Paul", "Langevin", 48);
// identidade dessa pessoa
print "Exemple1, personne=$p\n";
// criação de um objeto Pessoa incorreto
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";
}
// criação de um objeto Pessoa com erro
try {
  $p = new Personne("", "yy", 10);
} catch (\Exception $e1) {
  print "Exemple3, erreur : " . $e1->getMessage() . "\n";
} catch (\TypeError $e2) {
  print "Exemple3, erreur : " . $e2->getMessage() . "\n";
}

// fim
exit;

Comentários

  • linha 7: o script utilizará a classe [Personne]. Portanto, é necessário informar ao interpretador PHP onde ele pode encontrar a definição dessa classe. Essa é a função das instruções [include fichier] e [require fichier]. Aqui, utilizamos a instrução [include]. A diferença entre as duas instruções é a seguinte: se a instrução [include fichier] encontrar erros ao carregar [fichier], um erro de nível [E_WARNING] é emitido, mas aa execução continua, enquanto a instrução [require], no mesmo caso, gera um erro fatal e a execução do script é interrompida. Cada uma das duas instruções possui uma variante: [include_once] e [require_once]. Essas duas variantes permitem lidar com o caso de inclusões múltiplas de um mesmo arquivo. Podemos imaginar aqui um projeto composto por vários scripts PHP, dos quais vários fazem referência à classe [Personne]. Sua execução provocará, então, a inclusão do arquivo [Personne.php] várias vezes e causará um erro, pois uma classe não pode ser definida duas vezes. A solução é utilizar as variantes [_once], que garantem que o arquivo será incluído apenas uma vez no script global do projeto;
  • linha 7: a constante [__DIR__] é uma constante PHP que indica o nome completo da pasta na qual se encontra o script contendo a constante [__DIR__]. Assim, a expressão da 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 arquivo, é possível usar indistintamente os sinais / e \;

  • linha 14: utiliza-se a classe [Personne] que acabamos de definir. O script [classes-05.php] não possui namespace. A linha 14 utiliza o nome relativo da classe [Personne] sem namespace. Na ausência de um namespace para a classe [Personne], ela é procurada no próprio script [classes-05.php] e, portanto, não será encontrada. Há duas soluções para esse problema:
    • utilizar o nome completo da classe [\Exemples\Personne];
    • utilizar a instrução `use` da linha 10. Ela indica que o código a seguir utiliza a classe [\Exemples\Personne];
  • linha 10: a instrução `use` permite que o interpretador saiba que a classe [Personne] referenciada na linha 14 é, na verdade, a classe [\Exemples\Personne]. Dito isso, onde o interpretador encontrará o código dessa classe? É a linha 7 que lhe diz isso. Ela indica que, para executar o script atual, também é necessário carregar o script [Personne.php]. Aqui, utilizou-se o nome relativo do arquivo. Portanto, ele será procurado na pasta que contém o script [classes-05.php]. Portanto, os scripts [Personne.php] e [classes-05.php] precisam estar na mesma pasta. É o que ocorre aqui, onde ambos estão na pasta [exemples/classes]. A instrução da linha 10 é equivalente a:

use \Exemples\Personne as Personne;

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

  • linha 14: é criado um objeto [Personne]. É o método [__construct] da classe [Personne] que será executado implicitamente aqui;
  • linha 16: exibe a Pessoa $p. Para ser exibido, o valor da variável $p deve ser convertido em uma cadeia de caracteres. Implicitamente, é o método [Personne.__toString] que é executado nesse momento. Portanto, esse método deve retornar uma cadeia de caracteres;
  • vimos que o construtor da classe [Personne] pode lançar uma exceção do tipo [\Exception]. Portanto, é necessário tratá-la. Assim, o código da linha 14 está incompleto. É preciso utilizar o código das linhas 18 a 24 para tratar corretamente a exceção que pode ocorrer. Aqui, geramos uma exceção propositalmente ao passar uma idade que não é um número inteiro. Nesse caso específico, a exceção que ocorre é lançada pelo interpretador PHP e não pelo código da classe [Personne]. De fato, a assinatura do método [Personne.__construct] é a seguinte:

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

Portanto, o parâmetro [age] passado ao construtor deve ser do tipo inteiro. Caso contrário, o interpretador PHP gera um erro do tipo [TypeError]. Além disso, os métodos [set] da classe [Personne], por sua vez, lançam uma exceção do tipo [\Exception]. Como o construtor que os chama não possui uma estrutura try/catch, a exceção sobe um nível, para o código que chamou o construtor, ou seja, o código do script [classes-05.php]. Por fim, o script [classes-05.php] pode receber dois tipos de exceção: \Exception ou \TypeError. Observe-se que, quando o desenvolvedor tem certeza de que certas exceções não podem ocorrer, ele não utilizará as opções catch correspondentes. Aqui, elas são utilizadas sistematicamente apenas para fins de demonstração. No entanto, as opções catch serão utilizadas para qualquer exceção possível, mesmo que improvável;

Por esse motivo, a estrutura try das linhas 18 a 24 possui dois catch para lidar separadamente com os dois tipos de exceção;

  • linha 20: pode-se escrever tanto [Exception] quanto [\Exception]:
    • a primeira versão utiliza o nome relativo da classe, em relação ao espaço de nomes do script. Este não possui espaço de nomes. Seu espaço de nomes é, portanto, a raiz dos espaços de nomes: \. Portanto, escrever aqui [Exception] equivale a escrever [\Exception]. No entanto, a classe [Exception] está, de fato, no espaço de nomes [\];

É preferível usar o nome absoluto das exceções predefinidas de PHP em um script que não possua espaços de nomes. Assim, se decidirmos atribuir um namespace a esse script, a escrita dos nomes absolutos das classes continua válida, enquanto que, no outro caso, a mudança de namespace causará erros nos nomes relativos das classes;

  • linha 21: quando ocorre uma exceção, o método [Exception→getMessage] permite obter a mensagem de erro da exceção. O mesmo se aplica a um erro do tipo [TypeError]. No método [Personne.setPrenom], foi escrito:

  public function setPrenom(string $prénom) {
    // o nome deve ser preenchido
    $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 [Le prénom doit être non vide]. É isso 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. Adicionando um método que funciona como um segundo construtor

No PHP 7, não é possível ter vários construtores com parâmetros diferentes que permitam construir um objeto de diversas maneiras. Portanto, é possível utilizar métodos que funcionam como construtores:


  // método
  public function initWithPersonne(Personne $p) {
    // inicializa o objeto atual com uma pessoa $p
    $this->__construct($p->prenom, $p->nom, $p->age);
}

Comentários

  • linhas 2-5: o método initWithPersonne permite atribuir ao objeto atual os valores dos atributos de outro objeto Personne. Aqui, ele utiliza o construtor __construct, mas isso não é obrigatório. Ele poderia inicializar por conta própria os atributos da classe [Personne];

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


<?php

// inclusão da definição da classe Pessoa
require_once __DIR__."/Personne.php";
// declaração do nome qualificado da classe Pessoa
use \Exemples\Personne;

// teste
// criação de um objeto Pessoa
try {
  $p = new Personne("Paul", "Langevin", 48);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
// identidade dessa pessoa
print "personne=$p\n";
// criação de uma segunda pessoa
try {
  $p2 = new Personne("Laure", "Adeline", 67);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}
// inicialização da primeira com os valores da segunda
try {
  $p->initWithPersonne($p2);
} catch (\Exception $e) {
  print "erreur : " . $e->getMessage();
  exit;
}

// verificação
print "personne=$p\n";
// fim
exit;
  • linhas 14, 23, 30: é comum que, após uma exceção, seja necessário interromper a execução de um script de console se o erro encontrado for irrecuperável. Esse não é o caso em um script da web: não se interrompe a execução do script, mas sim exibe-se uma página de erro. Se estivermos em uma função, não será utilizada a instrução exit, mas sim return: não se interrompe a execução do script (exit), mas sim sai-se da função (return) após ter sido gerado um erro;

Resultados:

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

5.8. Uma tabela de objetos [Personne]

O exemplo a seguir, [classes-07.php], mostra que é possível ter tabelas de objetos.


<?php

require_once __DIR__."/Personne.php";
use \Exemples\Personne;

// teste
// criação de uma matriz de objetos Pessoa
// para facilitar a compreensão do código, não se lida com uma eventual exceção
$groupe = [new Personne("Paul", "Langevin", 48), new Personne("Sylvie", "Lefur", 70)];

// identidade dessas pessoas
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}

// fim
exit;

Resultados:

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

Comentários

  • linha 9: criação de um array com 2 pessoas;
  • linha 12: percorrer a tabela;
  • linha 13: $groupe[$i] é um objeto do tipo Personne. O método [Personne.__toString] é utilizado para exibi-lo;

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

Cria-se, em um arquivo [Enseignant.php], a seguinte classe [Enseignant]:


<?php

// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Exemples;

// uma classe derivada de pessoa
class Enseignant extends Personne {
  // atributos
  private $discipline;   // disciplina lecionada

  // getters e setters

  public function getDiscipline(): string {
    return $this->discipline;
  }

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

  // construtor
  public function __construct(string $prénom, string $nom, int $âge, string $discipline) {
    // atributos do pai
    parent::__construct($prénom, $nom, $âge);
    // outros atributos
    $this->setDiscipline($discipline);
  }

  // sobrecarregamento da função __toString da classe pai
  public function __toString(): string {
    return "[" . parent::__toString() . ",$this->discipline]";
  }

}

Comentários

  • linha 7: a classe [Enseignant] também faz parte do espaço de nomes [Exemples];
  • linha 10: a classe Enseignant deriva (extends) da classe Personne.. A classe derivada Enseignant herda os atributos e métodos de sua classe pai;
  • linha 12: a classe Enseignant adiciona um novo atributo, discipline, que é exclusivo dela;
  • linha 25: o construtor da classe Enseignant recebe 4 parâmetros:
    • 3 para inicializar sua classe pai (nome, sobrenome, idade), linha 27;
    • 1 para sua própria inicialização (disciplina), linha 29;
  • linha 27: a classe derivada tem acesso aos métodos e construtores de sua classe pai por meio da palavra-chave parent ::. Aqui, passamos os parâmetros (nome, sobrenome, idade) 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 [Enseignant] é utilizada pelo seguinte script [classes-08.php]:


<?php

// inclusão da definição das duas classes
require_once __DIR__."/Personne.php";
require_once __DIR__."/Enseignant.php";
// declaração das duas classes utilizadas
use \Exemples\Personne;
use \Exemples\Enseignant;

// teste
// criação de um array de objetos Pessoa e derivados
// para simplificar o exemplo, não tratamos as exceções
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));

// identidade dessas pessoas
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
// fim
exit;

Comentários

  • linhas 4-5: precisamos informar ao interpretador PHP onde estão localizadas as duas classes [Enseignant, Personne];
  • linhas 7-8: declaração dos nomes completos das duas classes. Isso nos permitirá referir-nos a elas no código simplesmente pelo nome, sem o sufixo de seus espaços de nomes;
  • linha 13: criamos uma matriz contendo um tipo [Personne] e um tipo [Enseignant];
  • linhas 16-18: exibem os elementos da matriz;
  • linha 17: o método __toString de cada elemento $groupe[$i] será chamado. A classe Personne possui um método __toString. A classe Enseignant possui dois: o de sua classe pai e o próprio. Podemos nos perguntar qual deles será chamado. A execução mostra que foi chamado o da classe Enseignant. É sempre assim: quando um método é chamado em um objeto, ele é procurado na seguinte ordem: no próprio objeto, em sua classe pai, se houver, depois na classe pai da classe pai, e assim por diante… A busca é interrompida 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 a seguir cria uma classe Etudiant derivada da classe Personne, no arquivo [Etudiant.php]:


<?php

// respeito estrito aos tipos declarados dos parâmetros das funções
declare (strict_types=1);

// espaço de nomes
namespace Exemples;

class Etudiant extends Personne {
  // atributos
  private $formation;   // cursos realizados

  // getters e setters
  public function getFormation(): string {
    return $this->formation;
  }

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

  // construtor
  public function __construct(string $prénom, string $nom, int $âge, string $formation) {
    // atributos do pai
    parent::__construct($prénom, $nom, $âge);
    // outros atributos
    $this->setFormation($formation);
  }

  // sobrecarregamento da função __toString da classe pai
  public function __toString(): string {
    return "[" . parent::__toString() . ",$this->formation]]";
  }

}

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


<?php

// inclusão e definição das classes utilizadas pelo 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;

// teste
// criação de uma matriz de objetos “pessoa” e derivados
// para facilitar a compreensão do exemplo, não tratamos as exceções
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));

// identidade dessas pessoas
for ($i = 0; $i < count($groupe); $i++) {
  print "groupe[$i]=$groupe[$i]\n";
}
// fim
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 a objetos, o construtor de uma classe derivada chama automaticamente o da sua classe pai. O código a seguir, [classes-16.php], mostra que, com PHP 7, esse não é o caso:


<?php

class Classe1 {

  // construtor
  public function __construct() {
    print "constructeur de la classe Classe1\n";
  }

}

class Classe2 extends Classe1 {

  // construtor
  public function __construct() {
    // o construtor da classe pai não é chamado implicitamente
    print "constructeur de la classe Classe2\n";
  }

}

class Classe3 extends Classe1 {

  // construtor
  public function __construct() {
    // chamada explícita do construtor da classe pai
    parent::__construct();
    // código específico da Classe3
    print "constructeur de la classe Classe3\n";
  }

}

// testes
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. Redefinição de um método da classe pai

Já vimos que um método da classe pai pode ser redefinido em uma classe filha. Assim, o método [__toString] da classe [Personne] (ver link) foi redefinido nas classes filhas [Enseignant] (ver link) e [Etudiant] (ver link). O script [classes-13.php] ilustra novamente o conceito:


<?php

// respeito estrito ao tipo dos parâmetros das funções
declare(strict_types=1);

// classe principal
class Classe1 {

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

  function g(): int {
    return 2;
  }

}

// classe derivada
class Classe2 extends Classe1 {

  // redefinimos a função f da classe pai
  public function f(): int {
    return parent::f() + 10;
  }

}

// código
$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 [Classe1] define dois métodos f e g;
  • linhas 20-27: a classe [Classe2] estende a classe [Classe1] e redefine o método f desta última;

Resultados

1
2
3
11
2
1

Comentários

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

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

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


<?php

// respeito estrito ao tipo dos parâmetros das funções
declare(strict_types=1);

// classe principal
class Classe1 {

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

  function g(): int {
    return 2;
  }

}

// classe derivada
class Classe2 extends Classe1 {

  // redefinimos a função f da classe pai
  public function f(): int {
    return parent::f() + 10;
  }

}

// o parâmetro da função é do tipo Classe1 ou derivado
function doSomething(Classe1 $c1): void {
  print $c1->f() + $c1->g() . "\n";
}

// código
// cria-se um objeto do tipo Classe2 derivado de Classe1
$c2 = new Classe2();
// chama-se doSomething com
doSomething($c2);

Comentários

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

Resultados

13

5.14. Classes abstratas

Uma classe abstrata é uma classe incompleta que não pode ser instanciada. Ela deve, obrigatoriamente, ser derivada para poder ser utilizada.

Para que serve uma classe abstrata? Às vezes, temos classes que compartilham um ou mais métodos, mas que se diferenciam por outros métodos ou atributos. Nesse caso, é recomendável reunir tudo o que é comum em uma classe pai. Por enquanto, não precisamos de uma classe abstrata. Mas suponhamos que as classes filhas difiram apenas por um método M: a assinatura do método seria a mesma em todas as classes filhas, mas sua implementação seria diferente. Para obrigar as classes filhas a implementar o método M:

  • vamos declarar a assinatura do método M na classe pai. Como esta não sabe como implementá-lo, prefixamos o método com a palavra-chave abstract: isso significa que a implementação do método M é transferida para as classes filhas;
  • como a classe pai não está totalmente implementada, ela também é declarada como abstrata com a mesma palavra-chave `abstract`. Isso faz com que a classe não possa mais ser instanciada. É obrigatório criar uma classe filha que defina a implementação do método M, para que o corpo da classe pai possa ser utilizado;

Veja um exemplo [classes-15.php]:


<?php

// respeito estrito ao tipo dos parâmetros das funções
declare(strict_types=1);

// classe principal abstrata
abstract Class Classe1 {

  // método conhecido por todas as classes derivadas
  public function f(): int {
    return 1;
  }

  // método g abstrato — será definido pelas classes derivadas
  abstract function g(): int;
}

// classe derivada
Class Classe2 extends Classe1 {

  // o método g da classe pai deve ser definido
  public function g(): int {
    return parent::f() + 10;
  }

}

// classe derivada
Class Classe3 extends Classe1 {

  // o método g da classe pai deve ser definido
  public function g(): int {
    return parent::f() + 20;
  }

  // é possível redefinir o método f da classe pai
  public function f(): int {
    return 2;
  }

}

// código
$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 [Classe1] é abstrata (linha 7) porque não sabe implementar o método g da linha 15. Portanto, ela deve ser derivada para poder ser utilizada;
  • linhas 19-26: a classe [Classe2] estende a classe [Classe1] e redefine o método g de sua classe pai (linhas 22-24);
  • linhas 29-41: a classe [Classe3] estende a classe [Classe1] e redefine o método g de sua classe pai (linhas 32-34);
  • linhas 37-39: a classe [Classe3] redefine o método f de sua classe pai;
  • linhas 44-49: criam-se dois objetos dos tipos [Classe2] e [Classe3] e chamam-se seus métodos f e g;

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. Consideremos o seguinte script [classes-11.php]:


<?php

// espaço de nomes
namespace Exemples;

// classe não derivável
final Class Classe1 {
  
}

// classe derivada
Class Classe2 extends Classe1 {
  
}

// código — deve gerar um erro
new Classe2();

Comentários

  • linhas 7-9: a palavra-chave final torna a classe [Classe1] uma classe final que não pode ser derivada;
  • linhas 12-14: a classe [Classe2] estende a classe final [Classe1], o que é um erro;
  • linha 17: o erro só será sinalizado durante a execução do script, quando se tentar manipular um objeto do tipo [Classe2];

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 redefinido por derivação. Veja um exemplo [classes-12.php]:


<?php

// respeito estrito ao tipo dos parâmetros das funções
declare(strict_types=1);

// espaço de nomes
namespace Exemples;

// classe principal
Class Classe1 {

  // esse método não pode ser redefinido em uma classe derivada
  public final function f(): int {
    return 1;
  }

}

// classe derivada
Class Classe2 extends Classe1 {

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

}

// código — deve causar um erro
new Classe2();

Comentários

  • linha 13: o método f da classe [Classe1] é declarado final pela palavra-chave final;
  • linha 20: a classe [Classe2] herda da classe [Classe1];
  • linhas 22-23: redefine-se a função f da classe pai [Classe1]. Isso deve causar um erro;
  • linha 29: cria-se um objeto do tipo [Classe2] para forçar o interpretador PHP a inspecionar a classe [Classe2];

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 vinculado à classe na qual está definido e não aos objetos instâncias da classe. Assim, se a classe C declarar um método estático M, para utilizá-lo, escrever-se-á:

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

Veja um exemplo [classes-17.php]:


<?php

class Classe1 {

  // método estático
  static function say(string $message): void {
    print "$message\n";
  }

}

// teste -------------------
Classe1::say("hello");

Comentários

  • linha 6: o método [say] é declarado estático com a palavra-chave static;
  • linha 13: chamada do método estático [say] com a notação: Classe1::say;

Resultados

hello

Consideremos agora o seguinte código [classes-18.php]:


<?php

class Classe1 {
  // atributo estático
  private static $nbObjects = 0;

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

  // método estático
  static function say(): void {
    print self::$nbObjects ." objets de type [Classe1] ont été construits\n";
  }

}

// teste -------------------
new Classe1();
new Classe1();
Classe1::say();

Comentários

  • linha 5: declara-se um atributo estático que contará o número de instâncias da classe [Classe1] criadas. Este não é um atributo que possa pertencer a uma instância da classe. De fato, se dois objetos O1 e O2 forem criados, nenhum deles tem conhecimento do outro. Ter um contador na instância não faz sentido: quando um novo objeto é criado, em qual instância se incrementaria o contador? Seria necessário incrementar o contador de um objeto específico, ignorando os contadores das outras instâncias. Um atributo estático é um atributo da classe e não da instância da classe;
  • linhas 7-10: é no construtor que contaremos os objetos criados, já que a criação de cada novo objeto provoca a execução do construtor;
  • linha 14: observe-se a notação self::$nbObjects para indicar que se está fazendo referência a um atributo estático da classe na qual o código está sendo executado;
  • linhas 13-15: o método estático [say] tem a função de exibir o número de objetos criados;
  • linhas 20-22: criam-se dois objetos e exibe-se 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 {
  // atributo
  private $attributeOfParent = 4;

  // método
  public function doTest(): void {
    // quem chama?
    print "parent :\n";
    var_dump($this);
    // exibição pai
    print "parent : attributeOfParent={$this->attributeOfParent}\n";
    print "parent : attributeOfChild={$this->attributeOfChild}\n";
  }

}

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

  // método
  public function doTest(): void {
    // exibição filha
    print "child : attributeOfParent={$this->attributeOfParent}\n";
    print "child : attributeOfChild={$this->attributeOfChild}\n";

    // pai
    parent::doTest();
  }
}

// script principal
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]. Observa-se que ela estende a classe [SomeParent] (linha 19);
  • linha 5: a classe [SomeParent] possui apenas um atributo;
  • linhas 8-17: o método [SomeParent::doTest] tem como objetivo exibir dois atributos:
    • [$attributeOfParent], que pertence à classe [SomeParent];
    • [$attributeOfChild], que pertence à classe [SomeChild] (linha 21);
  • linhas 10-11: exibe-se a identidade do chamador: de fato, 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] possui apenas um atributo;
  • linha 24: o método [SomeChild::doTest] tem como objetivo exibir dois atributos:
    • [$attributeOfParent], que pertence à classe [SomeParent];
    • [$attributeOfChild], que pertence à classe [SomeChild];
  • linhas 26-27: exibição dos dois atributos;
  • linha 30: chamada do método [doTest] da classe pai, que, por sua vez, exibirá os dois atributos;
  • linha 36: o método [SomeParent::doTest] é chamado;
  • linha 38: o método [SomeChild::doTest] é chamado;

No primeiro teste, a visibilidade dos dois atributos é [private]. Portanto, é de se esperar que a classe filha não veja o atributo de sua classe pai. Para que isso ocorra, o atributo da classe pai deveria ter, no mínimo, a visibilidade [protected]. Mas e quanto ao atributo da classe filha? Ele é visível na classe pai?

Aqui estão 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: percebe-se que o atributo [SomeParent::$attributeOfChild] não existe. Portanto, ele não é exibido;
  • linhas 11-30: resultados do segundo teste, em que o método [SomeChild::doTest] é chamado;
  • linhas 13-14: percebe-se que o atributo [SomeChild::$attributeOfParent] não existe. Portanto, ele não é exibido. Isso é 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: de forma bastante surpreendente, [$this], do tipo [SomeChild], apresenta aqui o atributo do pai [$attributeOfParent];
  • linhas 25-30: de forma igualmente surpreendente, o [$this], do tipo [SomeChild], não exibe seu atributo [$attributeOfChild];

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

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


<?php

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

  // método
  public function doTest(): void {
    // quem chama?
    print "parent :\n";
    var_dump($this);
    // exibição pai
    print "parent : attributeOfParent={$this->attributeOfParent}\n";
    print "parent : attributeOfChild={$this->attributeOfChild}\n";
  }

}

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

  // método
  public function doTest(): void {
    // exibição filha
    print "child : attributeOfParent={$this->attributeOfParent}\n";
    print "child : attributeOfChild={$this->attributeOfChild}\n";

    // pai
    parent::doTest();
  }

}

// script principal
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 tem visibilidade do atributo de seu pai [$attributeOfParent]. Isso é normal, já que este agora tem escopo [protected];
  • no método [someParent::doTest], o objeto [$this] é do tipo [SomeChild] (linhas 15-20). Ele vê o atributo de seu pai [$attributeOfparent] (linha 21), mas ainda não vê seu próprio atributo [$attributeOfChild] (linhas 23-28);

Na terceira tentativa, o atributo [$attributeOfChild] também possui um escopo [protected]:


<?php

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

  // método
  public function doTest(): void {
    // quem chama?
    print "parent :\n";
    var_dump($this);
    // exibição pai
    print "parent : attributeOfParent={$this->attributeOfParent}\n";
    print "parent : attributeOfChild={$this->attributeOfChild}\n";
  }

}

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

  // método
  public function doTest(): void {
    // exibição filha
    print "child : attributeOfParent={$this->attributeOfParent}\n";
    print "child : attributeOfChild={$this->attributeOfChild}\n";

    // pai
    parent::doTest();
  }

}

// script principal
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), acessa o atributo protegido [$attributeOfChild] de sua própria classe [SomeChild].

O que podemos concluir desses testes?

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

Esse é o comportamento esperado.

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

Esse é o comportamento esperado.

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

Esse é um comportamento inesperado.

5.19. Codificação jSON de uma classe

Em uma classe, o método [__toString] costuma estar presente: ele deve retornar uma sequência de caracteres que represente o objeto que o chama. Pode ser tentador que essa sequência seja uma sequência jSON. Vamos explorar essa possibilidade agora.

Image

Usaremos a seguinte classe [Personne]:


<?php

class Personne {
  // atributos
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;

  // setter global
  public function setFromArray(array $arrayOfAttributes): Personne {
    // inicialização de alguns atributos da classe
    foreach ($arrayOfAttributes as $attribute => $value) {
      $this->$attribute = $value;
    }
    // retorna o objeto
    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 {
    // identifica-se o objeto
    var_dump($this);
    // recuperam-se seus atributos
    $attributes = \get_object_vars($this);
    var_dump($attributes);
    // retorna a sequência jSON dos atributos
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }

}

Comentários

  • linhas 5-8: os quatro atributos da classe;
  • linhas 20-35: os getters que permitem obter o valor desses atributos;
  • linhas 11-18: um setter global que permite inicializar os atributos a partir de um array associativo [$arrayOfAttributes] cujas chaves são idênticas aos atributos da classe;
  • linhas 38-46: o método [__toString] da classe;
  • linha 42: a função PHP [get_object_vars] permite obter o valor dos atributos da classe na forma de um array associativo [‘nome’=>’nome1’, ‘sobrenome’=>’sobrenome1’, ‘idade’=>’idade1’, ‘filhos’=>[]];
  • linha 45: retorna-se a cadeia jSON dessa tabela de atributos;

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


<?php

// classe Pessoa
require "Personne.php";

// instanciação do pai
$père = new Personne();
// inicialização do pai
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// instanciação e inicialização do filho1
$enfant1 = (new Personne())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 17
  ]);
// instanciação e inicialização do filho2
$enfant2 = (new Personne())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// inicialização dos filhos do pai
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);

// exibição dos elementos do pai
$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: inicializa-se um objeto [Personne] [$père] com o método [Personne::setFromArray], que permiteinicializar um objeto [Personne] com um array cujas chaves são idênticas aos atributos da classe [Personne];
  • linhas 14-19: inicializa-se um objeto [Personne] [$enfant1] da mesma forma;
  • linhas 21-25: inicializa-se um objeto [Personne] [$enfant2];
  • linhas 27-29: inicializa-se o atributo [$père→enfants] com uma matriz contendo os dois filhos;
  • linhas 32-33: atribui-se os dois filhos ao pai;
  • linha 35: a operação [print] tentará transformar o objeto [$enfant1] em uma cadeia de caracteres. Para isso, ela utiliza o método [__toString] do objeto. Esperamos, portanto, ver a sequência de caracteres 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 [$enfant1];
  • linhas 12-21: a tabela de atributos do objeto [$enfant1]. Temos todos eles;
  • linha 22: temos a string jSON do objeto [$enfant1];
  • linhas 23-44: o mesmo vale para o objeto [$enfant2];
  • linhas 45-112: para o pai, é um pouco diferente, já que seu atributo [enfants] não é NULL, como era no caso dos filhos;
  • linha 112: percebe-se que, na sequência jSON do pai, faltam os filhos;
  • linhas 79-111: percebe-se que, na tabela de atributos do pai, o filho 1 (linhas 89-98) permaneceu como um objeto, assim como o filho 2 (linhas 99-110). Em resumo, a expressão [\get_object_vars($this)], em que [$this] representa o pai, não é recursiva: se um atributo da classe [Personne] for ele próprio um objeto, a expressão [\get_object_vars($this)] não tenta obter sua matriz de atributos;

É possível melhorar isso. Modificamos a classe [Personne] para a seguinte classe [Personne2]:


<?php

class Personne2 {
  // atributos
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;

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

    // retorna o objeto
    return $this;
  }

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



  // __toString
  public function __toString(): string {
    // recupera-se os atributos do objeto
    $attributes = $this->getAttributes($this);
    $enfants = $attributes["enfants"];
    if ($enfants != NULL) {
      $attributes["enfants"] = [$enfants[0]->getAttributes(), $enfants[1]->getAttributes()];
    }
    // retorna a string JSON dos atributos
    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] retorna a matriz de atributos do objeto que a chama;
  • linhas 25-34: a função [__toString];
  • linha 27: os atributos da classe [Personne] são recuperados na matriz [$attributes];
  • linha 28: sabemos, com base no exemplo anterior, que [$attributes["enfants"]] é uma matriz de dois objetos do tipo [Personne];
  • linhas 29-31: os dois objetos são substituídos por sua matriz de atributos;
  • linha 33: resta apenas codificar em jSON a matriz de atributos construída;

O script [json-02.php] utiliza a classe [Personne2] da seguinte maneira:


<?php

// classe Pessoa2
require "Personne2.php";

// instanciação do pai
$père = new Personne2();
// inicialização
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// instanciação e inicialização de filho1
$enfant1 = (new Personne2())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 17
  ]);
// instanciação e inicialização do filho2
$enfant2 = (new Personne2())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// inicialização dos filhos da classe pai
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);

// exibição do pai
print "------------------------père\n";
print "père=$père\n";

O script [json-02.php] é idêntico ao script [json-01.php], com a única diferença de que a classe [Personne2] substituiu a classe [Personne].

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 junto com o pai.

A solução anterior não é satisfatória, pois os filhos podem, por sua vez, ter filhos. Assim, voltamos ao problema anterior.

A classe [Personne3] resolve esse problema da seguinte maneira:


<?php

class Personne3 {
  // atributos
  private $nom;
  private $prénom;
  private $âge;
  private $enfants;

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

  }

  // getters


  // __toString
  public function __toString(): string {
    // retorna a cadeia JSON dos atributos
    $attributes = [];
    $this->getRecursiveAttributes($attributes, $this, []);
    // a string JSON dos atributos
    return \json_encode($attributes, JSON_UNESCAPED_UNICODE);
  }

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

  private function getRecursiveAttributes(array &$attributes, $value, $keys): void {
    // análise do valor [$value]
    // $keys é uma matriz [key1, key2, .., keyn]
    // $value=$attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX….[keyn]
    // se [$value] for um objeto, utiliza-se seu método [getAttributes]
    if (\is_object($value)) {
      // atributos do objeto [$value]
      $objectAttributes = $value->getAttributes();
      // o que fazemos com o resultado?
      if ($keys) {
        // em [$attributes], vamos substituir $value pela matriz de seus atributos
        // é preciso construir o elemento $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]
        // onde $keys é a matriz [key1, key2, .., keyn]
        //, utiliza-se a referência da tabela [$attributes]
        $attribute = &$attributes;
        // faz-se a varredura da tabela de chaves
        foreach ($keys as $key) {
          // pega-se a referência do atributo
          $attribute = &$attribute[$key];
        }
        // aqui, $attribut e $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[key(n)] são idênticos
        // e compartilham o mesmo espaço de memória
        // o objeto [$value] é substituído por sua matriz de atributos;
        // é preciso escrever $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]=$objectAttributes
        // o que equivale a $attribute = $objectAttributes
        $attribute = $objectAttributes;
      } else {
        // sem chaves — estamos no início da exploração do objeto
        // $objectAttributes representa os atributos de primeiro nível da classe
        $attributes += $objectAttributes;
      }
      // talvez em [$objectAttributes] ainda haja objetos
      // estamos explorando os atributos de [$objectAttributes]
      $this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
    } else {
      if (\is_array($value)) {
        // temos uma matriz — analisamos cada um de seus elementos
        foreach ($value as $key => $élément) {
          // adicionamos a chave atual à matriz $keys
          \array_push($keys, $key);
          // analisamos $élément
          $this->getRecursiveAttributes($attributes, $élément, $keys);
          // removemos da matriz $keys a chave que acabou de ser analisada
          \array_pop($keys);
        }
      }
    }
  }

Comentários

  • linhas 21-22: desta vez, o método [__toString] solicita os atributos de sua classe e determina que isso seja feito de forma recursiva: se um atributo for um objeto ou uma matriz de objetos, então cada objeto deve ser substituído por sua matriz de atributos na matriz de atributos final da classe;
  • linhas 31-78: a função [getRecursiveAttributes] realiza esse trabalho. O código foi comentado. Escrever uma função recursiva costuma ser algo complexo. É o caso aqui. O leitor não perderá nada se não compreender. Existem bibliotecas que realizam esse trabalho. A chamada recursiva ocorre nas linhas 64 e 72;
  • o interesse desse código é que ele não foi escrito apenas para a classe [Personne3]. Ele é válido para qualquer classe que possua atributos cujos valores sejam de tipos de objetos diferentes, desde que as classes utilizadas pela classe principal tenham, assim como ela, o método [getAttributes] das linhas 27 a 29

O script [json-03.php] utiliza a classe [Personne3] da seguinte maneira:


<?php

// classe Pessoa3
require "Personne3.php";

// instanciação do pai
$père = new Personne3();
// inicialização
$père->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Dieudonné",
  "âge" => 58
]);
// instanciação e inicialização de filho1
$enfant1 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Sylvain",
  "âge" => 27
  ]);
// instanciação e inicialização do filho2
$enfant2 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Géraldine",
  "âge" => 12
  ]);
// inicialização dos filhos da classe pai
$père->setFromArray([
  "enfants" => [$enfant1, $enfant2]
]);
// instanciação e inicialização do filho11
$enfant11 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Gaëtan",
  "âge" => 2
  ]);
// instanciação e inicialização do filho12
$enfant12 = (new Personne3())->setFromArray([
  "nom" => "Bertholomé",
  "prénom" => "Mathilde",
  "âge" => 1
  ]);
// inicialização dos filhos de filho1
$enfant1->setFromArray([
  "enfants" => [$enfant11, $enfant12]
]);
// exibição do pai
print "------------------------père\n";
print "père=$père\n";
  • linhas 30-45: atribuem-se dois filhos à classe [$enfant1];

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

Conseguimos recuperar corretamente a sequência jSON de todos os objetos [Personne] que compõem o pai.