Nesne Yönelimli Programlamaya Giriş

“OOP” terimi, kodu düzenlemenin ve yapılandırmanın bir yolu olan Nesne Yönelimli Programlama'yı (Object-Oriented Programming) anlatır. OOP, bir programı bir komut ve fonksiyon dizisi yerine, birbiriyle iletişen nesnelerden oluşan bir topluluk olarak görmemizi sağlar.

OOP'de “nesne”, veriyi ve o veri üzerinde çalışan fonksiyonları içeren bir birimdir. Nesneler, nesneler için taslak ya da şablon olarak anlaşılabilecek “sınıflara” göre oluşturulur. Bir sınıfımız olduğunda, onun “örneğini” (instance) yani o sınıftan yapılmış somut bir nesneyi oluşturabiliriz.

PHP'de basit bir sınıfı nasıl oluşturabileceğimize bakalım. Bir sınıf tanımlarken “class” anahtar sözcüğünü, ardından sınıf adını, sonra da sınıfın fonksiyonlarını (“metot” denir) ve sınıf değişkenlerini (“özellik” ya da “nitelik” denir) çevreleyen süslü parantezleri kullanırız:

class Car
{
	function honk()
	{
		echo 'Bip bip!';
	}
}

Bu örnekte, honk adında tek bir fonksiyonu (ya da “metodu”) olan Car adlı bir sınıf oluşturduk.

Her sınıf yalnızca tek bir ana işi çözmelidir. Bir sınıf çok fazla şey yapıyorsa, onu daha küçük, uzmanlaşmış sınıflara bölmek uygun olabilir.

Sınıflar, kodu düzenli ve gezinmesi kolay tutmak için genellikle ayrı dosyalarda saklanır. Dosya adı sınıf adıyla eşleşmelidir; yani Car sınıfı için dosya adı Car.php olur.

Sınıfları adlandırırken “PascalCase” kuralını izlemek iyidir; yani addaki her sözcük büyük harfle başlar ve alt çizgi ya da başka bir ayırıcı bulunmaz. Metotlar ve özellikler “camelCase” kuralını izler; yani küçük harfle başlarlar.

PHP'deki bazı metotların özel rolleri vardır ve __ (iki alt çizgi) ile başlarlar. En önemli özel metotlardan biri, __construct olarak adlandırılan “yapıcıdır” (constructor). Yapıcı, bir sınıfın yeni bir örneği oluşturulurken otomatik çağrılan bir metottur.

Yapıcıyı sıklıkla bir nesnenin başlangıç durumunu ayarlamak için kullanırız. Örneğin bir kişiyi temsil eden bir nesne oluştururken, yapıcıyı onun yaşını, adını ya da başka niteliklerini ayarlamak için kullanabilirsiniz.

PHP'de bir yapıcının nasıl kullanılacağına bakalım:

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person(25);
echo $person->howOldAreYou(); // Çıktı: 25

Bu örnekte Person sınıfının bir $age özelliği (değişkeni) ve bu özelliği ayarlayan bir yapıcısı var. howOldAreYou() metodu ise kişinin yaşına erişim sağlar.

$this sözde değişkeni, sınıfın içinde nesnenin özelliklerine ve metotlarına erişmek için kullanılır.

new anahtar sözcüğü bir sınıfın yeni bir örneğini oluşturmak için kullanılır. Yukarıdaki örnekte 25 yaşında yeni bir kişi oluşturduk.

Bir nesne oluşturulurken belirtilmezlerse kullanılacak, yapıcı parametreleri için varsayılan değerler de ayarlayabilirsiniz. Örneğin:

class Person
{
	private $age;

	function __construct($age = 20)
	{
		$this->age = $age;
	}

	function howOldAreYou()
	{
		return $this->age;
	}
}

$person = new Person;  // hiçbir argüman aktarılmıyorsa parantezler atlanabilir
echo $person->howOldAreYou(); // Çıktı: 20

Bu örnekte, bir Person nesnesi oluştururken yaş belirtmezseniz varsayılan 20 değeri kullanılacak.

İşin güzel yanı, özellik tanımının yapıcı aracılığıyla başlatılmasıyla birlikte şöyle kısaltılıp basitleştirilebilmesidir:

