SmartObject
O SmartObject aprimorou o comportamento dos objetos em PHP por anos. Desde a versão PHP 8.4, todas as suas funções já fazem parte do próprio PHP, completando assim sua missão histórica de ser um pioneiro da abordagem moderna orientada a objetos em PHP.
Instalação:
composer require nette/utils
O SmartObject surgiu em 2007 como uma solução revolucionária para as deficiências do modelo de objetos do PHP da época. Em um tempo em que o PHP sofria de vários problemas com o design de objetos, ele trouxe melhorias significativas e simplificou o trabalho para os desenvolvedores. Tornou-se uma parte lendária do framework Nette. Oferecia funcionalidades que o PHP só adquiriu muitos anos depois – desde o controle de acesso às propriedades dos objetos até sofisticados açúcares sintáticos. Com a chegada do PHP 8.4, ele completou sua missão histórica, pois todas as suas funções se tornaram parte nativa da linguagem. Ele antecipou o desenvolvimento do PHP em notáveis 17 anos.
Tecnicamente, o SmartObject passou por um desenvolvimento interessante. Originalmente, foi implementado como uma classe
Nette\Object
, da qual outras classes herdavam a funcionalidade necessária. Uma mudança fundamental veio com o PHP
5.4, que introduziu o suporte a traits. Isso permitiu a transformação na forma da trait Nette\SmartObject
, o que
trouxe maior flexibilidade – os desenvolvedores podiam usar a funcionalidade mesmo em classes que já herdavam de outra classe.
Enquanto a classe original Nette\Object
desapareceu com a chegada do PHP 7.2 (que proibiu a nomeação de classes com
a palavra Object
), a trait Nette\SmartObject
continua viva.
Vamos analisar as funcionalidades que Nette\Object
e, posteriormente, Nette\SmartObject
ofereceram.
Cada uma dessas funções, em sua época, representou um passo significativo no campo da programação orientada a objetos
em PHP.
Estados de erro consistentes
Um dos problemas mais urgentes do PHP inicial era o comportamento inconsistente ao trabalhar com objetos.
Nette\Object
trouxe ordem e previsibilidade a esse caos. Vejamos como era o comportamento original do PHP:
echo $obj->undeclared; // E_NOTICE, posteriormente E_WARNING
$obj->undeclared = 1; // passa silenciosamente sem aviso
$obj->unknownMethod(); // Fatal error (não capturável com try/catch)
Um erro fatal encerrava a aplicação sem a possibilidade de reagir de forma alguma. A escrita silenciosa em membros
inexistentes sem aviso poderia levar a erros graves que eram difíceis de detectar. Nette\Object
capturava todos
esses casos e lançava a exceção MemberAccessException
, o que permitia aos programadores reagir aos erros e
resolvê-los.
echo $obj->undeclared; // lança Nette\MemberAccessException
$obj->undeclared = 1; // lança Nette\MemberAccessException
$obj->unknownMethod(); // lança Nette\MemberAccessException
Desde o PHP 7.0, a linguagem não causa mais erros fatais não capturáveis e, desde o PHP 8.2, o acesso a membros não declarados é considerado um erro.
Ajuda “Did you mean?”
Nette\Object
introduziu uma função muito agradável: ajuda inteligente para erros de digitação. Quando um
desenvolvedor cometia um erro no nome de um método ou variável, ele não apenas relatava o erro, mas também oferecia uma mão
amiga na forma de uma sugestão do nome correto. Esta mensagem icônica, conhecida como “did you mean?”, economizou horas de
busca por erros de digitação para os programadores:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// lança Nette\MemberAccessException
// "Chamada para método estático indefinido Foo::form(), você quis dizer from()?"
O PHP atual não tem nenhuma forma de “did you mean?”, mas Tracy pode adicionar este adendo aos erros. E até mesmo corrigir automaticamente tais erros.
Propriedades com acesso controlado
Uma inovação significativa que o SmartObject trouxe para o PHP foram as propriedades com acesso controlado. Este conceito, comum em linguagens como C# ou Python, permitiu aos desenvolvedores controlar elegantemente o acesso aos dados do objeto e garantir sua consistência. As propriedades são uma ferramenta poderosa da programação orientada a objetos. Elas funcionam como variáveis, mas na realidade são representadas por métodos (getters e setters). Isso permite validar entradas ou gerar valores apenas no momento da leitura.
Para usar propriedades, você deve:
- Adicionar uma anotação à classe no formato
@property <type> $xyz
- Criar um getter com o nome
getXyz()
ouisXyz()
, um setter com o nomesetXyz()
- Garantir que o getter e o setter sejam public ou protected. Eles são opcionais – podem existir como propriedades read-only ou write-only
Vejamos um exemplo prático na classe Circle, onde usamos propriedades para garantir que o raio seja sempre um número não
negativo. Substituímos o public $radius
original por uma propriedade:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // não é public!
// getter para a propriedade $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter para a propriedade $radius
protected function setRadius(float $radius): void
{
// sanitizamos o valor antes de salvar
$this->radius = max(0.0, $radius);
}
// getter para a propriedade $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // na verdade chama setRadius(10)
echo $circle->radius; // chama getRadius()
echo $circle->visible; // chama isVisible()
Desde o PHP 8.4, a mesma funcionalidade pode ser alcançada usando property hooks, que oferecem uma sintaxe muito mais elegante e concisa:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Métodos de extensão
Nette\Object
trouxe outro conceito interessante para o PHP, inspirado em linguagens de programação
modernas – métodos de extensão. Esta função, emprestada do C#, permitiu aos desenvolvedores estender elegantemente classes
existentes com novos métodos sem a necessidade de modificá-las ou herdar delas. Por exemplo, você poderia adicionar um método
addDateTime()
ao seu formulário, que adiciona um DateTimePicker personalizado:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Os métodos de extensão mostraram-se impraticáveis porque seus nomes não eram sugeridos pelos editores; pelo contrário, relatavam que o método não existia. Portanto, seu suporte foi encerrado. Hoje, é mais comum usar composição ou herança para estender a funcionalidade das classes.
Obtendo o nome da classe
Para obter o nome da classe, o SmartObject oferecia um método simples:
$class = $obj->getClass(); // usando Nette\Object
$class = $obj::class; // desde PHP 8.0
Acesso à reflexão e anotações
Nette\Object
oferecia acesso à reflexão e anotações usando os métodos getReflection()
e
getAnnotation()
. Esta abordagem simplificou significativamente o trabalho com metainformações de classes:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // retorna 'John Doe'
Desde o PHP 8.0, é possível acessar metainformações na forma de atributos, que oferecem ainda mais possibilidades e melhor controle de tipo:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Getters de método
Nette\Object
oferecia uma maneira elegante de passar métodos como se fossem variáveis:
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
Desde o PHP 8.1, é possível usar a chamada sintaxe callable de primeira classe, que leva este conceito ainda mais longe:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Eventos
SmartObject oferece uma sintaxe simplificada para trabalhar com eventos. Eventos permitem que objetos informem outras partes da aplicação sobre mudanças em seu estado:
class Circle extends Nette\Object
{
public array $onChange = [];
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
O código $this->onChange($this, $radius)
é equivalente ao seguinte ciclo:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Por questões de clareza, recomendamos evitar o método mágico $this->onChange()
. Uma substituição prática
é, por exemplo, a função Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);