SmartObject

A SmartObject éveken át fejlesztette a PHP objektumok viselkedését. A PHP 8.4 verziótól kezdve minden funkciója beépült magába a PHP nyelvbe, így teljesítve történelmi küldetését a modern objektumorientált megközelítés úttörőjeként.

Telepítés:

composer require nette/utils

A SmartObject 2007-ben jött létre, mint forradalmi megoldás az akkori PHP objektummodell hiányosságaira. Amikor a PHP számos objektumorientált tervezési problémával küzdött, jelentős fejlesztéseket és egyszerűsítéseket hozott a fejlesztők munkájába. A Nette framework legendás részévé vált. Olyan funkcionalitást kínált, amelyet a PHP csak sok évvel később ért el – az objektumtulajdonságok validálásától a kifinomult hibakezelésig. A PHP 8.4 megjelenésével befejezte történelmi küldetését, mivel minden funkciója a nyelv natív részévé vált. Figyelemreméltó módon 17 évvel előzte meg a PHP fejlődését.

A SmartObject érdekes technikai fejlődésen ment keresztül. Eredetileg Nette\Object osztályként implementálták, amelyből más osztályok örökölték a szükséges funkcionalitást. Jelentős változás jött a PHP 5.4-gyel, amely bevezette a trait-ek támogatását. Ez lehetővé tette az átalakulást Nette\SmartObject trait-té, ami nagyobb rugalmasságot hozott – a fejlesztők olyan osztályokban is használhatták a funkcionalitást, amelyek már örököltek egy másik osztályból. Míg az eredeti Nette\Object osztály megszűnt a PHP 7.2 megjelenésével (amely megtiltotta az osztályok ‘Object’ szóval való elnevezését), a Nette\SmartObject trait tovább él.

Nézzük át azokat a tulajdonságokat, amelyeket egykor a Nette\Object, majd később a Nette\SmartObject kínált. Mindegyik funkció jelentős előrelépést jelentett a PHP objektumorientált programozásában a maga idejében.

Konzisztens hibaállapotok

A korai PHP egyik legégetőbb problémája az objektumokkal való munka következetlen viselkedése volt. A Nette\Object rendet és kiszámíthatóságot hozott ebbe a káoszba. Nézzük meg, hogyan működött eredetileg a PHP:

echo $obj->undeclared;    // E_NOTICE, később E_WARNING
$obj->undeclared = 1;     // csendben lefut, figyelmeztetés nélkül
$obj->unknownMethod();    // Fatal error (nem elfogható try/catch-csel)

A Fatal error leállította az alkalmazást anélkül, hogy bármilyen reakcióra lehetőség lett volna. A nem létező tagok csendes írása figyelmeztetés nélkül súlyos hibákhoz vezethetett, amelyeket nehéz volt felderíteni. A Nette\Object minden ilyen esetet elkapott és MemberAccessException kivételt dobott, lehetővé téve a programozóknak, hogy reagáljanak és kezeljék ezeket a hibákat:

echo $obj->undeclared;   // Nette\MemberAccessException kivételt dob
$obj->undeclared = 1;    // Nette\MemberAccessException kivételt dob
$obj->unknownMethod();   // Nette\MemberAccessException kivételt dob

A PHP 7.0 óta a nyelv már nem okoz elfoghatatlan fatal error-t, és a PHP 8.2 óta a nem deklarált tagok elérése hibaként kezelendő.

“Did you mean?” segítség

A Nette\Object egy nagyon hasznos funkcióval érkezett: intelligens javaslatokkal elgépelések esetén. Amikor egy fejlesztő hibát vétett egy metódus vagy változó nevében, nemcsak jelezte a hibát, hanem segítséget is nyújtott a helyes név javaslatával. Ez az ikonikus üzenet, amely “did you mean?” néven vált ismertté, órákat spórolt meg a programozóknak az elgépelések keresésében:

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

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

Bár a mai PHP-nek nincs beépített “did you mean?” funkciója, ezt a kiegészítést a Tracy biztosítja. Sőt, az ilyen hibákat automatikusan javítani is tudja.

Tulajdonságok ellenőrzött hozzáféréssel

