Skip to content

5. As classes

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

5.1. A estrutura hierárquica dos scripts

Image

5.2. Qualquer variável pode tornar-se 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 são, 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 este não existir, é criado, transformando assim a variável $obj num objeto com atributos. Vimos que PHP cria, por predefiniçã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 alterado por qualquer uma das referências;
  • linhas 23-27: destinam-se a mostrar que $obj1 e $obj2 são duas variáveis diferentes: não se encontram na mesma morada 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, a variável PHP manipula o objeto para o qual aponta a variável $obj. De acordo com o esquema abaixo, verifica-se que é possível alterar o objeto apontado quer através de $obj1, quer através 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 na mesma morada, [1 ci-dessous]. Pode dizer-se que as duas variáveis são aliases do mesmo local de memória. Ambas apontam para um objeto, o Objeto A, abaixo indicado como [2];
    • a operação $obj2=new stdClass() faz com que seja criado um novo objeto, o Objeto B ([3 ci-dessous]), e que a morada desse novo objeto seja atribuída à variável $obj2. Uma vez que $obj2 e $obj3 são dois aliases do mesmo local 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 fornece, como seria de esperar, o número de atributos. Assim, um objeto apresenta semelhanças com um dicionário, mas não é um dicionário;

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() {
    // à primeira vista, 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. Agrupa atributos e funções denominadas métodos. Não é obrigatório declarar os atributos;
  • linhas 8-11: o método identité apresenta o valor de três atributos não declarados na classe. A palavra-chave $this designa o objeto ao qual se aplica o método;
  • linha 17: cria-se um objeto $p do tipo Personne. A palavra-chave «new» serve 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 formas de escrita: «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 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 explicitamente declarados com a palavra-chave var;

5.5. A classe Pessoa com um construtor

Os exemplos anteriores mostravam classes Personne pouco comuns, como as que se podiam encontrar no PHP 4. Não é aconselhável seguir estes exemplos. Apresentamos agora uma classe Personne [classes-04.php] que segue as boas práticas de PHP 7:


<?php

// respeito rigoroso dos 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 desta 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. Estes atributos só são visíveis no interior da classe. As outras palavras-chave que podem ser utilizadas são:
  • public: torna o atributo visível a partir do exterior da classe,
  • protected: torna o atributo visível a partir do interior da classe e das classes dela derivadas;
  • uma vez que os atributos são privados, não é possível aceder-lhes a partir do exterior da classe. Por conseguinte, não é possível escrever o seguinte código:
$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, existem duas formas:

  • utilizar os métodos públicos set e get (o nome destes métodos pode ser qualquer um) das linhas 12-34. Poderemos então escrever:
$p=new Personne() ;
$p->setNom("Landru") ;
  • utilizar 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 denominado __construct;

  • linha 59: esta linha apresenta a pessoa $p sob a forma de uma cadeia de caracteres. Para tal, é utilizado o método da classe Personne, denominado __toString (linhas 45-47);
  • todos os métodos da classe (funções) foram precedidos pela palavra-chave public, que indica que a função é visível fora da classe. As outras palavras-chave que podem ser utilizadas são, tal como para os atributos e com o mesmo significado, private e protected. Sem um atributo de visibilidade explícito, 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 através dos seus métodos set ou equivalentes. Para evitar colocar as mesmas verificações em dois locais diferentes, estas podem ser incluídas nos métodos set. Se um valor de inicialização de um objeto se revelar incorreto, será lançada uma exceção. Eis um exemplo.

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


<?php

// respeito rigoroso pelos 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 próprio não pode estar 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 apelido não pode estar 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) {
    // utiliza-se 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 espaço de nomes (namespace). O nome completo (denominado qualifié) da classe Personne passa a ser, então, \Exemples\Personne. Repare no carácter \ que inicia o nome qualificado: temos, assim, um nome qualificado absolu. Se este carácter estiver ausente, temos um nome qualificado relatif (relativo ao espaço de nomes atual). Assim, se duas classes A e B pertencerem ao mesmo espaço de nomes E, no código da classe A será possível aceder à classe B através 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á referenciada através da 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. Por isso, vamos fazê-lo. Além disso, os espaços de nomes devem corresponder à estrutura hierárquica dos ficheiros. Assim, a classe A num espaço de nomes E1 deve estar num ficheiro E1/A.php. Não é obrigatório, mas, mais uma vez, o NetBeans emite um aviso se isso não for feito. No exemplo da classe [\Exemples\Personne], o NetBeans emite um aviso porque a estrutura de diretórios do ficheiro [Personne.php] é [exemples/classes/Personne.php] e, portanto, não corresponde ao espaço de nomes. Não se deve confundir árvore de diretórios 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 árvore de diretórios do ficheiro PHP da classe. A ligação entre a árvore de diretórios e o espaço de nomes é opcional e pode não ser respeitada, tal 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) elimina os espaços que se encontram no início e no fim de $chaine. Assim, trim(«abcd ») resulta na cadeia «abcd» e trim(« ») resulta na cadeia vazia;
  • linha 32: se o nome próprio estiver vazio, é lançada uma exceção (linha 33); caso contrário, o nome próprio é guardado (linha 35). Para lançar uma exceção, utilizou-se aqui a classe predefinida [Exception]. Aqui, é obrigatório utilizar o seu nome absoluto [\Exception]. Se utilizarmos o 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 irá 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 rigoroso do 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 desta 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 incorreto
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 vai utilizar a classe [Personne]. É, portanto, necessário indicar ao interpretador PHP onde pode encontrar a definição dessa classe. É essa a função das instruções [include fichier] e [require fichier]. Neste caso, utilizou-se a instrução [include]. A diferença entre as duas instruções é a seguinte: se a instrução [include fichier] encontrar erros ao carregar a [fichier], é emitido um erro de nível [E_WARNING], 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 tem uma variante: [include_once] e [require_once]. Estas duas variantes permitem gerir o caso de inclusões múltiplas de um mesmo ficheiro. Podemos imaginar aqui um projeto constituído por vários scripts PHP, dos quais vários fazem referência à classe [Personne]. A sua execução provocará, então, a inclusão do ficheiro [Personne.php] várias vezes e causará um erro, uma vez que uma classe não pode ser definida duas vezes. A solução consiste em utilizar as variantes [_once], que garantem que o ficheiro só será incluído uma vez no script global do projeto;
  • linha 7: a constante [__DIR__] é uma constante PHP que designa o nome completo da pasta na qual se encontra o script que contém 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 ficheiro, podem ser utilizados indistintamente os sinais / e \;

  • linha 14: utiliza-se a classe [Personne] que acabámos de definir. O script [classes-05.php] não tem espaços de nomes. A linha 14 utiliza o nome relativo da classe [Personne] sem espaço de nomes. Na ausência de um espaço de nomes para a classe [Personne], esta é procurada no 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 [\Exemples\Personne];
    • utilizar a instrução «use» da linha 10. Esta indica que o código que se segue utiliza a classe [\Exemples\Personne];
  • linha 10: a instrução «use» permite ao interpretador saber que a classe [Personne] referenciada na linha 14 é, na realidade, a classe [\Exemples\Personne]. Dito isto, onde é que o interpretador vai encontrar o código desta classe? É a linha 7 que lhe indica isso. Esta linha indica que, para executar o script atual, é necessário carregar também o script [Personne.php]. Aqui, utilizou-se o nome relativo do ficheiro. Por conseguinte, este será procurado na pasta que contém o script [classes-05.php]. Por isso, os scripts [Personne.php] e [classes-05.php] têm de estar na mesma pasta. É o que acontece aqui, onde ambos se encontram 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] remete para a classe [\Exemples\Personne];

  • linha 14: é criado um objeto [Personne]. É o método [__construct] da classe [Personne] que será aqui executado implicitamente;
  • linha 16: exibe a Pessoa $p. Para ser exibido, o valor da variável $p tem de ser convertido numa cadeia de caracteres. Implicitamente, é o método [Personne.__toString] que é então executado. Este deve, portanto, devolver uma cadeia de caracteres;
  • vimos que o construtor da classe [Personne] pode lançar uma exceção do tipo [\Exception]. É, portanto, necessário gerir essa exceção. Assim, o código da linha 14 está incompleto. É necessário utilizar o código das linhas 18 a 24 para gerir corretamente a exceção que pode ocorrer. Aqui, geramos deliberadamente uma exceção ao passar uma idade que não é um número inteiro. Neste caso específico, a exceção que ocorre é lançada pelo interpretador PHP e não pelo código da classe [Personne]. Com efeito, a assinatura do método [Personne.__construct] é a seguinte:

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