class Person
{
	function __construct(
		private $age = 20,
	) {
	}
}

Tamamlamak gerekirse, yapıcıların yanında nesnelerin, nesne bellekten serbest bırakılmadan önce çağrılan yıkıcıları (__destruct metodu) da olabilir.

Ad Alanları

Ad alanları (namespace), ilgili sınıfları, fonksiyonları ve sabitleri düzenleyip gruplamamızı, bu arada ad çakışmalarından kaçınmamızı sağlar. Onları bilgisayardaki klasörler gibi düşünebilirsiniz; her klasör belirli bir projeye ya da konuya ilişkin dosyaları içerir.

Ad alanları özellikle büyük projelerde ya da sınıf adı çakışmalarının ortaya çıkabileceği üçüncü taraf kütüphaneleri kullanırken yararlıdır.

Projenizde Car adında bir sınıfınız olduğunu ve onu Transport adlı bir ad alanına koymak istediğinizi düşünün. Bunu şöyle yaparsınız:

namespace Transport;

class Car
{
	function honk()
	{
		echo 'Bip bip!';
	}
}

Car sınıfını başka bir dosyada kullanmak isterseniz, sınıfın hangi ad alanından geldiğini belirtmeniz gerekir:

$car = new Transport\Car;

Basitleştirmek için, dosyanın başında belirli bir ad alanındaki hangi sınıfı kullanmak istediğinizi belirtebilirsiniz; böylece tam yolu anmadan örnek oluşturabilirsiniz:

use Transport\Car;

$car = new Car;

Kalıtım

Kalıtım, var olan sınıflara dayanarak yeni sınıflar oluşturmayı, onların özelliklerini ve metotlarını devralmayı, gerektiğinde genişletmeyi ya da yeniden tanımlamayı sağlayan bir nesne yönelimli programlama aracıdır. Kalıtım, kodun yeniden kullanılabilirliğini ve sınıf hiyerarşisini güvence altına alır.

Basitçe söylemek gerekirse, bir sınıfımız varsa ve ondan türeyen ama bazı değişiklikler taşıyan başka bir sınıf oluşturmak istiyorsak, yeni sınıfı özgün sınıftan “kalıtabiliriz”.

PHP'de kalıtım extends anahtar sözcüğüyle gerçekleştirilir.

Person sınıfımız yaş bilgisini saklıyor. Person sınıfını genişleten ve öğrenim alanı bilgisini ekleyen başka bir Student sınıfımız olabilir.

Bir örneğe bakalım:

class Person
{
	private $age;

	function __construct($age)
	{
		$this->age = $age;
	}

	function printInformation()
	{
		echo "Yaş: {$this->age} yıl\n";
	}
}

class Student extends Person
{
	private $fieldOfStudy;

	function __construct($age, $fieldOfStudy)
	{
		parent::__construct($age);
		$this->fieldOfStudy = $fieldOfStudy;
	}

	function printInformation()
	{
		parent::printInformation();
		echo "Öğrenim alanı: {$this->fieldOfStudy} \n";
	}
}

$student = new Student(20, 'Bilgisayar Bilimleri');
$student->printInformation();

Bu kod nasıl çalışıyor?

  • Person sınıfını genişletmek için extends anahtar sözcüğünü kullandık; bu, Student sınıfının Person sınıfındaki tüm metotları ve özellikleri devraldığı anlamına gelir.
  • parent:: anahtar sözcüğü, ana sınıftaki metotları çağırmamızı sağlar. Bu durumda, Student sınıfına kendi işlevselliğimizi eklemeden önce Person sınıfının yapıcısını çağırdık. Benzer biçimde, öğrenci bilgilerini listelemeden önce de ata printInformation() metodunu çağırdık.

Kalıtım, sınıflar arasında “bir … dır” ilişkisi bulunan durumlar içindir. Örneğin bir Student bir Person'dır. Bir kedi bir hayvandır. Kodda bir nesne (örneğin “Person”) beklediğimiz yerlerde, onun yerine türetilmiş bir nesne (örneğin “Student”) kullanmamızı sağlar.

Kalıtımın birincil amacının kod yinelenmesini önlemek olmadığını kavramak önemlidir. Tam tersine, kalıtımın kötüye kullanılması karmaşık ve bakımı zor koda yol açabilir. Sınıflar arasında “bir … dır” ilişkisi yoksa, kalıtım yerine kompozisyonu düşünmeliyiz.

