SPL (Standard PHP Library), PHP ile birlikte gelen ve kurulum gerektirmeyen bir veri yapıları ve arayüzler kütüphanesidir. Yığın (stack) için SplStack, kuyruk (queue) için SplQueue, öncelik sırasına göre işlem için SplPriorityQueue, her zaman en küçük ya da en büyük öğeyi üstte tutan yapılar için SplMinHeap ve SplMaxHeap, nesneleri anahtar olarak kullanmak için SplObjectStorage, boyutu sabit ve yalnızca tam sayı indeksli diziler için SplFixedArray, diziyi nesne gibi taşımak için ArrayObject ve ArrayIterator kullanılır.

PHP'nin normal dizisi zaten çok yönlüdür; array_push(), array_pop() ve array_shift() ile yığın ve kuyruk gibi de kullanılabilir. SPL sınıflarının farkı, amacı koda yazmaları ve her yapının kendi kurallarını uygulamalarıdır: Boş kuyruktan öğe çekmek bir istisna fırlatır, sabit dizide sınır dışı indeks hata verir, öncelik kuyruğu sıralamayı kendisi yapar. Bu yazıda her yapıyı php.net kılavuzuna göre, PHP 8.5.12 ile çalıştırılmış örneklerle ve PHP 8.4 ile 8.5'te gelen değişikliklerle birlikte ele alıyoruz. Diziler için PHP dizi temelleri yazısına, serinin tamamı için PHP öğrenme rehberi sayfasına bakabilirsiniz.

Yığın: SplStack

Yığın: SplStack

Yığın, son giren öğenin ilk çıktığı (LIFO) bir yapıdır. Tarayıcıdaki "geri" geçmişi, geri alma (undo) listesi veya iç içe parantezlerin kontrolü tipik kullanım alanlarıdır. Kılavuza göre SplStack, çift bağlı liste (SplDoublyLinkedList) üzerine kuruludur ve yineleme modunu IT_MODE_LIFO olarak ayarlar. push() üste ekler, pop() üstteki öğeyi çıkarıp döndürür, top() çıkarmadan üstteki öğeyi gösterir.

php
<?php
$yigin = new SplStack();
$yigin->push('sayfa1');
$yigin->push('sayfa2');
$yigin->push('sayfa3');
echo $yigin->top(), "\n"; // çıktı: sayfa3
echo $yigin->pop(), "\n"; // çıktı: sayfa3
echo count($yigin), "\n"; // çıktı: 2
foreach ($yigin as $oge) {
    echo $oge, ' ';
}
echo "\n";
// çıktı: sayfa2 sayfa1

foreach yığını üstten alta doğru gezdi ve öğeleri silmedi. Bunun nedeni varsayılan yineleme davranışının IT_MODE_KEEP olmasıdır; IT_MODE_DELETE seçilirse yineleyici gezdiği öğeleri siler. Kılavuz, SplStack ve SplQueue sınıflarında yineleme yönünün değiştirilemeyeceğini, denemenin RuntimeException fırlatacağını belirtir. Boş bir yapıda pop() veya top() çağırmak da RuntimeException üretir.

Kuyruk: SplQueue

Kuyruk, ilk giren öğenin ilk çıktığı (FIFO) yapıdır; sipariş işleme, iş sırası veya genişlik öncelikli arama gibi durumlarda kullanılır. SplQueue da çift bağlı liste üzerine kuruludur ve enqueue() ile sona ekler, dequeue() ile baştan çıkarır. Dizi sözdizimiyle $kuyruk[] = ... yazmak da sona ekler, çünkü sınıf ArrayAccess arayüzünü uygular.

php
<?php
$kuyruk = new SplQueue();
$kuyruk->enqueue('ilk sipariş');
$kuyruk->enqueue('ikinci sipariş');
$kuyruk[] = 'üçüncü sipariş';
while (!$kuyruk->isEmpty()) {
    echo $kuyruk->dequeue(), "\n";
}
try {
    $kuyruk->dequeue();
} catch (RuntimeException $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}
/*
ilk sipariş
ikinci sipariş
üçüncü sipariş
RuntimeException: Can't shift from an empty datastructure
*/

