[Go to site: main page, start]

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> $xyz açıklamasını eklemek
  • getXyz() ya da isXyz() 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);
versiyon: 4.x