SmartObject
SmartObject je leta izboljševal obnašanje objektov v PHP-ju. Od različice PHP 8.4 so vse njegove funkcije del samega PHP-ja, s čimer je zaključil svoje zgodovinsko poslanstvo biti pionir modernega objektno usmerjenega pristopa v PHP-ju.
Namestitev:
composer require nette/utils
SmartObject je nastal leta 2007 kot revolucionarna rešitev pomanjkljivosti tedanjega objektnega modela PHP. V času, ko je PHP trpel zaradi številnih težav z objektno usmerjenim načrtovanjem, je prinesel pomembne izboljšave in poenostavil delo razvijalcem. Postal je legendarna komponenta ogrodja Nette. Ponujal je funkcionalnost, ki jo je PHP dobil šele mnogo let pozneje – od validacije dostopa do lastnosti objektov do naprednega ravnanja z napakami. S prihodom PHP 8.4 je zaključil svoje zgodovinsko poslanstvo, saj so vse njegove funkcije postale sestavni del jezika. Prehitel je razvoj PHP za izjemnih 17 let.
SmartObject je doživel zanimiv tehnični razvoj. Prvotno je bil implementiran kot razred Nette\Object
, od
katerega so drugi razredi podedovali potrebno funkcionalnost. Pomembna sprememba je prišla s PHP 5.4, ki je uvedel podporo za
trait-e. To je omogočilo preoblikovanje v trait Nette\SmartObject
, kar je prineslo večjo prilagodljivost –
razvijalci so lahko uporabljali funkcionalnost tudi v razredih, ki so že podedovali od drugega razreda. Medtem ko je prvotni
razred Nette\Object
prenehal obstajati s PHP 7.2 (ki je prepovedal poimenovanje razredov z besedo ‘Object’),
trait Nette\SmartObject
živi naprej.
Poglejmo si funkcije, 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-ju.
Konsistentna obravnava napak
Ena najbolj perečih težav zgodnjega PHP-ja je bilo nedosledno obnašanje pri delu z objekti. Nette\Object
je
v ta kaos prinesel red in predvidljivost. Poglejmo si, kako se je PHP prvotno obnašal:
echo $obj->undeclared; // E_NOTICE, kasneje E_WARNING
$obj->undeclared = 1; // se izvede tiho brez opozorila
$obj->unknownMethod(); // Fatal error (ni mogoče ujeti s try/catch)
Fatal error je prekinil aplikacijo brez možnosti odziva. Tiho pisanje 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 odzivanje na napake in njihovo reševanje:
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 fatal error-jev, od PHP 8.2 pa se dostop do nedeklariranih članov šteje za napako.
Pomoč “Did you mean?”
Nette\Object
je prišel z zelo priročno funkcijo: inteligentnimi predlogi pri napakah v črkovanju. Ko je
razvijalec naredil napako v imenu metode ali spremenljivke, ni le sporočil napake, ampak je tudi ponudil pomoč v obliki
predloga pravilnega imena. To ikonično sporočilo, znano kot “did you mean?”, je programerjem prihranilo ure iskanja napak
v črkovanju:
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 to funkcionalnost v sporočila o napakah dodaja Tracy. In celo samodejno popravlja take napake.
Properties s kontroliranim dostopom
Pomembna inovacija, ki jo je SmartObject prinesel v PHP, so bile properties s kontroliranim dostopom. Ta koncept, običajen v jezikih kot sta C# in Python, je razvijalcem omogočil eleganten nadzor nad dostopom do podatkov objekta in zagotavljanje njihove konsistentnosti. Properties so močno orodje objektno usmerjenega programiranja. Delujejo kot spremenljivke, vendar so v resnici predstavljene z metodami (getterji in setterji). To omogoča validacijo vhodnih podatkov ali generiranje vrednosti šele ob branju.
Za uporabo properties morate:
- Dodati razredu anotacijo v obliki
@property <type> $xyz
- Ustvariti getter z imenom
getXyz()
aliisXyz()
, setter z imenomsetXyz()
- Zagotoviti, da sta getter in setter public ali protected. Sta opcijska – lahko torej obstajata kot read-only ali write-only property
Poglejmo si praktičen primer na razredu Circle, kjer bomo properties uporabili za zagotavljanje, da je polmer vedno
nenegativno število. Nadomestili bomo prvotni public $radius
s property:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // ni public!
// getter za property $radius
protected function getRadius(): float
{
return $this->radius;
}
// setter za property $radius
protected function setRadius(float $radius): void
{
// vrednost pred shranjevanjem preverimo
$this->radius = max(0.0, $radius);
}
// getter za property $visible
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // v resnici kliče setRadius(10)
echo $circle->radius; // kliče getRadius()
echo $circle->visible; // kliče isVisible()
Od PHP 8.4 lahko isto funkcionalnost dosežemo s 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;
}
}
Extension methods
Nette\Object
je v PHP prinesel še en zanimiv koncept, navdihnjen s sodobnimi programskimi jeziki – extension
methods. Ta funkcionalnost, prevzeta iz C#, je razvijalcem omogočila elegantno razširjanje obstoječih razredov z novimi
metodami brez potrebe po spreminjanju ali dedovanju. Na primer, obrazcu ste lahko 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');
Extension metode so se izkazale za nepraktične, ker njihovih imen urejevalniki niso predlagali, nasprotno, javljali so, da metoda ne obstaja. Zato je bila njihova podpora ukinjena. Danes je običajneje uporabljati kompozicijo ali dedovanje za razširitev funkcionalnosti razredov.
Pridobivanje imena razreda
Za pridobivanje 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 preko metod getReflection()
in
getAnnotation()
. Ta pristop je pomembno poenostavil delo z metainformacijami 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 metainformacij v obliki atributov, ki ponujajo še več možnosti in boljšo tipsko preverjanje:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Method getterji
Nette\Object
je ponujal eleganten način za predajanje 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 obvestijo 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 enakovredna naslednji zanki:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Zaradi jasnosti priporočamo, da se izognete magični metodi $this->onChange()
. Praktična zamenjava je
funkcija Nette\Utils\Arrays::invoke:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);