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