SmartObject
SmartObject, uzun yıllar boyunca PHP'nin nesne davranışını iyileştirdi. PHP 8.4'ten itibaren tüm özellikleri PHP'nin kendisinin doğal bir parçası hâline geldi; böylece PHP'de modern nesne yönelimli yaklaşımın öncüsü olarak tarihsel görevini tamamladı.
Kurulum:
composer require nette/utils
SmartObject, 2007'de PHP'nin o zamanki nesne modelinin eksiklerine devrim niteliğinde bir çözüm olarak ortaya çıktı. PHP'nin nesne yönelimli tasarımda pek çok sorunla boğuştuğu bir dönemde önemli iyileştirmeler getirdi ve geliştiricilerin işini kolaylaştırdı. Nette Framework'ün efsanevi bir bileşeni oldu. SmartObject, PHP'nin ancak yıllar sonra kavuştuğu işlevleri sunuyordu: nesne özelliklerine erişim denetiminden gelişmiş söz dizimi kolaylıklarına kadar. PHP 8.4'ün çıkışıyla birlikte, özelliklerinin çoğu dilin doğal parçası olduğundan tarihsel görevini yerine getirmiş oldu. PHP'nin gelişiminin tam 17 yıl önündeydi.
Teknik olarak SmartObject ilginç bir evrim geçirdi. Başlangıçta, diğer sınıfların gereken işlevleri miras aldığı
Nette\Object sınıfı olarak uygulanmıştı. Önemli bir değişiklik, trait desteğini getiren PHP 5.4 ile geldi.
Bu, Nette\SmartObject trait'ine dönüşümü olanaklı kıldı ve daha fazla esneklik sağladı: geliştiriciler
artık zaten başka bir sınıftan türeyen sınıflarda da bu işlevleri kullanabiliyordu. Özgün Nette\Object
sınıfı PHP 7.2 ile birlikte ortadan kalksa da (bu sürüm sınıfları Object sözcüğüyle adlandırmayı
yasakladı), Nette\SmartObject trait'i yaşamaya devam ediyor.
Nette\Object'in ve sonrasında Nette\SmartObject'in sunduğu özelliklere bakalım. Bunların her
biri, zamanında PHP'de nesne yönelimli programlama açısından önemli birer adımdı.
Tutarlı Hata Durumları
PHP'nin ilk dönemlerinin en can sıkıcı sorunlarından biri, nesnelerle çalışırken görülen tutarsız davranıştı.
Nette\Object bu kargaşaya düzen ve öngörülebilirlik getirdi. PHP'nin başlangıçta nasıl davrandığına
bakalım:
echo $obj->undeclared; // E_NOTICE, sonraları E_WARNING
$obj->undeclared = 1; // sessizce geçer, uyarı yok
$obj->unknownMethod(); // Fatal error (try/catch ile yakalanamaz)
Fatal error, uygulamayı tepki verme olanağı bırakmadan sonlandırırdı. Var olmayan üyelere uyarısız, sessizce yazmak
ise saptanması zor ciddi hatalara yol açabiliyordu. Nette\Object tüm bu durumları yakalayıp
MemberAccessException fırlatıyor, böylece programcıların bu hatalara tepki verip onları ele almasına olanak
tanıyordu:
echo $obj->undeclared; // Nette\MemberAccessException fırlatır
$obj->undeclared = 1; // Nette\MemberAccessException fırlatır
$obj->unknownMethod(); // Nette\MemberAccessException fırlatır
PHP 7.0'dan beri dil artık yakalanamayan fatal error'lara yol açmıyor; PHP 8.2'den beri de bildirilmemiş üyelere erişim hata sayılıyor.
“Bunu mu demek istediniz?” Yardımcısı
Nette\Object çok kullanışlı bir özellikle geliyordu: yazım hataları için akıllı öneriler. Geliştirici
bir metot ya da değişken adında hata yaptığında, yalnızca hatayı bildirmekle kalmayıp doğru adı önererek yardım
ediyordu. “did you mean?” adıyla bilinen bu ikonik mesaj, programcılara yazım hatası avında saatler kazandırdı:
class Foo extends Nette\Object
{
public static function from($var)
{
}
}
$foo = Foo::form($var);
// Nette\MemberAccessException fırlatır
// "Call to undefined static method Foo::form(), did you mean from()?"
Bugünkü PHP'de “bunu mu demek istediniz?” diye bir şey olmasa da, bu eki hatalara Tracy ekleyebilir. Hatta bu tür hataları otomatik düzeltebilir.
Erişimi Denetlenen Özellikler
SmartObject'in PHP'ye getirdiği önemli bir yenilik, erişimi denetlenen özelliklerdi. C# ya da Python gibi dillerde yaygın olan bu kavram, geliştiricilerin nesne verilerine erişimi şık bir şekilde denetlemesine ve tutarlılığını güvence altına almasına olanak tanıyordu. Özellikler, nesne yönelimli programlamanın güçlü bir aracıdır. Değişken gibi davranırlar, ama aslında metotlarla (getter ve setter'larla) temsil edilirler. Bu da okuma anında girdi doğrulaması ya da değer üretimi yapmayı sağlar.
Özellikleri kullanmak için şunlar gerekiyordu:
- Sınıfa
@property <tür> $xyzaçıklamasını eklemek getXyz()ya daisXyz()adında bir getter,setXyz()adında bir setter oluşturmak- Getter ve setter'ın public ya da protected olmasını sağlamak. İkisi de isteğe bağlıydı; yani yalnızca okunur ya da yalnızca yazılır özellikler tanımlanabiliyordu
Yarıçapın her zaman negatif olmamasını sağlamak için özellikleri kullanacağımız Circle sınıfıyla
pratik bir örneğe bakalım. public $radius yerine bir özellik koyuyoruz:
/**
* @property float $radius
* @property-read bool $visible
*/
class Circle
{
use Nette\SmartObject;
private float $radius = 0.0; // public değil!
// $radius özelliğinin getter'ı
protected function getRadius(): float
{
return $this->radius;
}
// $radius özelliğinin setter'ı
protected function setRadius(float $radius): void
{
// değeri saklamadan önce düzelt
$this->radius = max(0.0, $radius);
}
// $visible özelliğinin getter'ı
protected function isVisible(): bool
{
return $this->radius > 0;
}
}
$circle = new Circle;
$circle->radius = 10; // aslında setRadius(10) çağrılır
echo $circle->radius; // getRadius() çağrılır
echo $circle->visible; // isVisible() çağrılır
PHP 8.4'ten beri aynı işlev, çok daha şık ve derli toplu bir söz dizimi sunan property hook'larıyla elde edilebilir:
class Circle
{
public float $radius = 0.0 {
set => max(0.0, $value);
}
public bool $visible {
get => $this->radius > 0;
}
}
Genişletme Metotları
Nette\Object, modern programlama dillerinden esinlenen bir başka ilginç kavramı da PHP'ye getirdi: genişletme
metotları. C#'tan ödünç alınan bu özellik, geliştiricilerin var olan sınıfları değiştirmeden ya da onlardan
türetmeden yeni metotlarla şık biçimde genişletmesine olanak tanıyordu. Örneğin bir forma, özel bir DateTimePicker
ekleyen addDateTime() metodunu ekleyebilirdiniz:
Form::extensionMethod(
'addDateTime',
fn(Form $form, string $name) => $form[$name] = new DateTimePicker,
);
$form = new Form;
$form->addDateTime('date');
Genişletme metotları pratikte işe yaramadı; çünkü adlarını kod düzenleyicileri önermiyor, bunun yerine metodun var olmadığını bildiriyordu. Bu yüzden destekleri sona erdi. Bugün sınıf işlevselliğini genişletmek için kompozisyon ya da kalıtım kullanmak daha yaygındır.
Sınıf Adını Alma
SmartObject, sınıf adını almak için basit bir metot sunuyordu:
$class = $obj->getClass(); // Nette\Object ile
$class = $obj::class; // PHP 8.0'dan beri
Reflection ve Açıklamalara Erişim
Nette\Object, getReflection() ve getAnnotation() metotlarıyla reflection'a ve
açıklamalara erişim sağlıyordu. Bu yaklaşım, sınıfın meta bilgileriyle çalışmayı belirgin biçimde
kolaylaştırıyordu:
/**
* @author John Doe
*/
class Foo extends Nette\Object
{
}
$obj = new Foo;
$reflection = $obj->getReflection();
$reflection->getAnnotation('author'); // 'John Doe' döndürür
PHP 8.0'dan beri meta bilgilere, daha fazla olanak ve daha iyi tür denetimi sunan attribute'lar üzerinden erişilebiliyor:
#[Author('John Doe')]
class Foo
{
}
$obj = new Foo;
$reflection = new ReflectionObject($obj);
$reflection->getAttributes(Author::class)[0];
Metot Getter'ları
Nette\Object, metotları değişkenmiş gibi aktarmak için şık bir yol sunuyordu:
class Foo extends Nette\Object
{
public function adder($a, $b)
{
return $a + $b;
}
}
$obj = new Foo;
$method = $obj->adder;
echo $method(2, 3); // 5
PHP 8.1'den beri, bu kavramı daha da ileri götüren first-class callable syntax kullanabilirsiniz:
$obj = new Foo;
$method = $obj->adder(...);
echo $method(2, 3); // 5
Olaylar
SmartObject, olaylarla çalışmak için basitleştirilmiş bir söz dizimi sunar. Olaylar, nesnelerin durumlarındaki değişiklikleri uygulamanın diğer parçalarına bildirmesini sağlar:
class Circle
{
use Nette\SmartObject;
public array $
private float $radius = 0.0;
public function setRadius(float $radius): void
{
$this->onChange($this, $radius);
$this->radius = $radius;
}
}
$this->onChange($this, $radius) kodu, aşağıdaki döngüyle eşdeğerdir:
foreach ($this->onChange as $callback) {
$callback($this, $radius);
}
Anlaşılırlık açısından, sihirli $this->onChange() metodundan kaçınmanızı öneririz. Pratik bir
alternatif Nette\Utils\Arrays::invoke fonksiyonudur:
Nette\Utils\Arrays::invoke($this->onChange, $this, $radius);