Person ve Student sınıflarındaki printInformation() metotlarının biraz farklı bilgi çıktıladığına dikkat edin. Bu metodun başka gerçekleştirimlerini sunacak başka sınıflar (örneğin Employee) da ekleyebiliriz. Farklı sınıflardan nesnelerin aynı metoda farklı biçimlerde yanıt verebilme yeteneğine çok biçimlilik (polymorphism) denir:

$people = [
	new Person(30),
	new Student(20, 'Bilgisayar Bilimleri'),
	new Employee(45, 'Müdür'),
];

foreach ($people as $person) {
	$person->printInformation();
}

Kompozisyon

Kompozisyon, başka bir sınıftan özellikleri ve metotları kalıtmak yerine, onun örneğini kendi sınıfımızda kullandığımız bir tekniktir. Bu, karmaşık kalıtım yapıları oluşturmadan birden çok sınıfın işlevselliklerini ve özelliklerini birleştirmemizi sağlar.

Örneğin bir Engine sınıfımız ve bir Car sınıfımız var. “Bir araba bir motordur” demek yerine “Bir arabanın bir motoru vardır” deriz; bu tipik bir kompozisyon ilişkisidir.

class Engine
{
	function start()
	{
		echo 'Motor çalışıyor.';
	}
}

class Car
{
	private $engine;

	function __construct()
	{
		$this->engine = new Engine;
	}

	function start()
	{
		$this->engine->start();
		echo 'Araba sürüşe hazır!';
	}
}

$car = new Car;
$car->start();

Burada Car, Engine sınıfının tüm özelliklerine ve metotlarına sahip değildir, ama $engine özelliği aracılığıyla ona erişebilir.

Kompozisyonun avantajı, tasarımda daha fazla esneklik ve gelecekteki değişikliklere daha iyi uyum sağlamasıdır.

Görünürlük

PHP'de sınıf özellikleri, metotları ve sabitleri için “görünürlük” tanımlayabilirsiniz. Görünürlük, bu elemanlara nereden erişebileceğinizi belirler.

  1. Public: Bir eleman public olarak işaretliyse, ona her yerden, hatta sınıfın dışından bile erişebilirsiniz.
  2. Protected: protected olarak işaretli bir elemana yalnızca sınıfın içinden ve tüm alt sınıflarından (ondan kalıtan sınıflardan) erişilebilir.
  3. Private: Bir eleman private ise, ona yalnızca tanımlandığı sınıfın içinden erişebilirsiniz.

Görünürlüğü belirtmezseniz, PHP onu otomatik olarak public yapar.

Örnek bir koda bakalım:

class VisibilityExample
{
	public $publicProperty = 'Public';
	protected $protectedProperty = 'Protected';
	private $privateProperty = 'Private';

	public function printProperties()
	{
		echo $this->publicProperty;     // Çalışır
		echo $this->protectedProperty;  // Çalışır
		echo $this->privateProperty;    // Çalışır
	}
}

$object = new VisibilityExample;
$object->printProperties();
echo $object->publicProperty;        // Çalışır
// echo $object->protectedProperty;   // Hata fırlatır
// echo $object->privateProperty;     // Hata fırlatır

Sınıf kalıtımıyla devam edelim:

class ChildClass extends VisibilityExample
{
	public function printProperties()
	{
		echo $this->publicProperty;     // Çalışır
		echo $this->protectedProperty;  // Çalışır
		// echo $this->privateProperty;   // Hata fırlatır
	}
}

Bu durumda ChildClass içindeki printProperties() metodu public ve protected özelliklere erişebilir, ama ana sınıfın private özelliklerine erişemez.

Veri ve metotlar olabildiğince gizli olmalı ve yalnızca tanımlı bir arayüz üzerinden erişilebilmelidir. Bu, kodun geri kalanını etkilemeden sınıfın iç gerçekleştirimini değiştirmenizi sağlar.

Final Anahtar Sözcüğü

PHP'de bir sınıfın, metodun ya da sabitin kalıtılmasını veya geçersiz kılınmasını engellemek istersek final anahtar sözcüğünü kullanabiliriz. Bir sınıf final olarak işaretlendiğinde genişletilemez. Bir metot final olarak işaretlendiğinde alt sınıfta geçersiz kılınamaz.