Por conseguinte, o parâmetro [age] passado ao construtor deve ser do tipo inteiro. Caso contrário, o interpretador PHP gera um erro do tipo [TypeError]. Por outro lado, os métodos [set] da classe [Personne] lançam, por sua vez, 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. Note-se que, quando o programador tem a certeza de que certas exceções não podem ocorrer, não utilizará as opções catch correspondentes. Aqui, estas 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 esta razão, a estrutura try das linhas 18-24 tem dois catch para gerir separadamente os dois tipos de exceção;

  • linha 20: pode escrever-se indistintamente [Exception] ou [\Exception]:
    • a primeira versão utiliza o nome relativo da classe, relativo ao espaço de nomes do script. Este não possui espaço de nomes. O seu espaço de nomes é, portanto, a raiz dos espaços de nomes: \. Assim, escrever aqui [Exception] equivale a escrever [\Exception]. Ora, a classe [Exception] encontra-se efetivamente no espaço de nomes [\];

É preferível utilizar o nome absoluto das exceções predefinidas de PHP num script que, por si só, não possua espaços de nomes. Assim, se se decidir atribuir um espaço de nomes a este script, a utilização dos nomes absolutos das classes continua a ser válida, enquanto que, no outro caso, a alteração do espaço de nomes provocará 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], escreveu-se:

  public function setPrenom(string $prénom) {
    // o nome próprio não pode estar vazio
    $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]. É esta mensagem 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 funcione como segundo construtor

