SmartObject

SmartObject a îmbunătățit comportamentul obiectelor în PHP timp de mulți ani. Începând cu PHP 8.4, toate funcționalitățile sale au devenit parte nativă a PHP-ului, completându-și astfel misiunea istorică de a fi pionierul abordării moderne orientate pe obiecte în PHP.

Instalare:

composer require nette/utils

SmartObject a apărut în 2007 ca o soluție revoluționară pentru deficiențele modelului de obiecte din PHP la acea vreme. Într-o perioadă în care PHP suferea de numeroase probleme legate de design-ul orientat pe obiecte, acesta a adus îmbunătățiri semnificative și a simplificat munca dezvoltatorilor. A devenit o parte legendară a framework-ului Nette. A oferit funcționalități pe care PHP le-a obținut abia mulți ani mai târziu – de la validarea accesului la proprietăți până la gestionarea sofisticată a erorilor. Odată cu apariția PHP 8.4, și-a încheiat misiunea istorică, deoarece toate funcționalitățile sale au devenit părți native ale limbajului. A fost cu 17 ani înaintea dezvoltării PHP-ului.

SmartObject a trecut printr-o evoluție tehnică interesantă. Inițial, a fost implementat ca și clasă Nette\Object, de la care alte clase moșteneau funcționalitatea necesară. O schimbare semnificativă a venit cu PHP 5.4, care a introdus suportul pentru trait-uri. Acest lucru a permis transformarea în trait-ul Nette\SmartObject, oferind mai multă flexibilitate – dezvoltatorii puteau folosi funcționalitatea chiar și în clasele care moșteneau deja dintr-o altă clasă. În timp ce clasa originală Nette\Object a încetat să existe odată cu PHP 7.2 (care a interzis denumirea claselor cu cuvântul ‘Object’), trait-ul Nette\SmartObject continuă să existe.

Să explorăm funcționalitățile pe care Nette\Object și mai târziu Nette\SmartObject le-au oferit. Fiecare dintre aceste funcții a reprezentat un pas important înainte în programarea orientată pe obiecte în PHP.

Stări de eroare consistente

Una dintre cele mai presante probleme ale PHP-ului timpuriu a fost comportamentul inconsistent în lucrul cu obiectele. Nette\Object a adus ordine și predictibilitate în acest haos. Să vedem cum se comporta PHP-ul inițial:

echo $obj->undeclared;    // E_NOTICE, mai târziu E_WARNING
$obj->undeclared = 1;     // trece silențios fără avertisment
$obj->unknownMethod();    // Fatal error (imposibil de prins cu try/catch)

Fatal error termina aplicația fără posibilitatea de a reacționa. Scrierea silențioasă în membri inexistenți fără avertisment putea duce la erori grave, greu de detectat. Nette\Object prindea toate aceste cazuri și arunca o excepție MemberAccessException, permițând programatorilor să reacționeze și să gestioneze aceste erori:

echo $obj->undeclared;   // aruncă Nette\MemberAccessException
$obj->undeclared = 1;    // aruncă Nette\MemberAccessException
$obj->unknownMethod();   // aruncă Nette\MemberAccessException

Din PHP 7.0, limbajul nu mai cauzează fatal error-uri imposibil de prins, iar din PHP 8.2, accesul la membri nedeclarați este considerat o eroare.

Ajutorul “Did you mean?”

Nette\Object a venit cu o funcționalitate foarte convenabilă: sugestii inteligente pentru greșeli de scriere. Când un dezvoltator făcea o greșeală în numele unei metode sau variabile, nu doar raporta eroarea, ci oferea și ajutor sugerând numele corect. Acest mesaj iconic, cunoscut ca “did you mean?”, a economisit programatorilor ore întregi de căutare a greșelilor de scriere:

class Foo extends Nette\Object
{
	public static function from($var)
	{
	}
}

$foo = Foo::form($var);
// aruncă Nette\MemberAccessException
// "Call to undefined static method Foo::form(), did you mean from()?"

Deși PHP-ul în sine nu are nicio formă de “did you mean?”, această funcționalitate este acum oferită de Tracy. Poate chiar auto-corecta astfel de erori.

Properties cu acces controlat