Belirli bir sınıfın ya da metodun artık değiştirilmeyeceğini bilmek, olası çakışmalardan endişelenmeden değişiklikleri daha kolay yapmamızı sağlar. Örneğin, bir alt sınıfta aynı adı taşıyan bir metodun zaten bulunup çakışmaya yol açacağı korkusu olmadan yeni bir metot ekleyebiliriz. Ya da yine bir alt sınıftaki geçersiz kılınmış metotla tutarsızlık yaratma riski olmadan bir metodun parametrelerini değiştirebiliriz.

final class FinalClass
{
}

// Aşağıdaki kod hata fırlatacak, çünkü final bir sınıftan kalıtamayız.
class ChildOfFinalClass extends FinalClass
{
}

Bu örnekte, final FinalClass sınıfından kalıtma girişimi bir hatayla sonuçlanacak.

Statik Özellikler ve Metotlar

PHP'de bir sınıfın “statik” elemanlarından söz ettiğimizde, sınıfın belirli bir örneğine değil, sınıfın kendisine ait olan metotları ve özellikleri kastederiz. Bu, onlara erişmek için sınıfın bir örneğini oluşturmanıza gerek olmadığı anlamına gelir. Bunun yerine, onları doğrudan sınıf adı üzerinden çağırır ya da onlara erişirsiniz.

Statik elemanlar sınıfa ait olduğundan, örneklerine değil, statik metotların içinde $this sözde değişkenini kullanamayacağınızı unutmayın.

Statik özellikleri kullanmak tuzaklarla dolu, anlaşılmaz bir koda yol açar, dolayısıyla onları asla kullanmamalısınız ve burada bir örnek göstermeyeceğiz. Öte yandan statik metotlar yararlıdır. İşte bir örnek:

class Calculator
{
	public static function add($a, $b)
	{
		return $a + $b;
	}

	public static function subtract($a, $b)
	{
		return $a - $b;
	}
}

// Sınıfın bir örneğini oluşturmadan statik metodun kullanımı
echo Calculator::add(5, 3); // Çıktı: 8
echo Calculator::subtract(5, 3); // Çıktı: 2

Bu örnekte iki statik metodu olan bir Calculator sınıfı oluşturduk. Bu metotları, :: operatörünü kullanarak sınıfın bir örneğini oluşturmadan doğrudan çağırabiliriz. Statik metotlar özellikle, belirli bir sınıf örneğinin durumuna bağlı olmayan işlemler için yararlıdır.

Sınıf Sabitleri

Sınıfların içinde sabit tanımlama olanağımız var. Sabitler, programın çalışması boyunca hiç değişmeyen değerlerdir. Değişkenlerin tersine, bir sabitin değeri hep aynı kalır.

class Car
{
	public const NumberOfWheels = 4;

	public function displayNumberOfWheels(): void
	{
		echo self::NumberOfWheels;
	}
}

echo Car::NumberOfWheels;  // Çıktı: 4

Bu örnekte NumberOfWheels sabitine sahip bir Car sınıfımız var. Sabite sınıfın içinden erişirken, sınıf adı yerine self anahtar sözcüğünü kullanabiliriz.

Nesne Arayüzleri

Nesne arayüzleri, sınıflar için “sözleşme” görevi görür. Bir sınıf bir nesne arayüzünü gerçekleştirecekse, arayüzün tanımladığı tüm metotları içermelidir. Belirli sınıfların aynı “sözleşmeye” ya da yapıya uymasını güvence altına almanın harika bir yoludur.

PHP'de arayüzler interface anahtar sözcüğüyle tanımlanır. Bir arayüzde tanımlı tüm metotlar geneldir (public). Bir sınıf bir arayüzü gerçekleştirdiğinde implements anahtar sözcüğünü kullanır.

interface Animal
{
	function makeSound();
}

class Cat implements Animal
{
	public function makeSound()
	{
		echo 'Miyav';
	}
}

$cat = new Cat;
$cat->makeSound();

Bir sınıf bir arayüzü gerçekleştiriyor ama beklenen metotların tümü tanımlı değilse, PHP bir hata fırlatır.