No PHP 7, não é possível ter vários construtores com parâmetros diferentes que permitam construir um objeto de diversas formas. Por isso, podem ser utilizados 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, recorre ao construtor __construct, mas isso não é obrigatório. Poderia inicializar ela 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 desta 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: é frequente 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 se interrompe a execução do script, mas sim se apresenta uma página de erro. Se estivermos numa função, não é a instrução exit que será utilizada, 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 seguinte, [classes-07.php], mostra que é possível ter tabelas de objetos.


<?php

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

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

// identidade destas 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 tabuleiro com 2 pessoas;
  • linha 12: percurso pela tabela;
  • linha 13: $groupe[$i] é um objeto do tipo Personne. O método [Personne.__toString] é utilizado para o apresentar;

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

Cria-se, num ficheiro [Enseignant.php], a seguinte classe [Enseignant]:


<?php

// respeito rigoroso pelos 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

  // getter e setter

  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 os métodos da sua classe pai;
  • linha 12: a classe Enseignant adiciona um novo atributo, discipline, que lhe é próprio;
  • linha 25: o construtor da classe Enseignant recebe 4 parâmetros:
    • 3 para inicializar a sua classe pai (nome próprio, apelido, idade), linha 27;
    • 1 para a sua própria inicialização (disciplina), 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, passam-se os parâmetros (nome próprio, apelido, idade) ao 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 se tratam as exceções
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70));

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

Comentários

  • linhas 4-5: temos de indicar ao interpretador PHP onde se encontram as duas classes [Enseignant, Personne];
  • linhas 7-8: declaração dos nomes completos das duas classes. Isto permitir-nos-á referir-se a elas no código simplesmente pelo seu nome, sem o sufixo dos seus espaços de nomes;
  • linha 13: criamos um array com um tipo [Personne] e um tipo [Enseignant];
  • linhas 16-18: apresentam os elementos da tabela;
  • linha 17: o método __toString de cada elemento $groupe[$i] será chamado. A classe Personne possui um método __toString. A classe Enseignant tem dois: o da sua classe pai e o seu próprio. Podemos questionar-nos sobre qual deles será chamado. A execução mostra que foi o da classe Enseignant que foi chamado. É sempre assim: quando um método é chamado num objeto, este é procurado pela seguinte ordem: no próprio objeto, na sua classe pai, se houver, depois na classe pai da classe pai, etc. A procura termina assim que o método for 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 Etudiant derivada da classe Personne, no ficheiro [Etudiant.php]:


<?php

// respeito rigoroso pelos 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;   // formação frequentada

  // 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]]";
  }

}

Esta 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 um array de objetos «pessoa» e derivados
// para facilitar a compreensão do exemplo, não se tratam as exceções
$groupe = array(new Enseignant("Paul", "Langevin", 48, "anglais"), new Personne("Sylvie", "Lefur", 70), new Etudiant("Steve", "Boer", 23, "iup2 qualité"));

// identidade destas 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 para objetos, o construtor de uma classe derivada chama automaticamente o da sua classe pai. O código seguinte, [classes-16.php], mostra que, com o PHP 7, isso não acontece:


<?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 numa 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 rigoroso pelo 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 este existe, é executado;
  • a linha 32 do código chama o método g do objeto $c2. Como este não existe, é 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 este existe, é executado;

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

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


<?php

// respeito estrito pelo 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 {

  // redefine-se 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 a função 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. Tem de ser obrigatoriamente derivada para poder ser utilizada.