O inovație semnificativă pe care SmartObject a adus-o în PHP au fost properties cu acces controlat. Acest concept, comun în limbaje precum C# sau Python, a permis dezvoltatorilor să controleze elegant accesul la datele obiectelor și să asigure consistența acestora. Properties sunt un instrument puternic al programării orientate pe obiecte. Funcționează ca variabile, dar sunt de fapt reprezentate prin metode (getteri și setteri). Acest lucru permite validarea intrărilor sau generarea valorilor în momentul citirii.

Pentru a utiliza properties, trebuie să:

  • Adăugați adnotarea @property <type> $xyz la clasă
  • Creați un getter numit getXyz() sau isXyz(), un setter numit setXyz()
  • Să vă asigurați că getter-ul și setter-ul sunt public sau protected. Sunt opționali – astfel pot exista ca properties read-only sau write-only

Să vedem un exemplu practic folosind clasa Circle, unde vom folosi properties pentru a ne asigura că raza este întotdeauna non-negativă. Vom înlocui public $radius cu o property:

/**
 * @property float $radius
 * @property-read bool $visible
 */
class Circle
{
	use Nette\SmartObject;

	private float $radius = 0.0; // nu este public!

	// getter pentru property-ul $radius
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter pentru property-ul $radius
	protected function setRadius(float $radius): void
	{
		// sanitizăm valoarea înainte de salvare
		$this->radius = max(0.0, $radius);
	}

	// getter pentru property-ul $visible
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // de fapt apelează setRadius(10)
echo $circle->radius;  // apelează getRadius()
echo $circle->visible; // apelează isVisible()

Din PHP 8.4, aceeași funcționalitate poate fi obținută folosind property hooks, care oferă o sintaxă mult mai elegantă și concisă:

class Circle
{
	public float $radius = 0.0 {
		set => max(0.0, $value);
	}

	public bool $visible {
		get => $this->radius > 0;
	}
}

Extension methods

Nette\Object a adus în PHP un alt concept interesant inspirat din limbajele de programare moderne – extension methods. Această funcționalitate, împrumutată din C#, a permis dezvoltatorilor să extindă elegant clasele existente cu metode noi fără a le modifica sau moșteni. De exemplu, puteați adăuga o metodă addDateTime() unui formular care adaugă un DateTimePicker personalizat:

Form::extensionMethod(
	'addDateTime',
	fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);

$form = new Form;
$form->addDateTime('date');

Extension methods s-au dovedit nepractice deoarece editoarele nu sugerau numele lor și în schimb raportau că metoda nu există. Prin urmare, suportul lor a fost întrerupt. Astăzi, este mai comun să se folosească compoziția sau moștenirea pentru a extinde funcționalitatea clasei.

Obținerea numelui clasei

SmartObject oferea o metodă simplă pentru obținerea numelui clasei:

$class = $obj->getClass(); // folosind Nette\Object
$class = $obj::class;      // din PHP 8.0

Acces la Reflexie și Adnotări

Nette\Object oferea acces la reflexie și adnotări prin metodele getReflection() și getAnnotation(). Această abordare a simplificat semnificativ lucrul cu meta-informațiile claselor:

/**
 * @author John Doe
 */
class Foo extends Nette\Object
{
}

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // returnează 'John Doe'

Din PHP 8.0, este posibil să accesați meta-informațiile prin atribute, care oferă chiar mai multe posibilități și o verificare mai bună a tipurilor:

#[Author('John Doe')]
class Foo
{
}

$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];

Method getteri

Nette\Object oferea o modalitate elegantă de a transmite metode ca și cum ar fi fost variabile:

class Foo extends Nette\Object
{
	public function adder($a, $b)
	{
		return $a + $b;
	}
}

$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5

Din PHP 8.1, puteți utiliza first-class callable syntax, care duce acest concept și mai departe:

$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5

Evenimente

SmartObject oferă o sintaxă simplificată pentru lucrul cu evenimente. Evenimentele permit obiectelor să informeze alte părți ale aplicației despre schimbările din starea lor:

class Circle extends Nette\Object
{
	public array $onChange = [];

	public function setRadius(float $radius): void
	{
		$this->onChange($this, $radius);
		$this->radius = $radius;
	}
}

Codul $this->onChange($this, $radius) este echivalent cu următoarea buclă:

foreach ($this->onChange as $callback) {
	$callback($this, $radius);
}

Pentru claritate, recomandăm evitarea metodei magice $this->onChange(). O înlocuire practică este funcția Nette\Utils\Arrays::invoke:

Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);
versiune: 4.0