Bir sınıf aynı anda birden çok arayüzü gerçekleştirebilir; bu, bir sınıfın yalnızca tek bir sınıftan kalıtabildiği kalıtımdan farklıdır:

interface Guardian
{
	function guardHouse();
}

class Dog implements Animal, Guardian
{
	public function makeSound()
	{
		echo 'Hav';
	}

	public function guardHouse()
	{
		echo 'Köpek evi özenle koruyor';
	}
}

Soyut Sınıflar

Soyut sınıflar, başka sınıflar için temel şablon görevi görür, ama onların örneklerini doğrudan oluşturamazsınız. Tam metotlarla, tanımlı içeriği olmayan soyut metotların bir karışımını içerirler. Soyut sınıflardan kalıtan sınıflar, atadaki tüm soyut metotlar için tanım sağlamalıdır.

Soyut bir sınıfı tanımlamak için abstract anahtar sözcüğünü kullanırız.

abstract class AbstractClass
{
	public function regularMethod()
	{
		echo 'Bu olağan bir metottur';
	}

	abstract public function abstractMethod();
}

class Child extends AbstractClass
{
	public function abstractMethod()
	{
		echo 'Bu, soyut metodun gerçekleştirimidir';
	}
}

$instance = new Child;
$instance->regularMethod();
$instance->abstractMethod();

Bu örnekte bir olağan ve bir soyut metodu olan soyut bir sınıfımız var. Sonra AbstractClass sınıfından kalıtan ve soyut metot için bir gerçekleştirim sağlayan bir Child sınıfımız var.

Arayüzler ile soyut sınıflar arasındaki fark nedir? Soyut sınıflar hem soyut hem somut metotlar içerebilirken, arayüzler yalnızca sınıfın hangi metotları gerçekleştirmesi gerektiğini tanımlar, ama hiçbir gerçekleştirim sunmaz. Bir sınıf yalnızca tek bir soyut sınıftan kalıtabilir, ama istediği kadar arayüzü gerçekleştirebilir.

Tip Denetimi

Programlamada, çalıştığımız verinin doğru tipte olmasını güvence altına almak çok önemlidir. PHP'de bu güvenceyi sağlayan araçlarımız var. Verinin doğru tipte olduğunun doğrulanmasına “tip denetimi” denir.

PHP'de karşılaşabileceğimiz tipler:

  1. Temel tipler: Bunlar int (tam sayılar), float (kayan noktalı sayılar), bool (mantıksal değerler), string (dizeler), array (diziler) ve null tiplerini içerir.
  2. Sınıflar: Bir değerin belirli bir sınıfın örneği olmasını istediğimizde.
  3. Arayüzler: Bir sınıfın gerçekleştirmesi gereken metot kümesini tanımlar. Bir arayüzü karşılayan değerin bu metotlara sahip olması gerekir.
  4. Karma tipler: Bir değişkenin birden çok izin verilen tipe sahip olabileceğini belirtebiliriz.
  5. Void: Bu özel tip, bir fonksiyonun ya da metodun hiçbir değer döndürmediğini gösterir.

Kodu tipleri içerecek şekilde nasıl değiştireceğimize bakalım:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}

	public function printAge(): void
	{
		echo "Bu kişi {$this->age} yaşında.";
	}
}

/**
 * Bir Person nesnesi kabul eden ve kişinin yaşını yazdıran fonksiyon.
 */
function printPersonAge(Person $person): void
{
	$person->printAge();
}

Böylece kodumuzun doğru tipte veri beklemesini ve onunla çalışmasını güvence altına alırız; bu da olası hataları önlememize yardım eder.

Bazı tipler PHP'de doğrudan yazılamaz. Bu durumda, PHP kodunu belgelemenin standart biçimi olan ve /** ile başlayıp */ ile biten phpDoc yorumunda listelenirler. Sınıfların, metotların vb. açıklamalarını eklemenizi sağlar. Ayrıca @var, @param ve @return açıklamalarıyla karmaşık tipleri listelemenizi de sağlar. Bu tipler sonra statik kod çözümleme araçlarınca kullanılır, ama PHP'nin kendisi tarafından denetlenmez.

class Registry
{
	/** @var array<Person>  bunun Person nesnelerinden oluşan bir dizi olduğunu gösterir */
	private array $persons = [];

