SmartObject

A SmartObject évekig javította az objektumok viselkedését PHP-ban. A PHP 8.4 verziótól kezdve minden funkciója a PHP részévé vált, ezzel beteljesítve történelmi küldetését, hogy úttörője legyen a modern objektumorientált megközelítésnek PHP-ban.

Telepítés:

composer require nette/utils

A SmartObject 2007-ben jött létre forradalmi megoldásként a PHP akkori objektummodelljének hiányosságaira. Abban az időben, amikor a PHP számos objektumtervezési problémával küzdött, jelentős javulást és egyszerűsítést hozott a fejlesztők munkájába. A Nette keretrendszer legendás részévé vált. Olyan funkcionalitást kínált, amelyet a PHP csak sok évvel később kapott meg – az objektumtulajdonságokhoz való hozzáférés ellenőrzésétől a kifinomult szintaktikai cukorkákig. A PHP 8.4 megjelenésével beteljesítette történelmi küldetését, mivel minden funkciója a nyelv natív részévé vált. Figyelemre méltó 17 évvel előzte meg a PHP fejlődését.

Technikailag a SmartObject érdekes fejlődésen ment keresztül. Eredetileg Nette\Object osztályként valósult meg, amelyből más osztályok örökölték a szükséges funkcionalitást. Alapvető változás következett be 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 egy másik osztályból örököltek. Míg az eredeti Nette\Object osztály a PHP 7.2 megjelenésével megszűnt (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, később pedig a Nette\SmartObject kínált. Ezen funkciók mindegyike a maga idejében jelentős előrelépést jelentett az objektumorientált programozás területén PHP-ban.

Konzisztens hibaállapotok

A korai PHP egyik legégetőbb problémája az objektumokkal való munka inkonzisztens viselkedése volt. A Nette\Object rendet és kiszámíthatóságot hozott ebbe a káoszba. Nézzük meg, hogyan nézett ki a PHP eredeti viselkedése:

echo $obj->undeclared;    // E_NOTICE, később E_WARNING
$obj->undeclared = 1;     // projde tiše bez hlášení
$obj->unknownMethod();    // Fatal error (nem fogható el try/catch segítségével)

A Fatal error leállította az alkalmazást anélkül, hogy bármilyen módon reagálni lehetett volna rá. A nem létező tagokba történő csendes írás figyelmeztetés nélkül súlyos, nehezen felderíthető hibákhoz vezethetett. A Nette\Object mindezeket az eseteket elfogta és MemberAccessException kivételt dobott, ami lehetővé tette a programozóknak, hogy reagáljanak a hibákra és kezeljék azokat.

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 tagokhoz való hozzáférés hibának minősül.

“Did you mean?” súgó

A Nette\Object egy nagyon kellemes funkcióval érkezett: intelligens súgóval elgépelések esetén. Amikor a fejlesztő hibát vétett egy metódus vagy változó nevében, nemcsak a hibát jelezte, hanem segítő kezet is nyújtott a helyes név javaslatának formájában. Ez az ikonikus üzenet, amelyet “did you mean?” néven ismerünk, órákat spórolt meg a programozóknak az elgépelések keresésével:

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

A mai PHP ugyan nem rendelkezik „did you mean?” funkcióval, de ezt a kiegészítést a Tracy hozzá tudja adni a hibákhoz. Sőt, az ilyen hibákat automatikusan ki is tudja javítani.

Property-k ellenőrzött hozzáféréssel

Jelentős innováció, amelyet a SmartObject hozott a PHP-ba, az ellenőrzött hozzáféréssel rendelkező property-k voltak. Ez a koncepció, amely a C# vagy Python nyelvekben elterjedt, lehetővé tette a fejlesztők számára, hogy elegánsan ellenőrizzék az objektum adataihoz való hozzáférést és biztosítsák azok konzisztenciáját. A property-k az objektumorientált programozás erőteljes eszközei. Változókként működnek, de valójában metódusok (getterek és setterek) képviselik őket. Ez lehetővé teszi a bemenetek validálását vagy az értékek generálását csak az olvasás pillanatában.

A property-k használatához:

  • Adjon hozzá egy @property <type> $xyz alakú annotációt az osztályhoz
  • Hozzon létre egy getXyz() vagy isXyz() nevű gettert, és egy setXyz() nevű settert
  • Biztosítsa, hogy a getter és a setter public vagy protected legyen. Opcionálisak – tehát létezhetnek read-only vagy write-only property-ként is

Nézzünk egy gyakorlati példát a Circle osztályra, ahol a property-ket arra használjuk, hogy biztosítsuk, hogy a sugár mindig nemnegatív szám legyen. Cseréljük le az eredeti public $radius-t property-re:

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

	private float $radius = 0.0; // není public!

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

	// setter pro property $radius
	protected function setRadius(float $radius): void
	{
		// hodnotu před uložením sanitizujeme
		$this->radius = max(0.0, $radius);
	}

	// getter pro property $visible
	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;  // a getRadius()-t hívja
echo $circle->visible; // az isVisible()-t hívja

A PHP 8.4 óta ugyanezt a funkcionalitást property hook-okkal lehet elérni, amelyek sokkal elegánsabb és tömörebb szintaxist kínálnak:

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

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

Extension methods

A Nette\Object egy másik érdekes koncepciót hozott a PHP-ba, amelyet a modern programozási nyelvek ihlettek – az extension metódusokat. Ez a funkció, amelyet a C#-ból vettek át, lehetővé tette a fejlesztők számára, hogy elegánsan bővítsék a meglévő osztályokat új metódusokkal anélkül, hogy módosítaniuk vagy örökölniük kellene tőlük. Például hozzáadhatott egy addDateTime() metódust az űrlaphoz, amely egy saját DateTimePickert 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 a nevüket nem ismerték fel az editorok, sőt, azt jelezték, hogy a metódus nem létezik. Ezért a támogatásuk megszűnt. Ma már gyakoribb a kompozíció vagy az öröklődés használata az osztályok funkcionalitásának bővítésére.

Osztálynév lekérdezése

Az osztálynév lekérdezéséhez a SmartObject egyszerű metódust kínált:

$class = $obj->getClass(); // Nette\Object segítségével
$class = $obj::class;      // PHP 8.0 óta

Hozzáférés a reflexióhoz és annotációkhoz

A Nette\Object hozzáférést kínált a reflexióhoz és annotációkhoz a getReflection() és getAnnotation() metódusok segítségével. Ez a megközelítés jelentősen leegyszerűsítette az osztályok metaadataival 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 metaadatokhoz attribútumok formájában hozzáférni, 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];

Metódus getterek

A Nette\Object elegáns módot kínált arra, hogy a metódusokat úgy adjuk át, 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 lehetséges az ún. first-class callable syntax használata, amely ezt a koncepciót még tovább viszi:

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

Események

A SmartObject egyszerűsített szintaxist kínál az eseményekkel való munkához. Az események lehetővé teszik az objektumok számára, hogy tájékoztassá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 a következő ciklussal ekvivalens:

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

Az érthetőség kedvéért javasoljuk a mágikus $this->onChange() metódus elkerülését. Praktikus helyettesítője például a Nette\Utils\Arrays::invoke függvény:

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