Aynı işi normal diziyle yapmak mümkündür, fakat davranış farklıdır: Boş bir dizide array_shift() istisna fırlatmaz, sessizce null döndürür. null değerinin gerçek bir öğe mi yoksa boş kuyruk mu olduğunu ayırt etmek zorlaşır. Normal dizi ile kuyruk ve yığın davranışı şöyledir:

php
<?php
$dizi = ['a', 'b', 'c'];
array_push($dizi, 'd');
echo array_pop($dizi), "\n";   // çıktı: d
echo array_shift($dizi), "\n"; // çıktı: a
print_r($dizi);
/*
Array
(
    [0] => b
    [1] => c
)
*/

array_shift() ayrıca kalan elemanların sayısal anahtarlarını yeniden numaralandırır; yukarıda b artık 0 indeksinde. Dizi fonksiyonlarının tamamı PHP dizi fonksiyonları yazısında.

SplStack ve SplQueue sınıflarının ikisi de SplDoublyLinkedList sınıfından türer. Bu yüzden iki uçtan da işlem yapan metotlar her ikisinde de bulunur: push() ve pop() listenin sonunda, unshift() ve shift() listenin başında çalışır; top() sondaki, bottom() baştaki öğeyi çıkarmadan gösterir. Her iki uçtan ekleme ve çıkarma yapacağınız bir yapıya ihtiyacınız varsa doğrudan SplDoublyLinkedList sınıfını kullanabilir, yineleme yönünü ve silme davranışını setIteratorMode() ile kendiniz belirleyebilirsiniz.

Öncelik kuyruğu: SplPriorityQueue

SplPriorityQueue her öğeyi bir öncelik değeriyle birlikte saklar ve extract() her seferinde en yüksek öncelikli öğeyi döndürür. Kılavuza göre bu sınıf bir max heap ile uygulanmıştır. insert($deger, $oncelik) ile eklenir. setExtractFlags() ile extract() çağrısının yalnızca veriyi (EXTR_DATA, varsayılan), yalnızca önceliği (EXTR_PRIORITY) ya da ikisini birden (EXTR_BOTH) döndürmesini seçebilirsiniz.

php
<?php
$gorevler = new SplPriorityQueue();
$gorevler->insert('e-posta gönder', 1);
$gorevler->insert('sunucu alarmı', 10);
$gorevler->insert('rapor hazırla', 5);
echo count($gorevler), "\n"; // çıktı: 3
while (!$gorevler->isEmpty()) {
    echo $gorevler->extract(), "\n";
}
/*
sunucu alarmı
rapor hazırla
e-posta gönder
*/

$q = new SplPriorityQueue();
$q->setExtractFlags(SplPriorityQueue::EXTR_BOTH);
$q->insert('yedekle', 3);
print_r($q->extract());
/*
Array
(
    [data] => yedekle
    [priority] => 3
)
*/

Bu dizi önerisi PHP'nin dizi karşılaştırma kuralına dayanır: Aynı uzunluktaki iki dizi eleman eleman karşılaştırılır. Karşılaştırma kuralları eşitlik ve karşılaştırma yazısında, <=> operatörü PHP operatörleri yazısında anlatılıyor.

Yığın ağacı (heap): SplMinHeap ve SplMaxHeap

Yığın ağacı (heap): SplMinHeap ve SplMaxHeap

Heap, en küçük (SplMinHeap) veya en büyük (SplMaxHeap) öğeyi her zaman tepede tutan bir yapıdır. Öğelerin öncelik yerine kendi değerleriyle sıralanması gerektiğinde, örneğin en yakın tarihli işi ya da en düşük fiyatı hızlıca almak istediğinizde kullanılır. insert() ekler, top() tepedeki öğeyi gösterir, extract() çıkarır. Kendi sıralama kuralınız için SplHeap sınıfını genişletip compare() metodunu yazarsınız.

