SmartObject

SmartObject je leta izboljševal obnašanje objektov v PHP. Od različice PHP 8.4 so vse njegove funkcije že del samega PHP, s čimer je zaključil svojo zgodovinsko misijo biti pionir sodobnega objektnega pristopa v PHP.

Namestitev:

composer require nette/utils

SmartObject je nastal leta 2007 kot revolucionarna rešitev za pomanjkljivosti takratnega objektnega modela PHP. V času, ko je PHP trpel zaradi številnih težav z objektnim načrtovanjem, je prinesel znatno izboljšanje in poenostavitev dela za razvijalce. Postal je legendarni del ogrodja Nette. Ponujal je funkcionalnost, ki jo je PHP pridobil šele mnogo let kasneje – od nadzora dostopa do lastnosti objektov do sofisticiranih sintaktičnih sladkorčkov. S prihodom PHP 8.4 je zaključil svojo zgodovinsko misijo, saj so vse njegove funkcije postale naravni del jezika. Prehitel je razvoj PHP za izjemnih 17 let.

Tehnično je SmartObject doživel zanimiv razvoj. Prvotno je bil implementiran kot razred Nette\Object, od katerega so drugi razredi podedovali potrebno funkcionalnost. Ključna sprememba je prišla s PHP 5.4, ki je prinesel podporo za traite. To je omogočilo preoblikovanje v obliko traite Nette\SmartObject, kar je prineslo večjo prilagodljivost – razvijalci so lahko funkcionalnost uporabili tudi v razredih, ki so že dedovali od drugega razreda. Medtem ko je prvotni razred Nette\Object izginil s prihodom PHP 7.2 (ki je prepovedal poimenovanje razredov z besedo Object), traita Nette\SmartObject živi naprej.

Poglejmo si lastnosti, ki sta jih nekoč ponujala Nette\Object in kasneje Nette\SmartObject. Vsaka od teh funkcij je v svojem času predstavljala pomemben korak naprej na področju objektno usmerjenega programiranja v PHP.

Konsistentna stanja napak

Eden najbolj perečih problemov zgodnjega PHP je bilo nekonsistentno obnašanje pri delu z objekti. Nette\Object je v ta kaos vnesel red in predvidljivost. Poglejmo, kako je izgledalo prvotno obnašanje PHP:

echo $obj->undeclared;    // E_NOTICE, kasneje E_WARNING
$obj->undeclared = 1;     // gre tiho skozi brez poročanja
$obj->unknownMethod();    // Fatal error (neulovljiv s try/catch)

Fatal error je končal aplikacijo brez možnosti kakršnega koli odziva. Tiho zapisovanje v neobstoječe člane brez opozorila je lahko vodilo do resnih napak, ki jih je bilo težko odkriti. Nette\Object je vse te primere prestregel in vrgel izjemo MemberAccessException, kar je programerjem omogočilo, da se na napake odzovejo in jih rešijo.

echo $obj->undeclared;   // vrže Nette\MemberAccessException
$obj->undeclared = 1;    // vrže Nette\MemberAccessException
$obj->unknownMethod();   // vrže Nette\MemberAccessException

Od PHP 7.0 jezik ne povzroča več neulovljivih fatalnih napak in od PHP 8.2 se dostop do nedeklariranih članov šteje za napako.

Pomoč “Did you mean?”

Nette\Object je prišel z zelo prijetno funkcijo: inteligentno pomočjo pri tipkarskih napakah. Ko je razvijalec naredil napako v imenu metode ali spremenljivke, ni samo sporočil napake, ampak je ponudil tudi pomoč v obliki predloga pravilnega imena. To ikonično sporočilo, znano kot “did you mean?”, je programerjem prihranilo ure iskanja tipkarskih napak:

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

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

Današnji PHP sicer nima nobene oblike „did you mean?“, vendar ta dodatek zna v napake dodajati Tracy. In celo takšne napake samodejno popravljati.

