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() ou isXyz(), um setter chamado setXyz()
  • 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);
versão: 4.0