SmartObject

SmartObject a îmbunătățit comportamentul obiectelor în PHP timp de ani de zile. Începând cu versiunea PHP 8.4, toate funcțiile sale sunt deja parte a PHP însuși, încheindu-și astfel misiunea istorică de a fi pionier al 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ă la deficiențele modelului de obiecte PHP de atunci. Într-o perioadă în care PHP suferea de numeroase probleme de design de obiecte, a adus o îmbunătățire semnificativă și o simplificare a muncii pentru dezvoltatori. A devenit o parte legendară a framework-ului Nette. Oferea funcționalități pe care PHP le-a dobândit abia mulți ani mai târziu – de la controlul accesului la proprietățile obiectelor până la zahăr sintactic sofisticat. Odată cu apariția PHP 8.4, și-a încheiat misiunea istorică, deoarece toate funcțiile sale au devenit parte nativă a limbajului. A devansat dezvoltarea PHP cu remarcabili 17 ani.

Din punct de vedere tehnic, SmartObject a trecut printr-o evoluție interesantă. Inițial a fost implementat ca o clasă Nette\Object, de la care celelalte clase moșteneau funcționalitatea necesară. O schimbare fundamentală a venit cu PHP 5.4, care a introdus suportul pentru trait-uri. Acest lucru a permis transformarea în forma trait-ului Nette\SmartObject, ceea ce a adus o flexibilitate mai mare – dezvoltatorii puteau utiliza funcționalitatea chiar și în clasele care moșteneau deja de la o altă clasă. În timp ce clasa originală Nette\Object a dispărut odată cu apariția PHP 7.2 (care a interzis denumirea claselor cu cuvântul Object), trait-ul Nette\SmartObject trăiește în continuare.

Să trecem în revistă caracteristicile pe care Nette\Object și, mai târziu, Nette\SmartObject le-au oferit odată. Fiecare dintre aceste funcții, la vremea sa, a reprezentat un pas semnificativ înainte în domeniul programării orientate pe obiecte în PHP.

Stări de eroare consistente

Una dintre cele mai arzătoare 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 arăta comportamentul original al PHP:

echo $obj->undeclared;    // E_NOTICE, mai târziu E_WARNING
$obj->undeclared = 1;     // trece silențios fără raportare
$obj->unknownMethod();    // Eroare fatală (neinterceptabilă cu try/catch)

Eroarea fatală încheia aplicația fără nicio posibilitate de a reacționa. Scrierea silențioasă în membrii inexistenți fără avertisment putea duce la erori grave, greu de detectat. Nette\Object intercepta toate aceste cazuri și arunca excepția MemberAccessException, permițând programatorilor să reacționeze la erori și să le rezolve.

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

Începând cu PHP 7.0, limbajul nu mai provoacă erori fatale neinterceptabile, iar de la PHP 8.2, accesul la membrii nedeclarați este considerat o eroare.

Ajutor “Did you mean?”

Nette\Object a venit cu o funcție foarte plăcută: ajutor inteligent pentru greșelile de tipar. Când un dezvoltator făcea o greșeală în numele unei metode sau variabile, nu numai că raporta eroarea, dar oferea și o mână de ajutor sub forma unei sugestii a numelui corect. Acest mesaj iconic, cunoscut sub numele de “did you mean?”, a economisit programatorilor ore întregi de căutare a greșelilor de tipar:

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()?"

PHP-ul de astăzi nu are nicio formă de „did you mean?”, dar această adăugire poate fi completată în erori de către Tracy. Și chiar corectează automat astfel de erori.

Proprietăți cu acces controlat

O inovație semnificativă adusă de SmartObject în PHP au fost proprietățile cu acces controlat. Acest concept, comun în limbaje precum C# sau Python, a permis dezvoltatorilor să controleze elegant accesul la datele obiectului și să asigure consistența acestora. Proprietățile sunt un instrument puternic al programării orientate pe obiecte. Funcționează ca variabile, dar în realitate sunt reprezentate de metode (gettere și settere). Acest lucru permite validarea intrărilor sau generarea valorilor doar în momentul citirii.

Pentru a utiliza proprietățile, trebuie să:

  • Adăugați clasei o adnotare de forma @property <type> $xyz
  • Creați un getter cu numele getXyz() sau isXyz(), un setter cu numele setXyz()
  • Asigurați-vă că getter-ul și setter-ul sunt public sau protected. Sunt opționale – pot exista deci ca proprietăți read-only sau write-only

Să vedem un exemplu practic cu clasa Circle, unde vom folosi proprietățile pentru a ne asigura că raza este întotdeauna un număr non-negativ. Vom înlocui public $radius original cu o proprietate:

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

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

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

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

	// getter pentru proprietatea $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()

Începând cu 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;
	}
}

Metode de extensie

Nette\Object a adus în PHP un alt concept interesant inspirat de limbajele de programare moderne – metodele de extensie. Această funcție, preluată din C#, a permis dezvoltatorilor să extindă elegant clasele existente cu noi metode fără a fi nevoie să le modifice sau să moștenească de la ele. De exemplu, ați putea adăuga la formular o metodă addDateTime() care adaugă un DateTimePicker personalizat:

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

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

Metodele de extensie s-au dovedit a fi nepractice, deoarece numele lor nu erau sugerate de editori, dimpotrivă, raportau că metoda nu există. De aceea, suportul lor a fost întrerupt. Astăzi, este mai comun să se utilizeze compoziția sau moștenirea pentru extinderea funcționalității claselor.

Obținerea numelui clasei

Pentru a obține numele clasei, SmartObject oferea o metodă simplă:

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

Acces la reflecție și adnotări

Nette\Object oferea acces la reflecție și adnotări prin metodele getReflection() și getAnnotation(). Acest acces a simplificat semnificativ lucrul cu metainformațiile claselor:

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

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

Începând cu PHP 8.0, este posibil să accesați metainformații sub formă de atribute, care oferă și mai multe posibilități și un control mai bun al tipurilor:

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

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

Getter-e de metode

Nette\Object oferea o modalitate elegantă de a transmite metode ca și cum ar fi 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

Începând cu PHP 8.1, este posibil să utilizați așa-numita 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 stării 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ătorul ciclu:

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

Din motive de claritate, recomandăm evitarea metodei magice $this->onChange(). Un înlocuitor practic este, de exemplu, funcția Nette\Utils\Arrays::invoke:

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