Lastnosti z nadzorovanim dostopom

Pomembna inovacija, ki jo je SmartObject prinesel v PHP, so bile lastnosti z nadzorovanim dostopom. Ta koncept, običajen v jezikih, kot sta C# ali Python, je razvijalcem omogočil elegantno nadzorovanje dostopa do podatkov objekta in zagotavljanje njihove konsistentnosti. Lastnosti so močno orodje objektno usmerjenega programiranja. Delujejo kot spremenljivke, vendar so dejansko predstavljene z metodami (getterji in setterji). To omogoča validacijo vnosov ali generiranje vrednosti šele v trenutku branja.

Za uporabo lastnosti morate:

  • Dodati razredu anotacijo v obliki @property <type> $xyz
  • Ustvariti getter z imenom getXyz() ali isXyz(), setter z imenom setXyz()
  • Zagotoviti, da sta getter in setter public ali protected. Sta izbirna – lahko torej obstajata kot read-only ali write-only lastnost

Poglejmo si praktičen primer na razredu Circle, kjer lastnosti uporabimo za zagotovitev, da bo polmer vedno nenegativno število. Nadomestimo prvotno public $radius z lastnostjo:

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

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

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

	// setter za lastnost $radius
	protected function setRadius(float $radius): void
	{
		// vrednost pred shranjevanjem saniramo
		$this->radius = max(0.0, $radius);
	}

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

$circle = new Circle;
$circle->radius = 10;  // dejansko kliče setRadius(10)
echo $circle->radius;  // kliče getRadius()
echo $circle->visible; // kliče isVisible()

Od PHP 8.4 je mogoče doseči enako funkcionalnost z uporabo property hooks, ki ponujajo veliko bolj elegantno in jedrnato sintakso:

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

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

Razširitvene metode

Nette\Object je v PHP prinesel še en zanimiv koncept, navdihnjen s sodobnimi programskimi jeziki – razširitvene metode. Ta funkcija, prevzeta iz C#, je razvijalcem omogočila elegantno razširjanje obstoječih razredov z novimi metodami brez potrebe po njihovem urejanju ali dedovanju od njih. Na primer, lahko ste v obrazec dodali metodo addDateTime(), ki doda lasten DateTimePicker:

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

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

Razširitvene metode so se izkazale za nepraktične, saj njihova imena niso bila predlagana s strani urejevalnikov, nasprotno, poročali so, da metoda ne obstaja. Zato je bila njihova podpora ukinjena. Danes je bolj običajno uporabljati kompozicijo ali dedovanje za razširitev funkcionalnosti razredov.

Ugotavljanje imena razreda

Za ugotavljanje imena razreda je SmartObject ponujal preprosto metodo:

$class = $obj->getClass(); // z uporabo Nette\Object
$class = $obj::class;      // od PHP 8.0

Dostop do refleksije in anotacij

Nette\Object je ponujal dostop do refleksije in anotacij z metodama getReflection() in getAnnotation(). Ta pristop je znatno poenostavil delo z meta-informacijami razredov:

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

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

Od PHP 8.0 je mogoče dostopati do meta-informacij v obliki atributov, ki ponujajo še več možnosti in boljši tipski nadzor:

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

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

Metodni getterji

Nette\Object je ponujal eleganten način za posredovanje metod, kot da bi šlo za spremenljivke:

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

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

Od PHP 8.1 je mogoče uporabiti t.i. first-class callable syntax, ki ta koncept pelje še dlje:

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

Dogodki

SmartObject ponuja poenostavljeno sintakso za delo z dogodki. Dogodki omogočajo objektom, da obveščajo druge dele aplikacije o spremembah svojega stanja:

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

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

Koda $this->onChange($this, $radius) je ekvivalentna naslednji zanki:

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

Zaradi razumljivosti priporočamo, da se izogibate magični metodi $this->onChange(). Praktična zamenjava je na primer funkcija Nette\Utils\Arrays::invoke:

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