Para que serve uma classe abstrata? Por vezes, temos classes que partilham um ou mais métodos, mas que se diferenciam por outros métodos ou outros atributos. Nesse caso, é aconselhável reunir 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 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: isto significa que a implementação do método M é transferida para as classes filhas;
  • uma vez que a classe pai não está totalmente implementada, também é declarada como abstrata com a mesma palavra-chave «abstract». Isto faz com que a classe já não possa 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;

Eis um exemplo [classes-15.php]:


<?php

// respeito rigoroso do 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. Por isso, tem de ser obrigatoriamente derivada para poder ser utilizada;
  • linhas 19-26: a classe [Classe2] estende a classe [Classe1] e redefine o método g da sua classe pai (linhas 22-24);
  • linhas 29-41: a classe [Classe3] estende a classe [Classe1] e redefine o método g da sua classe pai (linhas 32-34);
  • linhas 37-39: a classe [Classe3] redefine o método f da sua classe pai;
  • linhas 44-49: criam-se dois objetos dos tipos [Classe2] e [Classe3] e chamam-se os 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 provocar um erro
new Classe2();

Comentários

  • linhas 7-9: a palavra-chave «final» torna a classe [Classe1] numa classe final que não pode ser derivada;
  • linhas 12-14: a classe [Classe2] estende a classe final [Classe1], o que constitui 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. Eis um exemplo [classes-12.php]:


<?php

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

// espaço de nomes
namespace Exemples;

// classe principal
Class Classe1 {

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

}

// classe derivada
Class Classe2 extends Classe1 {

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

}

// código - deve provocar um erro
new Classe2();

Comentários

  • linha 13: o método f da classe [Classe1] é declarado final através da 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]. Isto deve provocar 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 associado à classe em que está definido e não aos objetos instâncias da classe. Assim, se a classe C declarar um método estático M, para utilizar este último escrever-se-á:

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

Eis 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 irá 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. Com efeito, se forem criados dois objetos O1 e O2, nenhum deles tem conhecimento do outro. Ter um contador na instância não faz sentido: quando um novo objeto é criado, em que instância se irá incrementar 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 se vão contar os objetos criados, uma vez que a criação de cada novo objeto provoca a execução do construtor;
  • linha 14: repare-se na notação self::$nbObjects para indicar que se está a fazer referência a um atributo estático da classe na qual se encontra o código executado;
  • linhas 13-15: o método estático [say] tem como função apresentar 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

Analisemos 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);
    // visualizaçã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 {
    // visualização filho
    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]. Vemos que esta estende a classe [SomeParent] (linha 19);
  • linha 5: a classe [SomeParent] tem apenas um atributo;
  • linhas 8-17: o método [SomeParent::doTest] tem como objetivo apresentar dois atributos:
    • [$attributeOfParent], que pertence à classe [SomeParent];
    • [$attributeOfChild], que pertence à classe [SomeChild] (linha 21);
  • linhas 10-11: é apresentada a identidade do autor da chamada: iremos, de facto, chamar o método de duas formas 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] tem como objetivo apresentar 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, irá 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]. Podemos, portanto, esperar que a classe filha não veja o atributo da sua classe pai. Este teria de ter, pelo menos, a 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: verifica-se que o objeto que chama o método é do tipo [SomeParent];
  • linha 7: exibição do atributo [$attributeOfParent];
  • linhas 9-10: verifica-se que o atributo [SomeParent::$attributeOfChild] não existe. Por isso, não é apresentado;
  • linhas 11-30: resultados do segundo teste, em que o método [SomeChild::doTest] é chamado;
  • linhas 13-14: verifica-se que o atributo [SomeChild::$attributeOfParent] não existe. Por isso, não é apresentado. Isto é normal: o atributo [SomeParent::$attributeOfParent] é [private] e, por isso, 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: verifica-se 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 apresenta o seu 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 fazer um novo teste com o script [classes-20.php]. O atributo [$attributeOfParent] tem agora uma 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);
    // visualizaçã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 {
    // visualizaçã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] passa a ver o atributo do seu pai [$attributeOfParent]. Isto é normal, uma vez que este 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 continua sem ver o seu próprio atributo [$attributeOfChild] (linhas 23-28);

Na terceira tentativa, o atributo [$attributeOfChild] também tem um âmbito [protected]:


<?php

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

  // método
  public function doTest(): void {
    // quem chama?
    print "parent :\n";
    var_dump($this);
    // visualizaçã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 {
    // visualizaçã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, no interior do elemento pai, o [$this], do tipo [SomeChild] (linhas 15-20), acede ao atributo protegido [$attributeOfChild] da sua própria classe [SomeChild].

O que podemos concluir destes testes?

  • que a instância [$this] de uma classe pai, utilizada num método da classe pai, acede a:
    • os atributos e métodos da classe pai, independentemente da sua visibilidade;
    • não vê nada 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, desde que tenham, pelo menos, a 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, a visibilidade [protected]. Aqueles com a 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: destina-se a devolver uma cadeia de caracteres que representa o objeto que o invoca. Pode ser tentador que essa cadeia seja uma cadeia jSON. Vamos explorar essa possibilidade agora.

Image

Iremos utilizar 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 determinados 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 os seus atributos
    $attributes = \get_object_vars($this);
    var_dump($attributes);
    // retorna a cadeia 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 tabuleiro associativo [$arrayOfAttributes] com chaves 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 sob a forma de um tabuleiro associativo [‘nome’=>’nome1’, ‘nome próprio’=>’nome próprio1’, ‘idade’=>’idade1’, ‘filhos’=>[]];
  • linha 45: devolve-se a cadeia jSON desta tabela de atributos;

Analisemos 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 tabuleiro 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 um array dos dois filhos;
  • linhas 32-33: atribuem-se os dois filhos ao pai;
  • linha 35: a operação [print] procura transformar o objeto [$enfant1] numa cadeia de caracteres. Para tal, utiliza o método [__toString] do objeto. Esperamos, portanto, ver a cadeia jSON do objeto;
  • linhas 38-39: faz-se 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, de facto, a cadeia jSON do objeto [$enfant1];
  • linhas 23-44: o mesmo se aplica ao objeto [$enfant2];
  • linhas 45-112: no caso do pai, é um pouco diferente, uma vez que o seu atributo [enfants] não é NULL, como era no caso dos filhos;
  • linha 112: verifica-se que, na cadeia jSON do pai, faltam os filhos;
  • linhas 79-111: verifica-se que, na tabela de atributos do pai, o filho 1 (linhas 89-98) permaneceu como um objeto, o mesmo se aplicando ao filho 2 (linhas 99-110). Em suma, 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 a sua matriz de atributos;

É possível melhorar esta situação. 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 cadeia 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] devolve o tabuleiro de atributos do objeto que a chama;
  • linhas 25-34: a função [__toString];
  • linha 27: recuperam-se os atributos da classe [Personne] na tabela [$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 pelo seu tabuleiro 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 forma:


<?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 do 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 juntamente 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 este problema da seguinte forma:


<?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 cadeia 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 o 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 dos seus atributos
        // é necessário construir o elemento $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[keyn]
        // onde $keys é a matriz [key1, key2, .., keyn]
        // pega-se na referência da tabela [$attributes]
        $attribute = &$attributes;
        // analisa-se a tabela de chaves
        foreach ($keys as $key) {
          // obtém-se a referência do atributo
          $attribute = &$attribute[$key];
        }
        // aqui, $attribut e $attributesQZXW2HTMLBW2tleTFdZQXQZXW2HTMLBW2tleTJdZQX…[key(n)] são idênticos
        // e partilham a mesma localização na memória
        // o objeto [$value] é substituído pela sua matriz de atributos;
        // é necessário 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 1.º nível da classe
        $attributes += $objectAttributes;
      }
      // talvez em [$objectAttributes] ainda haja objetos
      // estamos a explorar os atributos de [$objectAttributes]
      $this->getRecursiveAttributes($attributes, $objectAttributes, $keys);
    } else {
      if (\is_array($value)) {
        // temos um array — analisamos cada um dos 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);
          // retira-se da tabela $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 da sua classe e pede que isso seja feito de forma recursiva: se um atributo for um objeto ou um tabuleiro de objetos, então cada objeto deve ser substituído pelo seu tabuleiro de atributos no tabuleiro de atributos final da classe;
  • linhas 31-78: a função [getRecursiveAttributes] realiza este trabalho. O código foi comentado. Escrever uma função recursiva é, muitas vezes, algo complexo. É o caso aqui. O leitor não perderá nada se não o compreender. Existem bibliotecas que realizam este trabalho. A chamada recursiva ocorre nas linhas 64 e 72;
  • o interesse deste código reside no facto de não ter sido escrito exclusivamente para a classe [Personne3]. É 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, tal como esta, o método [getAttributes] das linhas 27-29

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


<?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 do 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: são atribuídos 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 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
        }
    ]
}

Conseguimos recuperar corretamente a cadeia jSON de todos os objetos [Personne] que constituem o pai.