	public function addPerson(Person $person): void
	{
		$this->persons[] = $person;
	}
}

Karşılaştırma ve Özdeşlik

PHP'de nesneleri iki şekilde karşılaştırabilirsiniz:

  1. Değer karşılaştırması ==: Nesnelerin aynı sınıftan olup olmadığını ve özelliklerinde aynı değerleri taşıyıp taşımadığını denetler.
  2. Özdeşlik ===: Nesnenin aynı örneği olup olmadığını denetler.
class Car
{
	public string $brand;

	public function __construct(string $brand)
	{
		$this->brand = $brand;
	}
}

$car1 = new Car('Skoda');
$car2 = new Car('Skoda');
$car3 = $car1;

var_dump($car1 == $car2);   // true, çünkü aynı değere sahipler
var_dump($car1 === $car2);  // false, çünkü aynı örnek değiller
var_dump($car1 === $car3);  // true, çünkü $car3, $car1 ile aynı örnek

instanceof Operatörü

instanceof operatörü, verilen bir nesnenin belirli bir sınıfın örneği mi, o sınıfın bir alt sınıfının örneği mi, yoksa belirli bir arayüzü gerçekleştirip gerçekleştirmediğini saptamanızı sağlar.

Bir Person sınıfımız ve Person sınıfının alt sınıfı olan başka bir Student sınıfımız olduğunu düşünelim:

class Person
{
	private int $age;

	public function __construct(int $age)
	{
		$this->age = $age;
	}
}

class Student extends Person
{
	private string $major;

	public function __construct(int $age, string $major)
	{
		parent::__construct($age);
		$this->major = $major;
	}
}

$student = new Student(20, 'Bilgisayar Bilimleri');

// $student'ın Student sınıfının örneği olup olmadığını denetle
var_dump($student instanceof Student);  // Çıktı: bool(true)

// $student'ın Person sınıfının örneği olup olmadığını denetle (çünkü Student, Person'ın alt sınıfıdır)
var_dump($student instanceof Person);   // Çıktı: bool(true)

Çıktılardan, $student nesnesinin hem Student hem Person sınıfının örneği sayıldığı açıkça görülüyor.

Akıcı Arayüzler

“Akıcı arayüz” (Fluent Interface), OOP'de metotların tek bir çağrıda zincirlenmesini sağlayan bir tekniktir. Bu, kodu sıklıkla basitleştirir ve netleştirir.

Akıcı arayüzün anahtar unsuru, zincirdeki her metodun geçerli nesneye bir başvuru döndürmesidir. Bu, metodun sonunda return $this; kullanılarak sağlanır. Bu programlama biçimi sıklıkla, bir nesnenin özelliklerinin değerlerini ayarlayan “setter” adlı metotlarla ilişkilendirilir.

E-posta göndermek için bir akıcı arayüzün nasıl görünebileceğine bakalım:

public function sendMessage()
{
	$email = new Email;
	$email->setFrom('sender@example.com')
		  ->setRecipient('admin@example.com')
		  ->setMessage('Merhaba, bu bir mesajdır.')
		  ->send();
}

Bu örnekte setFrom(), setRecipient() ve setMessage() metotları karşılık gelen değerleri (gönderen, alıcı, mesaj içeriği) ayarlamak için kullanılıyor. Bu değerlerin her biri ayarlandıktan sonra metotlar geçerli nesneyi ($email) döndürüyor; bu da ardına başka bir metodu zincirlememizi sağlıyor. Son olarak, e-postayı gerçekten gönderen send() metodunu çağırıyoruz.

Akıcı arayüzler sayesinde sezgisel ve kolay okunur kod yazabiliriz.

clone ile Kopyalama

PHP'de clone operatörünü kullanarak bir nesnenin kopyasını oluşturabiliriz. Böylece aynı içeriğe sahip yeni bir örnek elde ederiz.

Bir nesneyi kopyalarken bazı özelliklerini değiştirmemiz gerekiyorsa, sınıfta özel bir __clone() metodu tanımlayabiliriz. Bu metot, nesne klonlanırken otomatik çağrılır.

class Sheep
{
	public string $name;

	public function __construct(string $name)
	{
		$this->name = $name;
	}