php
<?php
$h = new SplMinHeap();
foreach ([42, 7, 19, 3, 25] as $n) {
    $h->insert($n);
}
echo $h->top(), "\n"; // çıktı: 3
foreach ($h as $n) {
    echo $n, ' ';
}
echo "\n", count($h), "\n";
/*
3 7 19 25 42
0
*/

Bu çıktıdaki ayrıntı sürpriz olabilir: foreach sonrasında heap boşaldı. Kılavuzdaki SplHeap::next() sayfası, yineleyicinin bir sonraki öğeye geçerken tepedeki düğümü sildiğini belirtir. Yani bir heap'i gezmek onu tüketir. Öğeleri korumak istiyorsanız gezmeden önce clone ile kopyalayın; nesne kopyalama clone ve nesne karşılaştırma yazısında anlatılıyor. Değerleri yalnızca bir kez sıralı almak istiyorsanız normal dizi ve sort() daha basit olabilir; bkz. dizi sıralama.

Nesneleri anahtar yapmak: SplObjectStorage

PHP dizilerinin anahtarları yalnızca tam sayı veya string olabilir; bir nesneyi dizi anahtarı olarak kullanamazsınız. SplObjectStorage bu boşluğu doldurur: Kılavuz onu nesnelerden veriye bir eşleme (map) ya da veri kısmı yok sayılırsa bir nesne kümesi (set) olarak tanımlar. Bir nesne aynı depoya yalnızca bir kez girer; aynı nesneye yeniden değer atamak önceki değeri değiştirir.

php
<?php
final class Kullanici
{
    public function __construct(public string $ad) {}
}
$ayse = new Kullanici('Ayşe');
$ali = new Kullanici('Ali');

$oturumlar = new SplObjectStorage();
$oturumlar[$ayse] = ['giris' => '09:15'];
$oturumlar[$ali] = ['giris' => '10:02'];
$oturumlar[$ayse] = ['giris' => '11:30'];

echo count($oturumlar), "\n";              // çıktı: 2
var_dump(isset($oturumlar[$ali]));         // çıktı: bool(true)
echo $oturumlar[$ayse]['giris'], "\n";     // çıktı: 11:30
unset($oturumlar[$ali]);
var_dump(isset($oturumlar[$ali]));         // çıktı: bool(false)

foreach ($oturumlar as $i => $kullanici) {
    echo $i, ': ', $kullanici->ad, ' ', $oturumlar[$kullanici]['giris'], "\n";
}
// çıktı: 0: Ayşe 11:30
var_dump(isset($oturumlar[new Kullanici('Ayşe')])); // çıktı: bool(false)

Son satır, depolamanın nesnenin kimliğine göre çalıştığını gösterir: Aynı özelliklere sahip yeni bir nesne, depodaki nesneyle aynı sayılmaz. foreach içinde anahtar sıra numarasıdır, değer ise nesnenin kendisidir; ilişkili veriye $oturumlar[$kullanici] ile ulaşılır. Nesne kimliği ve == ile === farkı clone ve nesne karşılaştırma yazısında anlatılıyor; orada nesne silinince girdisi de kaybolan WeakMap sınıfına da kısaca değiniliyor.

PHP 8.5: attach, detach ve contains kullanımdan kaldırıldı

PHP 8.5 geçiş notlarına göre SplObjectStorage::attach(), detach() ve contains() metotları kullanımdan kaldırıldı; yerlerine sırasıyla offsetSet(), offsetUnset() ve offsetExists() önerilir. Yukarıdaki örnekte kullanılan $depo[$nesne] = $veri, unset($depo[$nesne]) ve isset($depo[$nesne]) sözdizimi zaten bu metotları çağırır ve eski PHP sürümlerinde de çalışır. Eski metodu kullandığınızda PHP 8.5.12 şu uyarıyı verir:

php
<?php
$s = new SplObjectStorage();
$s->attach(new stdClass());
/*
Deprecated: Method SplObjectStorage::attach() is deprecated since 8.5, use method SplObjectStorage::offsetSet() instead in /yol/ornek.php on line 3
*/