Jelentős újítás, amit a SmartObject hozott a PHP-ba, az ellenőrzött hozzáféréssel rendelkező tulajdonságok voltak. Ez a koncepció, amely olyan nyelvekben általános, mint a C# vagy Python, lehetővé tette a fejlesztőknek, hogy elegánsan szabályozzák az objektumadatok elérését és biztosítsák azok konzisztenciáját. A tulajdonságok az objektumorientált programozás hatékony eszközei. Változókként működnek, de valójában metódusok (getter-ek és setter-ek) reprezentálják őket. Ez lehetővé teszi a bemenetek validálását vagy az értékek generálását olvasás időpontjában.

A tulajdonságok használatához szükséges:

  • A @property <type> $xyz annotáció hozzáadása az osztályhoz
  • Getter létrehozása getXyz() vagy isXyz() néven, setter létrehozása setXyz() néven
  • A getter és setter public vagy protected láthatóságú legyen. Opcionálisak – így létezhetnek csak olvasható vagy csak írható tulajdonságokként

Nézzünk egy gyakorlati példát a Circle osztályon, ahol a tulajdonságokat használjuk annak biztosítására, hogy a sugár mindig nem negatív szám legyen. A public $radius-t tulajdonságra cseréljük:

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

	private float $radius = 0.0; // nem public!

	// getter a $radius tulajdonsághoz
	protected function getRadius(): float
	{
		return $this->radius;
	}

	// setter a $radius tulajdonsághoz
	protected function setRadius(float $radius): void
	{
		// érték sanitizálása mentés előtt
		$this->radius = max(0.0, $radius);
	}

	// getter a $visible tulajdonsághoz
	protected function isVisible(): bool
	{
		return $this->radius > 0;
	}
}

$circle = new Circle;
$circle->radius = 10;  // valójában a setRadius(10)-et hívja
echo $circle->radius;  // getRadius()-t hív
echo $circle->visible; // isVisible()-t hív

A PHP 8.4 óta ugyanez a funkcionalitás elérhető property hooks segítségével, amely sokkal elegánsabb és tömörebb szintaxist kínál:

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

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

Extension methods (Kiterjesztő metódusok)

A Nette\Object egy másik érdekes koncepciót hozott a PHP-ba a modern programozási nyelvekből – az extension methods-t. Ez a C#-ból kölcsönzött funkció lehetővé tette a fejlesztőknek, hogy elegánsan bővítsék a meglévő osztályokat új metódusokkal anélkül, hogy módosítaniuk kellene azokat vagy örökölniük kellene belőlük. Például hozzáadhattak egy addDateTime() metódust egy űrlaphoz, amely egy egyéni DateTimePicker-t ad hozzá:

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

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

Az extension metódusok nem bizonyultak praktikusnak, mert az editorok nem ajánlották fel a nevüket, sőt jelezték, hogy a metódus nem létezik. Ezért a támogatásuk megszűnt. Ma már általánosabb a kompozíció vagy öröklődés használata az osztályfunkcionalitás kiterjesztésére.

Osztálynév lekérdezése

A SmartObject egyszerű metódust kínált az osztálynév lekérdezésére:

$class = $obj->getClass(); // Nette\Object használatával
$class = $obj::class;      // PHP 8.0 óta

Reflexió és annotáció hozzáférés

A Nette\Object hozzáférést biztosított a reflexióhoz és az annotációkhoz a getReflection() és getAnnotation() metódusok segítségével. Ez a megközelítés jelentősen egyszerűsítette az osztály metainformációkkal való munkát:

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

$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // 'John Doe'-t ad vissza

A PHP 8.0 óta lehetséges a metainformációk elérése attribútumok formájában, amelyek még több lehetőséget és jobb típusellenőrzést kínálnak:

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

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

Method getterek

A Nette\Object elegáns módot kínált metódusok átadására, mintha változók lennének:

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

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

A PHP 8.1 óta használható az úgynevezett first-class callable syntax, amely még tovább viszi ezt a koncepciót:

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

Események

A SmartObject egyszerűsített szintaxist kínál az események kezeléséhez. Az események lehetővé teszik az objektumoknak, hogy értesítsék az alkalmazás többi részét állapotuk változásairól:

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

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

A $this->onChange($this, $radius) kód egyenértékű a következő ciklussal:

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

Az átláthatóság érdekében javasoljuk a $this->onChange() mágikus metódus kerülését. Praktikus helyettesítője a Nette\Utils\Arrays::invoke függvény:

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