SmartObject
O SmartObject aprimorou o comportamento dos objetos em PHP durante muitos anos. Desde o PHP 8.4, todas as suas funcionalidades se tornaram parte nativa do próprio PHP, concluindo assim sua missão histórica como pioneiro da abordagem orientada a objetos moderna em PHP.
Instalação:
composer require nette/utils
O SmartObject surgiu em 2007 como uma solução revolucionária para as limitações do modelo de objetos do PHP da época. Quando o PHP sofria com vários problemas de design orientado a objetos, ele trouxe melhorias significativas e simplificou o trabalho dos desenvolvedores. Tornou-se uma parte lendária do framework Nette. Oferecia funcionalidades que o PHP só obteria muitos anos depois – desde a validação de acesso às propriedades até o tratamento sofisticado de erros. Com a chegada do PHP 8.4, completou sua missão histórica, pois todas as suas funcionalidades se tornaram partes nativas da linguagem. Esteve à frente do desenvolvimento do PHP por notáveis 17 anos.
O SmartObject passou por uma evolução técnica interessante. Inicialmente, foi implementado como a classe
Nette\Object
, da qual outras classes herdavam a funcionalidade necessária. Uma mudança significativa veio com
o PHP 5.4, que introduziu o suporte a traits. Isso permitiu a transformação em um trait Nette\SmartObject
,
trazendo maior flexibilidade – os desenvolvedores podiam usar a funcionalidade mesmo em classes que já herdavam de outra
classe. Enquanto a classe original Nette\Object
deixou de existir com o PHP 7.2 (que proibiu nomear classes com a
palavra ‘Object’), o trait Nette\SmartObject
continua vivo.
Vamos explorar as funcionalidades que o Nette\Object
e depois o Nette\SmartObject
ofereciam. Cada uma
dessas funções representava um passo significativo na programação orientada a objetos em PHP na época.
Estados de Erro Consistentes
Um dos problemas mais críticos do PHP inicial era o comportamento inconsistente ao trabalhar com objetos. O
Nette\Object
trouxe ordem e previsibilidade a esse caos. Vejamos como o PHP se comportava originalmente:
echo $obj->undeclared; // E_NOTICE, depois E_WARNING
$obj->undeclared = 1; // passa silenciosamente sem aviso
$obj->unknownMethod(); // Fatal error (não capturável por try/catch)
O fatal error terminava a aplicação sem possibilidade de reação. A escrita silenciosa em membros inexistentes sem aviso
poderia levar a erros graves difíceis de detectar. O Nette\Object
capturava todos esses casos e lançava uma
MemberAccessException
, permitindo que os programadores reagissem e tratassem esses erros:
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 fatal errors não capturáveis, e desde o PHP 8.2, o acesso a membros não declarados é considerado um erro.
Sugestão “Did you mean?”
O Nette\Object
veio com uma funcionalidade muito conveniente: sugestões inteligentes 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
ajuda sugerindo o nome correto. Esta mensagem icônica, conhecida como “did you mean?”, poupou horas de busca por erros de
digitação:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// lança Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"
Embora o PHP em si não tenha nenhuma forma de “did you mean?”, essa funcionalidade agora é fornecida pelo Tracy. E ele pode até mesmo corrigir automaticamente esses erros.
Properties com Acesso Controlado
Uma inovação significativa que o SmartObject trouxe ao PHP foram as properties com acesso controlado. Esse conceito, comum em linguagens como C# ou Python, permitiu que os desenvolvedores controlassem elegantemente o acesso aos dados do objeto e garantissem sua consistência. Properties são uma ferramenta poderosa da programação orientada a objetos. Funcionam como variáveis, mas na verdade são representadas por métodos (getters e setters). Isso permite validar entradas ou gerar valores no momento da leitura.
Para usar properties, você deve:
- Adicionar a anotação
@property <type> $xyz
à classe - Criar um getter chamado
getXyz()
ouisXyz()
, um setter chamadosetXyz()
- Garantir que o getter e setter sejam public ou protected. Eles são opcionais – assim podem existir como properties read-only ou write-only
Vejamos um exemplo prático usando a classe Circle, onde usaremos properties para garantir que o raio seja sempre não
negativo. Substituiremos public $radius
por uma property:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // não é public!
// getter para a property $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter para a property $radius
protected function setRadius(float $radius): void
{
// sanitiza o valor antes de salvar
$this->radius = max(0.0, $radius);
}
// getter para a property $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;
}
}
Extension Methods
O Nette\Object
trouxe outro conceito interessante ao PHP inspirado em linguagens de programação modernas –
extension methods. Essa funcionalidade, emprestada do C#, permitia que os desenvolvedores estendessem elegantemente classes
existentes com novos métodos sem modificá-las ou herdar delas. Por exemplo, você poderia adicionar um método
addDateTime()
a um 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');
As extension methods se mostraram impráticas porque os editores não sugeriam seus nomes e, em vez disso, relatavam que o método não existia. Portanto, seu suporte foi descontinuado. Hoje, é mais comum usar composição ou herança para estender a funcionalidade da classe.
Obtendo o Nome da Classe
O SmartObject oferecia um método simples para obter o nome da classe:
$class = $obj->getClass(); // usando Nette\Object
$class = $obj::class; // desde PHP 8.0
Acesso à Reflexão e Anotações
O Nette\Object
fornecia acesso à reflexão e anotações através dos métodos getReflection()
e
getAnnotation()
. Essa abordagem simplificou significativamente o trabalho com metainformações de classe:
/**
* @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 através de atributos, que oferecem ainda mais possibilidades e melhor verificação de tipo:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Method Getters
O 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, você pode usar a first-class callable syntax, que leva esse conceito ainda mais longe:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Eventos
O 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 loop:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Para maior clareza, recomendamos evitar o método mágico $this->onChange()
. Uma substituição prática é a
função Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);