PHP 8.4 ile de SplObjectStorage sınıfı SeekableIterator arayüzünü uygulamaya başladı ve yeni seek() metodu eklendi. Kullanımdan kaldırma uyarılarının nasıl ele alınacağı deprecated uyarıları yazısında, #[\Deprecated] özniteliği PHP attributes yazısında anlatılıyor.

Sabit boyutlu dizi: SplFixedArray

SplFixedArray boyutu önceden belirlenen ve yalnızca bu aralıktaki tam sayıları indeks olarak kabul eden bir dizidir. Kılavuz, normal diziden temel farkının boyutun elle değiştirilmesi ve yalnızca aralık içindeki tam sayı indekslere izin vermesi olduğunu, avantajının da standart diziden daha az bellek kullanması olduğunu belirtir. Atanmamış elemanlar null değerindedir.

php
<?php
$sabit = new SplFixedArray(3);
$sabit[0] = 'pazartesi';
$sabit[2] = 'çarşamba';
var_dump($sabit[1]);           // çıktı: NULL
echo $sabit->getSize(), "\n"; // çıktı: 3
$sabit->setSize(5);
echo count($sabit), "\n";      // çıktı: 5
print_r($sabit->toArray());
/*
Array
(
    [0] => pazartesi
    [1] => 
    [2] => çarşamba
    [3] => 
    [4] => 
)
*/
try {
    $sabit[7] = 'x';
} catch (OutOfBoundsException $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}
try {
    $sabit['iki'] = 'x';
} catch (Throwable $e) {
    echo get_class($e), ': ', $e->getMessage(), "\n";
}
/*
OutOfBoundsException: Index invalid or out of range
TypeError: Cannot access offset of type string on SplFixedArray
*/

PHP 8.4 geçiş notlarına göre sınır dışı erişimler artık RuntimeException yerine OutOfBoundsException fırlatıyor. OutOfBoundsException, RuntimeException sınıfının alt sınıfı olduğu için eski catch (RuntimeException $e) blokları çalışmaya devam eder. PHP 8.4'te SplFixedArray::__wakeup() kullanımdan kaldırıldı; alt sınıflarda __serialize() ve __unserialize() geçersiz kılınmalıdır. PHP 8.5 notları da sınıfın indeks erişimlerinde ve metotlarında performans iyileştirmesi yapıldığını belirtir. Normal diziye dönüştürmek için toArray(), diziden oluşturmak için statik SplFixedArray::fromArray() kullanılır.

Diziyi nesne olarak taşımak: ArrayObject ve ArrayIterator

ArrayObject bir diziyi nesne içine sarar; nesne ArrayAccess, Countable ve IteratorAggregate arayüzlerini uyguladığı için köşeli parantezle erişim, count() ve foreach çalışır. ArrayObject::ARRAY_AS_PROPS bayrağı elemanlara özellik sözdizimiyle ($nesne->anahtar) erişmeye izin verir. Asıl pratik fark, nesnelerin fonksiyonlara dizi gibi kopyalanarak değil, aynı nesne olarak geçmesidir:

php
<?php
$ayarlar = new ArrayObject(['tema' => 'koyu', 'dil' => 'tr'], ArrayObject::ARRAY_AS_PROPS);
echo $ayarlar['tema'], ' ', $ayarlar->dil, "\n"; // çıktı: koyu tr
$ayarlar['saat'] = 'Europe/Istanbul';
echo count($ayarlar), "\n"; // çıktı: 3

function ekle(array $a) { $a['yeni'] = 1; return count($a); }
function ekleNesne(ArrayObject $a) { $a['yeni'] = 1; return count($a); }
$dizi = ['x' => 1];
echo ekle($dizi), ' ', count($dizi), "\n";             // çıktı: 2 1
echo ekleNesne($ayarlar), ' ', count($ayarlar), "\n"; // çıktı: 4 4

