SmartObject in StaticClass

SmartObject razredom PHP doda podporo za lastnosti. StaticClass se uporablja za označevanje statičnih razredov.

Namestitev:

composer require nette/utils

Lastnosti, nastavljalci in pridobitelji

V sodobnih objektno usmerjenih jezikih (npr. C#, Python, Ruby, JavaScript) se izraz lastnost nanaša na posebne člane razredov, ki so videti kot spremenljivke, v resnici pa jih predstavljajo metode. Ko se vrednost te “spremenljivke” dodeli ali prebere, se pokliče ustrezna metoda (imenovana getter ali setter). To je zelo priročno, saj nam omogoča popoln nadzor nad dostopom do spremenljivk. Potrdimo lahko vnos ali ustvarimo rezultate samo takrat, ko je lastnost prebrana.

Lastnosti PHP niso podprte, vendar jih lahko posnemamo z lastnostmi Nette\SmartObject. Kako jo uporabiti?

  • V razred dodajte opombo v obliki @property <type> $xyz
  • Ustvarite getter z imenom getXyz() ali isXyz(), setter z imenom setXyz()
  • Getter in setter morata biti javna ali zaščitena in sta neobvezna, zato je lahko lastnost samo za branje ali samo za pisanje

Lastnost za razred Circle bomo uporabili za zagotovitev, da se v spremenljivko $radius vnesejo samo nenegativna števila. Zamenjajte public $radius z lastnostjo:

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

	private float $radius = 0.0; // ni javno

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

	// setter za lastnost $radius
	protected function setRadius(float $radius): void
	{
		// sanitizacija vrednosti pred shranjevanjem
		$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 pokliče setRadius(10)
echo $circle->radius;  // pokliče getRadius()
echo $circle->visible; // pokliče isVisible()

Lastnosti so predvsem sintaktični sladkor, ki je namenjen temu, da programerju s poenostavitvijo kode osladi življenje. Če jih ne želite, vam jih ni treba uporabljati.

Statični razredi

Statične razrede, tj. razrede, ki niso namenjeni instanciranju, lahko označimo z lastnostjo Nette\StaticClass:

class Strings
{
	use Nette\StaticClass;
}

Pri poskusu ustvarjanja instance se vrže izjema Error, ki označuje, da je razred statičen.

Pogled v zgodovino

SmartObject je v preteklosti na različne načine izboljševal in popravljal obnašanje razredov, vendar je zaradi razvoja PHP večina prvotnih funkcij postala odveč. Zato je v nadaljevanju podan pogled v zgodovino, kako so se stvari razvijale.

Objektni model PHP je že od začetka trpel zaradi številnih resnih pomanjkljivosti in neučinkovitosti. Zato je bil leta 2007 oblikovan razred Nette\Object, ki jih je poskušal odpraviti in izboljšati izkušnjo uporabe PHP. Dovolj je bilo, da so ga podedovali tudi drugi razredi in pridobili prednosti, ki jih je prinesel. Ko je PHP 5.4 začel podpirati lastnosti, je razred Nette\Object nadomestil razred Nette\SmartObject. Tako ni bilo več treba dedovati po skupnem predniku. Poleg tega je bilo trait mogoče uporabiti v razredih, ki so že dedovali od drugega razreda. Dokončni konec razreda Nette\Object se je zgodil z izdajo PHP 7.2, ki je prepovedal, da bi se razredi imenovali Object.

Z nadaljnjim razvojem jezika PHP sta se izboljšala objektni model in zmožnosti jezika. Posamezne funkcije razreda SmartObject so postale nepotrebne. Od izdaje PHP 8.2 je ostala edina funkcija, ki še ni neposredno podprta v PHP, možnost uporabe tako imenovanih lastnosti.

Katere funkcije sta nekoč ponujala razreda Nette\Object in Nette\Object? Tukaj je pregled. (Primeri uporabljajo razred Nette\Object, vendar večina lastnosti velja tudi za lastnost Nette\SmartObject.)

Nedosledne napake

PHP se je nedosledno obnašal pri dostopu do nereklariranih članov. Stanje v času Nette\Object je bilo naslednje:

echo $obj->undeclared; // E_NOTICE, pozneje E_WARNING
$obj->undeclared = 1;  // poteka tiho, brez poročanja
$obj->unknownMethod(); // usodna napaka (ki je ni mogoče ujeti s funkcijo try/catch)

Usodna napaka je prekinila aplikacijo brez možnosti odziva. Tiho pisanje v neobstoječe člene brez opozorila bi lahko privedlo do resnih napak, ki bi jih bilo težko odkriti. Nette\Object Vsi ti primeri so bili ujeti in zavržena je bila izjema MemberAccessException.

echo $obj->undeclared;   // vrgel Izjemo Nette\MemberAccessException
$obj->undeclared = 1;    // throw Nette\MemberAccessException
$obj->unknownMethod();   // throw Nette\MemberAccessException

Od PHP 7.0 PHP ne povzroča več usodnih napak, ki jih ni mogoče ujeti, dostop do nedeklariranih članov pa je napaka od PHP 8.2.

Ali ste mislili?

Če se je vrgla napaka Nette\MemberAccessException, morda zaradi tiskarske napake pri dostopu do predmetne spremenljivke ali klicu metode, je Nette\Object v sporočilu o napaki poskušal podati namig, kako napako odpraviti, in sicer v obliki ikoničnega dodatka “Ali ste mislili?”.

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

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

Današnji PHP morda nima oblike “ali ste mislili?”, vendar Tracy napakam dodaja ta dodatek. In take napake lahko celo sam popravi.

Metode razširitve

Navdih za razširitvene metode iz C#. Omogočale so dodajanje novih metod obstoječim razredom. Na primer, obrazcu lahko dodate metodo addDateTime() in tako dodate svoj 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 urejevalniki njihovih imen niso samodejno dopolnili, temveč so sporočili, da metoda ne obstaja. Zato je bila njihova podpora ukinjena.

Pridobivanje imena razreda

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

Dostop do razmisleka in opomb

Nette\Object ponujen dostop do refleksije in anotacij z metodama getReflection() in getAnnotation():

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

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

Od različice PHP 8.0 je mogoče dostopati do metainformacij v obliki atributov:

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

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

Metode, ki pridobivajo

Nette\Object je ponujal eleganten način za ravnanje z metodami, kot da bi bile 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 različice PHP 8.1 lahko uporabljate tako imenovano sintakso “first-class callable syntax”::https://www.php.net/…lable_syntax

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

Dogodki

Nette\Object ponujen sintaktični sladkor za sprožitev dogodka:

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 enakovredna naslednjemu:

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

Zaradi jasnosti priporočamo, da se izognete čarobni metodi $this->onChange(). Praktično nadomestilo je funkcija Nette\Utils\Arrays::invoke:

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