Ganchos de propriedade, também conhecidos como "acessadores de propriedade" em algumas outras linguagens, são uma forma de interceptar e substituir o comportamento de leitura e gravação de uma propriedade. Esta funcionalidade tem dois propósitos:
Existem dois ganchos disponíveis em propriedades não estáticas: get
e set
.
Eles permitem substituir o comportamento de leitura e gravação de uma propriedade, respectivamente.
Os ganchos estão disponíveis para propriedades tipadas e não tipadas.
Uma propriedade pode ser "apoiada" ou "virtual". Uma propriedade apoiada é aquela que realmente armazena um valor. Qualquer propriedade que não tenha ganchos será apoiada. Uma propriedade virtual é aquela que possui ganchos e esses ganchos não interagem com a propriedade em si. Nesse caso, os ganchos são efetivamente iguais aos métodos, e o objeto não usa nenhum espaço para armazenar um valor para aquela propriedade.
Ganchos de propriedade são incompatíveis com propriedades readonly
(somente-leitura).
Se houver necessidade de restringir acesso a uma operação get
ou set
além de alterar seu comportamente, deve ser usada a
visibilidade de propriedade assimétrica.
A sintaxe geral para declarar um gancho é a seguinte.
Exemplo #1 Ganchos de propriedade (versão completa)
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get {
if ($this->modified) {
return $this->foo . ' (modified)';
}
return $this->foo;
}
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
$example = new Example();
$example->foo = 'changed';
print $example->foo;
?>
A propriedade $foo termina com {}
, em vez de ponto e vírgula.
Isso indica a presença de ganchos.
Ambos os ganchos get
e set
são definidos,
embora seja permitido definir apenas um ou outro.
Ambos os ganchos possuem um corpo, indicado por {}
, que pode conter código arbitrário.
O gancho set
permite adicionalmente especificar o tipo e o nome de um valor recebido,
usando a mesma sintaxe de um método.
O tipo deve ser igual ao tipo da propriedade
ou uma contravariante (mais amplo) a ela.
Por exemplo, uma propriedade do tipo string poderia ter um
gancho set
que aceita string|Stringable,
mas não aquele que aceita apenas array.
Pelo menos um dos ganchos faz referência a $this->foo
, a própria propriedade.
Isso significa que a propriedade será "apoiada".
Ao chamar $example->foo = 'changed'
,
a string fornecida será primeiro convertida em minúsculas e depois salva no valor de apoio.
Ao ler a propriedade, o valor salvo anteriormente pode ser anexado condicionalmente
com texto adicional.
Existem também várias variantes de sintaxe abreviada para lidar com casos comuns.
Se o gancho get
for uma expressão única,
{}
poderá ser omitido e substituído por uma expressão de seta.
Exemplo #2 Expressão para leitura de propriedade
Este exemplo é equivalente ao anterior.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set(string $value) {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Se o tipo de parâmetro do gancho set
for igual ao tipo de propriedade (o que é típico),
ele poderá ser omitido. Nesse caso, o valor a ser definido recebe automaticamente o nome $value.
Exemplo #3 Padrão de definição de propriedade
Este exemplo é equivalente ao anterior.
<?php
class Example
{
private bool $modified = false;
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set {
$this->foo = strtolower($value);
$this->modified = true;
}
}
}
?>
Se o gancho set
estiver apenas definindo uma versão modificada do valor passado,
então ele também poderá ser simplificado para uma expressão de seta.
O valor avaliado pela expressão será definido no valor de apoio.
Exemplo #4 Expressão para definição de propriedade
<?php
class Example
{
public string $foo = 'default value' {
get => $this->foo . ($this->modified ? ' (modified)' : '');
set => strtolower($value);
}
}
?>
Este exemplo não é exatamente equivalente ao anterior,
pois também não modifica $this->modified
.
Se várias instruções forem necessárias no corpo do gancho definido, use a versão entre chaves.
Uma propriedade pode implementar zero, um ou ambos os ganchos conforme a situação exigir. Todas as versões abreviadas são mutuamente independentes. Ou seja, usar um 'get' curto com um 'set' longo, ou um 'set' curto com um tipo explícito, ou assim por diante, é válido.
Em uma propriedade apoiada, omitir um gancho get
ouset
significa que o comportamento padrão de leitura ou gravação será usado.
Nota: Gachos podem ser definidos ao usar promoção de propriedade de construtor. No entando, quando isso é feito, os valores fornecidos ao construtor precisam ter o tipo associado à propriedade correspondido, independentemente do que o gancho
set
possa permitir. Considere o seguinte:Internamente, o mecanismo decompõe para o seguinte:class Example
{
public function __construct(
public private(set) DateTimeInterface $created {
set (string|DateTimeInterface $value) {
if (is_string($value)) {
$value = new DateTimeImmutable($value);
}
$this->created = $value;
}
},
) {
}
}Quaisquer tentativas de definir a propriedade fora do construtor permitirão valores do tipo string ou DateTimeInterface mas o construtor permitirá apenas DateTimeInterface. Isto acontece porque o tipo definido para a propriedade (DateTimeInterface) é usado como o tipo do parâmetro dentro da assinatura do construtor, não importando o que o ganchoclass Example
{
public private(set) DateTimeInterface $created {
set (string|DateTimeInterface $value) {
if (is_string($value)) {
$value = new DateTimeImmutable($value);
}
$this->created = $value;
}
}
public function __construct(
DateTimeInterface $created,
) {
$this->created = $created;
}
}set
permite. Se este tipo de comportamento do construtor for necessário, a promoção de propriedade de construtor não pode ser usada.
Propriedades virtuais são propriedades que não possuem valor de apoio.
Uma propriedade é virtual se nem seu gancho get
nem seu gancho set
fizerem referência à própria propriedade usando sintaxe exata.
Ou seja, uma propriedade chamada $foo
cujo gancho contém $this->foo
será apoiada.
Mas a seguinte não é uma propriedade apoiada e causará erro:
Exemplo #5 Propriedade virtual inválida
<?php
class Example
{
public string $foo {
get {
$temp = __PROPERTY__;
return $this->$temp; // Não faz referência a $this->foo, por isso não é válida.
}
}
}
?>
Para propriedades virtuais, se um gancho for omitido, essa operação não existe e tentar usá-la produzirá um erro. As propriedades virtuais não ocupam espaço de memória em um objeto. As propriedades virtuais são adequadas para propriedades "derivadas", como aquelas que são a combinação de duas outras propriedades.
Exemplo #6 Propriedade virtual
<?php
readonly class Rectangle
{
// Uma propriedade virtual.
public int $area {
get => $this->h * $this->w;
}
public function __construct(public int $h, public int $w) {}
}
$s = new Rectangle(4, 5);
print $s->area; // exibe 20
$s->area = 30; // Erro, pois não há operação 'set' definida.
?>
Definir ambos os ganchos get
e set
em uma propriedade virtual também é permitido.
Todos os ganchos operam no escopo do objeto que está sendo modificado. Isso significa que eles têm acesso a todos os métodos públicos, privados ou protegidos do objeto, bem como a quaisquer propriedades públicas, privadas ou protegidas, incluindo propriedades que possam ter seus próprios ganchos de propriedade. Acessar outra propriedade de dentro de um gancho não ignora os ganchos definidos nessa propriedade.
A implicação mais notável disso é que ganchos não triviais podem fazer subchamadas a um método arbitrariamente complexo, se desejarem.
Exemplo #7 Chamando um método a partir de um gancho
<?php
class Person {
public string $phone {
set => $this->sanitizePhone($value);
}
private function sanitizePhone(string $value): string {
$value = ltrim($value, '+');
$value = ltrim($value, '1');
if (!preg_match('/\d\d\d\-\d\d\d\-\d\d\d\d/', $value)) {
throw new \InvalidArgumentException();
}
return $value;
}
}
?>
Como a presença de ganchos intercepta o processo de leitura e gravação de propriedades,
eles causam problemas ao adquirir uma referência a uma propriedade ou com modificação
indireta, como $this->arrayProp['chave'] = 'valor';
.
Isso ocorre porque qualquer tentativa de modificação do valor por referência ignoraria um gancho 'set',
se algum estiver definido.
No caso raro de ser necessário obter uma referência a uma propriedade que tenha ganchos definidos,
o gancho get
pode ser prefixado com &
para fazer com que ele retorne por referência.
Definir get
e &get
na
mesma propriedade é um erro de sintaxe.
Não é permitido definir ganchos &get
e set
em uma propriedade apoiada.
Conforme observado acima, escrever no valor retornado por referência ignoraria o gancho set
.
Nas propriedades virtuais, não há nenhum valor comum necessário compartilhado entre os dois ganchos, portanto, é permitido definir ambos.
Escrever em um índice de uma propriedade de array também envolve uma referência implícita.
Por esse motivo, gravar em uma propriedade de array apoiada com ganchos definidos é
permitido se e somente se ela definir apenas um gancho &get
.
Em uma propriedade virtual, escrever no array retornado de
get
ou &get
é legal,
mas se isso terá algum impacto no objeto depende da implementação do gancho.
Substituir toda a propriedade do array é aceitável e se comporta da mesma forma que qualquer outra propriedade. Trabalhar apenas com elementos do array requer cuidados especiais.
Um gancho pode também ser declarado como final, neste caso ele não pode ser substituído.
Exemplo #8 Ganchos finais
<?php
class User
{
public string $username {
final set => strtolower($value);
}
}
class Manager extends User
{
public string $username {
// Isto é permitido
get => strtoupper($this->username);
// Mas isto NÃO é permitido, porque 'set' é final na classe superior.
set => strtoupper($value);
}
}
?>
Uma propriedade também pode ser declarada como final. Uma propriedade final não pode ser declarada novamente por uma classe filha de forma alguma, o que impede a alteração de ganchos ou a ampliação de seu acesso.
Declarar ganchos como finais em uma propriedade declarada como final é redundante e será silenciosamente ignorado. Este é o mesmo comportamento dos métodos finais.
Uma classe filha pode definir ou redefinir ganchos individuais em uma propriedade redefinindo a propriedade e apenas os ganchos que deseja substituir. Uma classe filha também pode adicionar ganchos a uma propriedade que não possui nenhum. Isto é, essencialmente, como se os ganchos fossem métodos.
Exemplo #9 Herança de gancho
<?php
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Muito pequeno');
}
$this->x = $value;
}
}
}
?>
Cada gancho substitui as implementações pai independentemente umas das outras. Se uma classe filha adicionar ganchos, qualquer valor padrão definido na propriedade será removido e deverá ser declarado novamente. Isso é consistente com o modo como a herança funciona em propriedades sem gancho.
Um gancho em uma classe filha pode acessar a propriedade da classe pai usando a
palavra-chave parent::$prop
, seguida pelo gancho desejado.
Por exemplo, parent::$propName::get()
.
Pode ser lido como "acessar a prop definida na classe pai
e então executar sua operação 'get'" (ou operação 'set', conforme apropriado).
Se não for acessado dessa forma, o gancho da classe pai será ignorado. Esse comportamento é consistente com o funcionamento de todos os métodos. Isso também oferece uma maneira de acessar o armazenamento da classe pai, se houver. Se não houver gancho na propriedade pai, seu comportamento 'get'/'set' padrão será usado. Os ganchos não podem acessar nenhum outro gancho, exceto seu próprio pai em sua própria propriedade.
O exemplo acima poderia ser reescrito de forma mais eficiente da seguinte maneira.
Exemplo #10 Acessando gancho pai ('set')
<?php
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('Muito pequeno');
}
$this->x = $value;
}
}
}
?>
Um exemplo de substituição apenas de um gancho 'get' poderia ser:
Exemplo #11 Acessando gancho pai ('get')
<?php
class Strings
{
public string $val;
}
class CaseFoldingStrings extends Strings
{
public bool $uppercase = true;
public string $val {
get => $this->uppercase
? strtoupper(parent::$val::get())
: strtolower(parent::$val::get());
}
}
?>
O PHP possui diversas maneiras diferentes pelas quais um objeto pode ser serializado, seja para consumo público ou para fins de depuração. O comportamento dos ganchos varia dependendo do caso de uso. Em alguns casos, o valor bruto de apoio de uma propriedade será usado, ignorando quaisquer ganchos. Em outros, a propriedade será lida ou escrita "através" do gancho, assim como qualquer outra ação normal de leitura/gravação.