$it = new ArrayIterator([3, 1, 2]);
$it->asort();
print_r($it->getArrayCopy());
/*
Array
(
    [1] => 1
    [2] => 2
    [0] => 3
)
*/
var_dump(is_array($ayarlar));    // çıktı: bool(false)
var_dump(is_iterable($ayarlar)); // çıktı: bool(true)

Normal dizi fonksiyona kopyalanarak geçtiği için fonksiyon içindeki ekleme dışarıyı etkilemedi; ArrayObject ise aynı nesne olduğu için değişiklik dışarıda da görüldü. Bu davranış referans semantiğinin bir sonucudur ve PHP referansları ile clone yazılarında ayrıntılı anlatılıyor. ArrayIterator ise dizi üzerinde gezinirken eleman silmenize ve değiştirmenize izin veren bir yineleyicidir; asort() gibi sıralama metotları da vardır. Dikkat: ArrayObject gerçek bir dizi değildir; is_array() false döner ve array_map() gibi array tipi bekleyen fonksiyonlara doğrudan verilemez. Gerektiğinde getArrayCopy() ile diziye çevirirsiniz.

Kılavuz, ArrayObject ile nesne sarmanın temelden sorunlu olduğunu ve bu kullanımdan kaçınılması gerektiğini belirtir. PHP 8.5'te ArrayObject ve ArrayIterator sınıflarının nesnelerle kullanımı resmen kullanımdan kaldırıldı ve enum kabul edilmiyor. Bu iki sınıfı yalnızca dizilerle kullanın:

php
<?php
$o = new ArrayObject(new stdClass());
/*
Deprecated: ArrayObject::__construct(): Using an object as a backing array for ArrayObject is deprecated, as it allows violating class constraints and invariants in /yol/ornek.php on line 2
*/

Hangi yapıyı ne zaman seçmeli?

Hangi yapıyı ne zaman seçmeli? SplStack, SplQueue, SplPriorityQueue, SplMinHeap / SplMaxHeap
YapıDüzenTemel metotlarTipik kullanım
SplStackLIFOpush, pop, topgeri alma, geçmiş, parantez kontrolü
SplQueueFIFOenqueue, dequeueiş sırası, genişlik öncelikli arama
SplPriorityQueueen yüksek öncelik önceinsert, extract, setExtractFlagsgörev önceliklendirme
SplMinHeap / SplMaxHeapen küçük / en büyük önceinsert, extract, topen yakın tarih, en düşük fiyat
SplObjectStoragenesne anahtarlı eşlemeoffsetSet, offsetExists, offsetUnsetnesneye veri bağlama, nesne kümesi
SplFixedArraysabit boyut, tam sayı indekssetSize, toArray, fromArrayboyutu bilinen büyük listeler
ArrayObjectdizi saran nesnegetArrayCopy, ARRAY_AS_PROPSdiziyi nesne olarak paylaşmak

Çoğu günlük iş için normal PHP dizisi yeterlidir. SPL yapıları, kodun niyetini açıkça göstermek, kuralları (boş yapıdan okuma, sınır dışı indeks) istisnayla zorlamak veya nesneleri anahtar olarak kullanmak gibi somut bir ihtiyaç olduğunda değer kazanır. Bu sınıflar global ad alanındadır; bir ad alanı içinden kullanırken başlarına \ koyun ya da use ile içe aktarın. Ayrıntı PHP namespace ve use yazısında.

Uygulamalı örnek: parantez dengesini kontrol etmek

Yığının klasik kullanımlarından biri, bir ifadedeki parantezlerin doğru eşleşip eşleşmediğini kontrol etmektir. Açılan her parantez yığına eklenir, kapanan parantez yığının tepesindeki açılışla eşleşmelidir.

php
<?php
function dengeliMi(string $ifade): bool
{
    $esler = [')' => '(', ']' => '[', '}' => '{'];
    $yigin = new SplStack();
    foreach (str_split($ifade) as $karakter) {
        if (in_array($karakter, $esler, true)) {
            $yigin->push($karakter);
        } elseif (isset($esler[$karakter])) {
            if ($yigin->isEmpty() || $yigin->pop() !== $esler[$karakter]) {
                return false;
            }
        }
    }
    return $yigin->isEmpty();
}

