Property Hooks no PHP 8.4
Marcos Marcolin • April 15, 2024
php phprfc php84 opensourceProperty Hooks
Há algum tempo, a comunidade vem discutindo a implementação dos Property Hooks
, ou Ganchos de Propriedade
, como são chamados em português.
Agora, chegou-se a um consenso e sua implementação estará disponível no PHP 8.4.
Essa funcionalidade foi inspirada em outras linguagens, como C#
, Kotlin
e Swift
.
Este artigo é um resumo da RFC
criada por Ilija Tovilo e Larry Garfield. O link para a RFC completa está disponível no final deste post.
Destaquei os pontos mais interessantes neste momento, mas posso escrever outro artigo abordando o que ficou de fora.
Motivação
Quando precisamos encapsular uma propriedade de classe, geralmente recorremos à criação de getter
e setter
, certo?
Se essa foi a sua primeira ideia, estamos na mesma página.
Por exemplo, se temos uma classe User
com o atributo $name
, é comum criar métodos getName()
e setName()
para acessar e modificar essa propriedade.
Assim, o exemplo mencionado acima seria representado da seguinte forma:
class User {
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function getName(): string {
return $this->name;
}
public function setName(string $name): void {
$this->setName = $name
}
}
No PHP 8.0, podemos simplificar ainda mais ao usar Constructor Property Promotion
, que nos permite definir a propriedade diretamente nos argumentos do construtor.
class User {
public function __construct(public string $name) {
$this->name = $name;
}
}
Fique atento, pois o atributo é public
, caso precise ser private
, não haverá como evitar a necessidade de getter
e setter
.
Além disso, podemos recorrer aos métodos mágicos __set
e __get
, no entanto, eles são executados para todos os atributos das classes,
o que significa que seria necessário verificar qual atributo está sendo escrito ou lido no momento e, se necessário,
aplicar validações específicas. Isso resulta em um código confuso e de difícil leitura.
class User
{
private string $name;
public function __construct(string $name) {
$this->name = $name;
}
public function __get(string $prop): mixed {
return match ($prop) {
'name' => $this->name,
default => throw new Error("Propriedade $prop não definida."),
};
}
public function __set(string $prop, $value): void {
switch ($prop) {
case 'name':
if (! is_string($value)) {
throw new TypeError('Nome deve ser uma string');
}
if (strlen($value) === 0) {
throw new ValueError('Nome não pode ser vazio');
}
$this->name = $value;
break;
default:
throw new Error("Erro ao definir a propriedade $prop");
}
}
}
Você notou como o código pode se tornar confuso? Agora, imagine se a classe tiver várias propriedades.
Utilização
Os Ganchos de Propriedade
foram projetados para facilitar a aplicação de comportamentos adicionais de forma específica para uma única propriedade.
Por exemplo, podemos verificar se o $name
está vazio.
class User
{
public string $name {
set {
if (strlen($value) === 0) {
throw new ValueError("Nome não pode ser vazio");
}
$this->name = $value;
}
get {
return strtoupper($this->name);
}
}
public function __construct(string $name) {
$this->name = $name;
}
}
Atualmente, a RFC
introduziu dois ganchos, get
e set
, mas o design desenvolvido é flexível o suficiente para permitir a inclusão de mais ganchos no futuro.
Além disso, a sintaxe proposta também suporta uma forma abreviada ou sintaxe curta.
class User
{
public string $name {
get => strtoupper($this->name);
}
public function __construct(string $name) {
$this->name = $name;
}
}
Interfaces
Essa funcionalidade também pode ser utilizada dentro de Interfaces
.
interface Named
{
public string $name { get; }
}
class User implements Named
{
public function __construct(public readonly string $name) {}
}
get
O gancho get
se definido, substitui o comportamento padrão de leitura do PHP.
class User
{
public function __construct(private string $firstName, private string $lastName) {}
public string $fullName {
get {
return $this->firstName . " " . $this->lastName;
}
}
}
$user = new User('Marcos', 'Marcolin');
echo $user->fullName; // Marcos Marcolin
Obviamente, os tipos são validados em tempo de execução, portanto, o gancho deve retornar o tipo definido na propriedade.
No exemplo mencionado, $fullName
não é uma propriedade gravável, então resultará em um erro ao tentar escrever nela.
No exemplo a seguir, a escrita é permitida, então podemos fazer:
class User
{
public string $name {
get {
return strtoupper($this->name);
}
}
}
$user = new User();
$user->name = 'marcos'; // O valor salvo será 'marcos'
echo $user->name; // A saída será 'MARCOS'
set
O gancho set
se definido, substitui o comportamento padrão de gravação do PHP.
class User
{
public function __construct(private string $firstName, private string $lastName) {}
public string $fullName {
set (string $value) {
[$this->firstName, $this->lastName)] = explode(' ', $value, 2);
}
}
public function getFirst(): string {
return $this->firstName;
}
}
$user = new User('Marcos', 'Marcolin');
$user->fullName = 'Rasmus Lerdorf';
echo $user->getFirst(); // Rasmus
O gancho aceita apenas um argumento, se específicado, deve informar o tipo e nome do parâmetro.
set implícito
Se o tipo da propriedade for definida, pode-se o omitir o tipo do argumento do gancho.
public string $fullName {
set (string $value) {
[$this -> first, $this -> last] = explode ('', $value , 2);
}
}
// Omite o tipo
public string $fullName {
set {
[$this -> first, $this -> last] = explode (' ', $value, 2);
}
}
Se o parâmetro não for especificado, o padrão será $value
.
Como mencionei anteriormente com o get
, também é possível usar a sintaxe curta para o set
.
class User {
public string $username {
set(string $value) {
$this->username = strtolower($value);
}
}
// Sintaxe curta
public string $username {
set => strtolower($value);
}
}
Herança
Uma classe filha pode definir ou redefinir ganchos individuais em uma propriedade ao redefinir a própria propriedade e apenas
os ganchos que deseja substituir. O tipo e a visibilidade da propriedade estão sujeitos às suas próprias regras,
independentemente desta RFC
.
Além disso, uma classe filha pode adicionar ganchos a uma propriedade que não possui nenhum.
class Point
{
public int $x;
public int $y;
}
class PositivePoint extends Point
{
public int $x {
set {
if ($value < 0) {
throw new \InvalidArgumentException('X não pode ser negativo.');
}
$this->x = $value;
}
}
}
Cada gancho substitui as implementações pai independentemente umas das outras.
Ganchos finais
Os ganchos também podem ser declarados final
, nesse caso, não podem ser substituídos.
class User
{
public string $username {
final set => strtolower($value);
}
}
class Manager extends User
{
public string $username {
// Isso é permitido
get => strtoupper($this->username);
// Isso não é permtido
set => strtoupper($value);
}
}
Bom, eu vou parando por aqui. :)
Espero que tenham gostado desta breve introdução aos Property Hooks
.
Você pode ler mais sobre a proposta na publicação oficial: clique aqui.
Até a próxima e abraços! 🐘