	public function __clone()
	{
		$this->name = 'Klonu: ' . $this->name;
	}
}

$original = new Sheep('Dolly');
echo $original->name . "\n";  // Çıktı: Dolly

$clone = clone $original;
echo $clone->name . "\n";     // Çıktı: Klonu: Dolly

Bu örnekte, tek bir $name özelliği olan bir Sheep sınıfımız var. Bu sınıfın bir örneğini klonladığımızda, __clone() metodu klonlanan koyunun adının “Klonu:” önekini almasını sağlıyor.

Trait'ler

PHP'de trait'ler, sınıflar arasında metot, özellik ve sabit paylaşmayı sağlayan ve kod yinelenmesini önleyen bir araçtır. Onları, bir trait'in içeriğinin sınıflara “yapıştırıldığı” bir “kopyala ve yapıştır” mekanizması (Ctrl-C ve Ctrl-V) gibi düşünebilirsiniz. Bu, karmaşık sınıf hiyerarşileri oluşturmak zorunda kalmadan kodu yeniden kullanmanızı sağlar.

PHP'de trait'lerin nasıl kullanılacağına ilişkin basit bir örneğe bakalım:

trait Honking
{
	public function honk()
	{
		echo 'Bip bip!';
	}
}

class Car
{
	use Honking;
}

class Truck
{
	use Honking;
}

$car = new Car;
$car->honk(); // 'Bip bip!' çıktısını verir

$truck = new Truck;
$truck->honk(); // O da 'Bip bip!' çıktısını verir

Bu örnekte, tek bir honk() metodu içeren Honking adlı bir trait'imiz var. Sonra iki sınıfımız var: Car ve Truck; ikisi de Honking trait'ini kullanıyor. Sonuç olarak iki sınıf da honk() metoduna “sahip” oluyor ve onu her iki sınıfın nesneleri üzerinde çağırabiliyoruz.

Trait'ler, kodu sınıflar arasında kolayca ve verimli biçimde paylaşmanızı sağlar. Kalıtım hiyerarşisine girmezler; yani $car instanceof Honking ifadesi false döndürür.

İstisnalar

OOP'de istisnalar, kodumuzdaki hataları ve beklenmedik durumları zarif biçimde ele almamızı sağlar. Bunlar, bir hata ya da olağandışı durum hakkında bilgi taşıyan nesnelerdir.

PHP'de, tüm istisnaların temeli olarak hizmet eden yerleşik bir Exception sınıfı vardır. Bunun, istisna hakkında daha fazla bilgi almamızı sağlayan birkaç metodu vardır; örneğin hata mesajı, hatanın oluştuğu dosya ve satır vb.

Kodda bir hata oluştuğunda, istisnayı throw anahtar sözcüğüyle “fırlatabiliriz”.

function division(float $a, float $b): float
{
	if ($b == 0) {
		throw new Exception('Sıfıra bölme!');
	}
	return $a / $b;
}

division() fonksiyonu ikinci argümanı sıfır olarak aldığında, 'Sıfıra bölme!' hata mesajıyla bir istisna fırlatır. İstisna fırlatıldığında programın çökmesini önlemek için onu try/catch bloğunda yakalarız:

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'İstisna yakalandı: '. $e->getMessage();
}

İstisna fırlatabilen kod bir try bloğuyla sarılır. İstisna fırlatılırsa, kodun yürütülmesi bir catch bloğuna geçer; orada istisnayı ele alabiliriz (örneğin bir hata mesajı yazdırabiliriz).

try ve catch bloklarından sonra, istisna fırlatılmış olsun ya da olmasın her zaman çalışan isteğe bağlı bir finally bloğu ekleyebiliriz (try ya da catch bloğunda return, break veya continue kullansak bile):

try {
	echo division(10, 0);
} catch (Exception $e) {
	echo 'İstisna yakalandı: '. $e->getMessage();
} finally {
	// İstisna fırlatılmış olsun ya da olmasın her zaman çalışan kod
}

Exception sınıfından kalıtan kendi istisna sınıflarımızı (hiyerarşimizi) de oluşturabiliriz. Örnek olarak, para yatırma ve çekme işlemlerine izin veren basit bir bankacılık uygulamasını ele alalım:

class BankingException extends Exception {}
class InsufficientFundsException extends BankingException {}
class ExceededLimitException extends BankingException {}

class BankAccount
{
	private int $balance = 0;
	private int $dailyLimit = 1000;

	public function deposit(int $amount): int
	{
		$this->balance += $amount;
		return $this->balance;
	}

	public function withdraw(int $amount): int
	{
		if ($amount > $this->balance) {
			throw new InsufficientFundsException('Hesapta yeterli bakiye yok.');
		}

		if ($amount > $this->dailyLimit) {
			throw new ExceededLimitException('Günlük çekim limiti aşıldı.');
		}

		$this->balance -= $amount;
		return $this->balance;
	}
}

Farklı istisna tipleri bekliyorsanız, tek bir try bloğu için birden çok catch bloğu belirtilebilir.

$account = new BankAccount;
$account->deposit(500);

try {
	$account->withdraw(1500);
} catch (ExceededLimitException $e) {
	echo $e->getMessage();
} catch (InsufficientFundsException $e) {
	echo $e->getMessage();
} catch (BankingException $e) {
	echo 'İşlem sırasında bir hata oluştu.';
}

Bu örnekte catch bloklarının sırasına dikkat etmek önemlidir. Tüm istisnalar BankingException sınıfından kalıttığından, bu bloğu ilk sıraya koysaydık tüm istisnalar onda yakalanır ve kod sonraki catch bloklarına hiç ulaşmazdı. Bu yüzden daha özgül istisnaları (yani başkalarından kalıtanları), catch blok sırasında ana istisnalarından daha yukarıda tutmak önemlidir.

Yinelemeler

PHP'de, tıpkı bir diziyi dolaştığınız gibi nesneleri de foreach döngüsüyle dolaşabilirsiniz. Bunun çalışması için nesnenin özel bir arayüzü gerçekleştirmesi gerekir.

İlk seçenek, geçerli değeri döndüren current(), anahtarı döndüren key(), sonraki değere geçen next(), başa dönen rewind() ve sona gelip gelmediğimizi denetleyen valid() metotlarına sahip Iterator arayüzünü gerçekleştirmektir.

Diğer seçenek, yalnızca tek bir getIterator() metodu olan IteratorAggregate arayüzünü gerçekleştirmektir. Bu ya dolaşmayı sağlayacak bir aracı nesne döndürür ya da anahtarları ve değerleri sırayla döndürmek için yield kullanan özel bir fonksiyon olan bir üreteç (generator) olabilir:

class Person
{
	public function __construct(
		public int $age,
	) {
	}
}

class Registry implements IteratorAggregate
{
	private array $people = [];

	public function addPerson(Person $person): void
	{
		$this->people[] = $person;
	}

	public function getIterator(): Generator
	{
		foreach ($this->people as $person) {
			yield $person;
		}
	}
}

$list = new Registry;
$list->addPerson(new Person(30));
$list->addPerson(new Person(25));

foreach ($list as $person) {
	echo "Yaş: {$person->age} yıl\n";
}

En İyi Uygulamalar

Nesne yönelimli programlamanın temel ilkelerini kavradıktan sonra, OOP'deki en iyi uygulamalara odaklanmak çok önemlidir. Bunlar yalnızca işleyen değil, aynı zamanda okunabilir, anlaşılır ve bakımı kolay kod yazmanıza yardım eder.

  1. Sorumlulukların ayrılması: Her sınıfın açıkça tanımlı bir sorumluluğu olmalı ve yalnızca tek bir ana işi ele almalıdır. Bir sınıf çok fazla şey yapıyorsa, onu daha küçük, uzmanlaşmış sınıflara bölmek uygun olabilir.
  2. Kapsülleme: Veri ve metotlar olabildiğince gizli olmalı ve yalnızca tanımlı bir arayüz üzerinden erişilebilmelidir. Bu, kodun geri kalanını etkilemeden bir sınıfın iç gerçekleştirimini değiştirmenizi sağlar.
  3. Dependency Injection: Bağımlılıkları doğrudan bir sınıfın içinde oluşturmak yerine, onları dışarıdan “enjekte” etmelisiniz. Bu ilkeyi daha derinlemesine anlamak için Dependency Injection bölümlerini öneririz.
versiyon: 4.x