var_dump(dengeliMi('f(a[1], {b: 2})')); // çıktı: bool(true)
var_dump(dengeliMi('f(a[1)]'));         // çıktı: bool(false)
var_dump(dengeliMi('((x)'));            // çıktı: bool(false)

isEmpty() kontrolü, boş yığında pop() çağrılıp RuntimeException fırlatılmasını önler. Fonksiyon sonunda yığının boş olması, açılan her parantezin kapatıldığını gösterir. str_split() bayt bazında böler; ifadede Türkçe karakter olsa bile parantezler tek baytlık ASCII karakterler olduğu için kontrol doğru çalışır. Karakter bazında bölmek gerekirse mbstring yazısındaki mb_str_split() kullanılabilir.

Sık yapılan hatalar

  • Boş bir SplQueue veya SplStack üzerinde kontrol etmeden dequeue(), pop() ya da top() çağırmak. Önce isEmpty() kontrol edin ya da RuntimeException yakalayın.
  • SplPriorityQueue içinde eşit öncelikli öğelerin ekleme sırasıyla çıkacağını varsaymak. Kılavuza göre sıra tanımsızdır.
  • Bir heap'i foreach ile gezip sonra yeniden kullanmaya çalışmak. Yineleme heap'i boşaltır.
  • PHP 8.5'te SplObjectStorage::attach(), detach() ve contains() kullanmaya devam etmek. Dizi sözdizimine geçin.
  • SplFixedArray ile string anahtar kullanmaya çalışmak. Yalnızca aralık içindeki tam sayı indeksler kabul edilir.
  • ArrayObject nesnesini array tipi bekleyen bir fonksiyona vermek. getArrayCopy() ile diziye çevirin.
  • ArrayObject ile bir nesneyi sarmak. PHP 8.5'te deprecated; yalnızca dizilerle kullanın.

Sık Sorulan Sorular

SPL kullanmak için bir eklenti kurmam gerekir mi?

Hayır. SPL PHP ile birlikte gelir ve bu yazıdaki sınıflar ek kurulum gerektirmeden kullanılabilir. Bu yazının örnekleri de php.ini olmadan çalıştırılan PHP 8.5.12 ile denendi.

SplStack mi, normal dizi mi?

İkisi de çalışır. SplStack niyeti açıkça gösterir ve boş yığından okumayı istisnayla bildirir. Basit ve kısa ömürlü bir liste için array_push() ve array_pop() yeterlidir.

SplObjectStorage ile WeakMap arasındaki fark nedir?

SplObjectStorage içine koyduğunuz nesneleri tutar; nesne başka yerde kullanılmasa bile depoda yaşamaya devam eder. WeakMap ise anahtar nesnenin başka referansı kalmadığında girdiyi kendiliğinden bırakır. Önbellek gibi nesnenin yaşam süresini uzatmak istemediğiniz durumlarda WeakMap düşünülebilir.

SPL'de başka neler var?

Bu yazı veri yapılarına odaklandı. SPL ayrıca çok sayıda yineleyici sınıfı, SplFileObject gibi dosya sınıfları, LogicException ve RuntimeException ailesinden istisna sınıfları ve sınıf otomatik yükleme fonksiyonu spl_autoload_register() içerir. Otomatik yükleme spl_autoload_register, istisna sınıfları try/catch ve exception, yineleme için alternatif bir yol da generator ve yield yazısında anlatılıyor.

Marka adları sahiplerine aittir. Bu yazı bağımsız bir teknik rehberdir; adı geçen kuruluşlarla bayilik veya ortaklık ilişkisi bulunduğu anlamına gelmez.

İlgili içerikler: PHP öğrenme rehberi · PHP dizi temelleri · PHP dizi fonksiyonları · PHP clone ve nesne karşılaştırma · PHP 8.5 yenilikleri