İçeriğe atla
Üyelik kaydınızı yaparak son yazılan içeriklerden haberdar olun! ×

PHP

  • makale
    10
  • yorum
    0
  • görüntüleme
    865

PHP'nin Type Sistemi


Doğuhan ELMA

54 görünüm

PHP 7'nin en önemli özelliklerinden biri, performansın yanı sıra, geliştirilmiş tip sistemidir. Kabul etmek gerekir ki, çoğu temel özelliğin hayata geçirilmesi PHP 8.0'a kadar sürdü, ancak genel olarak PHP'nin tip sistemi yıllar içinde önemli ölçüde gelişti. Olgunlaşan tip sistemiyle birlikte, bazı topluluk projeleri tipleri tam anlamıyla kullanmaya başladı. Statik analizörler oluşturuldu ve bu da programlamanın bambaşka bir yolunun kapılarını açtı. Bir sonraki makalede bu statik analizörlerin faydalarını daha derinlemesine inceleyeceğiz. Şimdilik, PHP'nin tip sisteminde sürüm 5 ile 8 arasında tam olarak nelerin değiştiğine odaklanacağız. Her şeyden önce, daha fazla yerleşik tip eklendi: "skaler" tipler olarak adlandırıldı. Bu tipler tamsayılar, dizeler, booleanlar ve kayan sayılardır:

public function formatMoney(int $money): string
{
 // …
}

 

Daha sonra (bunu önceki örnekte zaten fark etmiş olabilirsiniz) dönüş türleri eklendi. PHP 7'den önce türleri zaten kullanabiliyordunuz ama sadece girdi parametreleri için. Bu da doküman blok tipleri ve satır içi tiplerin bir karışımına yol açıyordu - bazıları buna karmaşa diyebilir:

/**
 * @param \App\Offer $offer
 * @param bool $sendMail
 *
 * @return \App\Offer
 */
public function createOffer(Offer $offer, $sendMail)
{
 // …
}

 

Bu tür dağınık kodlarla uğraşmak istemeyen bazı geliştiriciler türleri hiç kullanmamayı tercih etti. Sonuçta, doc block tipleri hiç yorumlanmadığı için yalnızca sınırlı düzeyde "tip güvenliği" vardı. Öte yandan, IDE'niz doc block versiyonunu anlayabilirdi, ancak bazı insanlar için bu yeterli bir fayda değildi. Ancak PHP 7.0'dan itibaren, önceki örnek şu şekilde yeniden yazılabilir:

public function createOffer(Offer $offer, bool $sendMail): Offer
{
 // …
}

Bu aynı bilgidir, ancak çok daha özlüdür ve yorumlayıcı tarafından gerçekten kontrol edilir - bir tip sisteminin ve tip güvenliğinin faydalarına daha sonra değineceğiz. Parametre ve dönüş tiplerinin yanı sıra, artık sınıflar için tiplendirilmiş özellikler de vardır. Tıpkı parametre ve dönüş tipleri gibi, bunlar da isteğe bağlıdır; PHP herhangi bir tip kontrolü yapmaz.

class Offer
{
 public string $offerNumber;
 public Money $totalPrice;
}

Parametre ve dönüş türlerinde olduğu gibi hem skaler türlere hem de nesnelere izin verildiğini görebilirsiniz. Şimdi, tiplendirilmiş özelliklerin ilginç bir özelliği vardır. Aşağıdaki örneğe bir göz atın ve ilk bakışta düşündüğünüzün aksine, geçerli olduğunu ve herhangi bir hata vermeyeceğini fark edin:

class Money
{
 public int $amount;
}
$money = new Money();

Bu örnekte, Money'nin bir kurucusu yoktur ve $amount özelliğine bir değer atanmamıştır. Money nesnesi oluşturulduktan sonra $amount değeri bir tamsayı olmasa bile, PHP yalnızca kodun ilerleyen kısımlarında bu özelliğe erişmeye çalıştığımızda hata verecektir:

var_dump($money->amount);
// Fatal error: Typed property Money::$amount
// must not be accessed before initialization

 

Hata mesajında görebileceğiniz gibi, yeni bir tür değişken durumu var: başlatılmamış. Eğer $amount bir türe sahip olmasaydı, değeri basitçe null olurdu. Tipli özellikler null olabilir, bu nedenle unutulan bir tip ile null olan bir tip arasında ayrım yapmak önemlidir. Bu yüzden "uninitialized" eklenmiştir. Uninitialized hakkında hatırlanması gereken birkaç önemli şey vardır:

- Az önce gördüğümüz gibi, başlatılmamış özelliklerden okuma yapamazsınız; bunu yapmak ölümcül bir hataya neden olur.

- Bir özelliğe erişirken uninitialized durumu kontrol edildiğinden, uninitialized özelliğe sahip bir nesne oluşturabilirsiniz.

- Başlatılmamış bir özelliği okumadan önce bu özelliğe yazmanıza izin verilir; bu da $money >amount = 1 ifadesini oluşturduktan sonra yazabileceğiniz anlamına gelir.

- Typed bir özellik üzerinde unset kullanmak özelliği uninitialized hale getirirken, unset untyped bir özelliği null hale getirir.

- Uninitialized durumu yalnızca bir özelliğin değeri okunurken kontrol edilirken, tip doğrulaması özelliğe yazılırken yapılır. Bu, geçersiz bir türün hiçbir zaman bir özelliğin değeri olmayacağından emin olabileceğiniz anlamına gelir. Bu davranışın bir programlama dilinin yapmasını beklediğiniz şey olmayabileceğinin farkındayım. Daha önce PHP'nin güncellemeleriyle geriye dönük uyumluluğu bozmamak için çok uğraştığından bahsetmiştim, bu da bazen başlatılmamış durum gibi tavizlerle sonuçlanıyor. Bir değişkenin durumunun bir nesne oluşturulduktan hemen sonra kontrol edilmesi gerektiğini düşünsem de, elimizdekiyle yetinmek zorundayız. En azından bu, bir sonraki makalede tartışacağımız statik analizden yararlanmak için bir başka iyi neden.

 

Null ile başa çıkma:

Hazır başlatılmamış durum konusuna girmişken, null kavramını da tartışalım. Bazıları null kavramını "milyar dolarlık hata" olarak adlandırmış ve kod yazarken dikkate almamız gereken bir dizi uç duruma izin verdiğini savunmuştur. Null'u desteklemeyen bir programlama dilinde çalışmak garip görünebilir, ancak null'un yerini alacak ve tuzaklarından kurtulacak faydalı kalıplar vardır. Önce bu dezavantajları bir örnekle açıklayalım. Burada zaman damgası değişkeni, format fonksiyonu ve now adında statik kurucusu olan bir Date değeri nesnemiz var.

class Date
{
    public int $timestamp;
    public static function now(): self { /* … */ }
    public function format(): string { /* … */ }
    // …
}

Ardından, ödeme tarihi olan bir faturamız var:

class Invoice
{
 public ?Date $paymentDate = null;
 // …
}

Ödeme tarihi nullable'dır çünkü faturalar beklemede olabilir ve henüz bir ödeme tarihine sahip olmayabilir. Bir yan not olarak: nullable gösterimine bir göz atın; bundan daha önce bahsetmiştim ama şimdiye kadar göstermemiştim. Date'in önüne bir soru işareti ekleyerek Date ya da null olabileceğini belirttik. Ayrıca, karşılaşabileceğiniz tüm çalışma zamanı hatalarını önlemek için değerin asla başlatılmamasını sağlayan bir varsayılan = null değeri ekledik. Örneğimize geri dönelim: Ödeme tarihimizin zaman damgasıyla bir şey yapmak istersek ne olur?

$invoice->paymentDate->timestamp;

$invoice >paymentDate'in bir Date veya null olduğundan emin olmadığımız için
çalışma zamanı hataları:

// Trying to get property 'timestamp' of non-object

PHP 7.0'dan önce, bu tür hataları önlemek için isset kullanırdınız:

isset($invoice >paymentDate)
 ? $invoice >paymentDate >timestamp
 : null;

Yine de bu oldukça ayrıntılıdır ve bu nedenle yeni bir operatör tanıtılmıştır: null birleştirme operatörü.

$invoice->paymentDate->timestamp ?? null;

Bu işleç, sol işleneninde otomatik olarak bir isset denetimi gerçekleştirir. Bu kontrol yanlış sonuç verirse, sağ işleneni tarafından sağlanan geri dönüşü döndürür. Bu durumda, ödeme tarihinin zaman damgası veya null. Bu, kodumuzun karmaşıklığını azaltan güzel bir eklentidir. PHP 7.4 başka bir null birleştirme kısaltması ekledi: null birleştirme atama işleci. Bu sadece varsayılan değer geri dönüşünü desteklemekle kalmaz, aynı zamanda onu doğrudan sol taraftaki işlenene yazar. Şöyle görünür:

$temporaryPaymentDate = $invoice->paymentDate ??= Date::now();

Dolayısıyla, ödeme tarihi zaten ayarlanmışsa, bunu $temporaryPaymentDate içinde kullanırız, aksi takdirde $temporaryPaymentDate için geri dönüş olarak Date :now() kullanırız ve ayrıca bunu hemen $invoice >paymentDate'e yazarız. Null birleştirme atama operatörü için daha yaygın bir kullanım örneği, bir memoization fonksiyonudur: sonucu hesaplandıktan sonra saklayan bir fonksiyon. Bu işlev, bir dizeyle bir desen üzerinde regex eşleştirmesi gerçekleştirir, ancak aynı dize ve aynı desen sağlanırsa, önbelleğe alınan sonucu döndürür.

<?php

function match_pattern(string $input, string $pattern) {
    static $cache = [];
    return $cache[$input][$pattern] ??=
 (function (string $input, string $pattern) {
     preg_match($pattern, $input, $matches);
     return $matches[0];
 })($input, $pattern);
}

Null birleştirme operatörü ataması olmadan önce, bunu şu şekilde yazmamız gerekirdi:

function match_pattern(string $input, string $pattern) {
    static $cache = [];
    $key = $input . $pattern;

    if (! isset($cache[$key])) {
        $cache[$key] = (function (string $input, string $pattern) {
            preg_match($pattern, $input, $matches);

            return $matches[0];
        })($input, $pattern);
    }
    return $cache[$key];
}

PHP'ye PHP 8.0'da eklenen null odaklı bir özellik daha var: nullsafe operatörü. Bu örneğe bir göz atın:

$invoice->paymentDate->format();

Ödeme tarihimiz null ise ne olur? Yine bir hata alırsınız:

// Call to a member function format() on null

İlk aklınıza gelen null birleştirme operatörünü kullanmak olabilir, ancak bu işe yaramaz:

$invoice->paymentDate->format('Y-m-d') ?? null;

null birleştirme işleci null üzerinde yapılan yöntem çağrılarında çalışmaz. Bu yüzden PHP 8.0'dan önce bunu yapmanız gerekirdi:

$paymentDate = $invoice->paymentDate;
$paymentDate ? $paymentDate->format('Y-m-d') : null;

Neyse ki, yalnızca mümkün olduğunda yöntem çağrılarını gerçekleştirecek ve aksi takdirde bunun yerine null döndürecek nullsafe operatörü vardır:

$invoice->getPaymentDate()?->format('Y-m-d');

 

 

Null ile başa çıkmanın başka bir yolu var:

Bu bölüme null'un "milyar dolarlık bir hata" olduğunu söyleyerek başladım, ancak daha sonra PHP'nin süslü sözdizimiyle null'u kucakladığı üç yolu gösterdim. Gerçek şu ki, null PHP'de sıkça karşılaşılan bir durumdur ve bununla aklı başında bir şekilde başa çıkmak için sözdizimine sahip olmamız iyi bir şeydir. Bununla birlikte, null'u tamamen kullanmanın alternatiflerine bakmak da iyidir. Bu alternatiflerden biri null nesne modelidir. Ödenip ödenmediği ile ilgili dahili durumu yöneten bir Invoice sınıfı yerine; iki sınıfımız olsun: PendingInvoice ve PaidInvoice. PendingInvoice uygulaması şu şekilde görünür:

class PendingInvoice extends Invoice
{
    public function getPaymentDate(): UnknownDate
    {
        return new UnknownDate();
    }
}

PaidInvoice şu şekilde görünür:

class PaidInvoice implements Invoice
{
 // …
 public function getPaymentDate(): Date
 {
 return $this->date;
 }
}

 

Sonra, bir Invoice arayüzü var:

interface Invoice
{
 public function getPaymentDate(): Date;
}

Son olarak, işte iki tarih sınıfı:

class Date
{
 // …
}
class UnknownDate extends Date
{
 public function format(): string
 {
 return '/';
 }
}

Null nesne kalıbı, null yerine gerçek nesneleri, gerçek nesnenin "yokluğunu" temsil ettikleri için farklı davranan nesneleri koymayı amaçlar. Bu kalıbı kullanmanın bir diğer faydası da sınıfların gerçek dünyayı daha iyi temsil eder hale gelmesidir: "tarih veya null" yerine "tarih veya bilinmeyen tarih", "durumu olan fatura" yerine "ödenmiş fatura veya bekleyen fatura". Artık null hakkında endişelenmenize gerek kalmayacaktır.

$invoice >getPaymentDate() >format(); // A date or '/'

 

Bu model hoşunuza gitmeyebilir, ancak sorunun şu şekilde çözülebileceğini bilmek önemlidir Bu şekilde çözüldü.

 

Değişen tipler:

Yukarıdaki kod örneklerinde bir gerçek olarak yazdığım bir şey oldu, ancak bu PHP'nin tip sistemine önemli bir katkıdır. Kalıtım sırasında yöntem imzalarını değiştirebildik. Fatura arayüzüne bir göz atın:

interface Invoice
{
 public function getPaymentDate(): Date;
}

getPaymentDate öğesinin dönüş türünü Date olarak bildirir, ancak biz bunu PendingInvoice öğemizde UnknownDate olarak değiştirdik:

class PendingInvoice implements Invoice
{
 public function getPaymentDate(): UnknownDate
 {
 /* … */
 }
}

Bu güçlü tekniğe tip değişimi denir; PHP 7.4'ten itibaren desteklenmektedir. O kadar güçlü (ve biraz karmaşık) ki, bu kitabın ilerleyen bölümlerinde bu konuya tam bir bölüm ayıracağız. Şimdilik bilmeniz gereken tek şey, dönüş türlerinin ve girdi parametre türlerinin kalıtım sırasında değişmesine izin verildiği, ancak her ikisinin de uyması gereken farklı kurallar olduğudur. Bir başka ilginç ayrıntı da nullable tiplerle ilgilidir. ? kullanarak nullable bir tip ile default = null değeri arasında bir fark vardır; daha önceki bir örnekte bunların birlikte kullanıldığını görmüştünüz. Aradaki fark şudur: PHP'de bir türü nullable yaparsanız, yine de o işleve bir şey geçirmeniz beklenir; parametreyi atlayamazsınız:

function createOrUpdate(?Offer $offer): void
{
 // …
}
createOrUpdate();
// Uncaught ArgumentCountError:
// Too few arguments to function createOrUpdate(),
// 0 passed and exactly 1 expected

Dolayısıyla, açık bir = null varsayılan değeri ekleyerek, değeri atlamanıza izin verilir Hep birlikte:

function createOrUpdate(?Offer $offer = null): void
{
 // …
}
createOrUpdate();

Ne yazık ki, geriye dönük uyumluluk nedeniyle, bu sistemde küçük bir tuhaflık vardır. Eğer bir değişkene default = null değeri atarsanız, değişken her zaman null edilebilir olacaktır.

Türün açıkça nullable yapılıp yapılmadığına bakılmaksızın, bu İzin verildi:

function createOrUpdate(Offer $offer = null): void
{
 // …
}
createOrUpdate(null);

 

 

Birleşme ve Diğer Türler:

Bu bölümde bahsetmeye değer birkaç şey daha var ve en heyecan verici olanı birlik tipleridir. Bu türler, bir değişkeni birkaç türle işaretlemenizi sağlar. Not: Girdinin bildirilen türlerden biri olması gerekir. İşte bir örnek:

interface Repository
{
 public function find(int|string $id);
}

 

Bunun gibi ? operatörünü kullanarak bir birlik türünü null yapmak mümkün değildir: ?int|string bunun yerine union tipleriyle birlikte tanıtılan null tipi kullanılmalıdır: int|string|null.

Yine de birlik türlerini aşırı kullanmamaya dikkat edin. Aynı union içinde çok fazla tür olması, bu fonksiyonun aynı anda çok fazla şey yapmaya çalıştığını gösterebilir. Örneğin: bir framework, denetleyici yöntemlerinden hem bir Response hem de View nesnesi döndürmenize izin verebilir. Bazen doğrudan bir View döndürmek uygun olurken, diğer zamanlarda yanıt üzerinde ince taneli kontrole sahip olmak istersiniz. Bunlar, Response|View üzerinde bir union türünün iyi olduğu durumlardır. Öte yandan, array|Collection|string birleşimini kabul eden bir fonksiyonunuz varsa, bu muhtemelen fonksiyonun çok fazla şey yapması gerektiğinin bir göstergesidir. Bu gibi durumlarda ortak bir zemin bulmayı düşünmek en iyisidir; belki sadece Collection veya array kabul edebilir.

 

 

Kesişim türleri:

PHP 8.1 tipleri yazmanın başka bir yolunu ekler, bunlar union tiplerine benzer görünebilir, ancak küçük bir farkla. Birlik tipleri belirli bir girdinin A veya B tipinde olmasını gerektirirken, kesişim tipleri girdinin A ve B tipinde olmasını gerektirir. Tek bir sınıfın birkaç farklı arayüzü uygulayabildiği arayüzleri düşünün. Örneğin bu iki arayüzü ele alalım:

interface WithUuid
{
 public function getUuid(): Uuid;
}
interface WithSlug
{
 public function getSlug(): string;
}

Bir Post sınıfı her ikisini de uygulayabilir:

class Post implements WithUuid, WithSlug { /* … */ }

Peki ya her iki arayüzü de uygulayan bir nesne gerektiren bir işlev oluşturmak istersek?

function url($object): string { /* … */ }

Kesişim türlerinden önce, bu kullanım durumu için özel bir arayüz oluşturmanız ve url işlevimizi tür açısından güvenli bir şekilde kullanmak için bunu manuel olarak uygulamanız gerekir:

interface WithUrl extends WithUuid, WithSlug {}
class Post implements WithUrl { /* … */ }
function url(WithUrl $object): string { /* … */ }

Ancak kesişim türlerinde, aracı WithUrl arayüzüne ihtiyacımız yoktur artık!

class Post implements WithUuid, WithSlug { /* … */ }
function url(WithUuid&WithSlug $object): string { /* … */ }

 

Ayrık normal form türleri:

Union ve intersection tipleri dile pek çok fayda sağlamıştır. Ayrık normal form türlerinin (DNF türleri) tanıtılmasıyla, daha da karmaşık türler oluşturabilirsiniz. DNF türleri PHP 8.2'de eklendi ve kesişim ve tekli türleri birlik yapmanıza izin verdi. Bu, kodunuza daha da ayrıntılı tip denetimi eklemenize olanak tanır. Daha önce verdiğimiz örneğe bir göz atalım. Ya şöyle bir arayüzümüz olsaydı:

interface WithUrlSegments
{
 public function getUrlSegments(): array;
}

Bir Page sınıfı artık bu sınıfı uygulayabilir:

class Page implements WithUrlSegments
{
 //
}

Şimdi URL fonksiyonumuzun türünü aşağıdaki gibi genişletebiliriz:

function url((WithUuid&WithSlug)|WithUrlSegments $object): string
{
 //
}

İşlev artık hem WithUuid hem de WithSlug uygulayan nesnelere veya tek WithUrlSegments arayüzünü uygulayan nesnelere izin vermektedir

DNF türleri için en belirgin kullanım durumu muhtemelen kesişim türlerini geçersiz kılmak olacaktır:

function url((WithUuid&WithSlug)|null $object): ?string
{
 //
}

DNF türleri, DNF formunun kurallarına uymalıdır; bu, türleri yalnızca kesişimlerin ve tekli türlerin birleşimi olarak oluşturabileceğiniz anlamına gelir. Bu, bunun gibi bir türe izin verilmediği anlamına gelir:

interface A {}
interface B {}
interface C {}
function url((A|B)&C $object): string
{
 //
}

Bir kesişimler birliği şeklinde yeniden yazılabilir:

function url((C&A)|(C&B) $object): string {
 //
}

 

 

Diğer türler
En son PHP sürümlerinde birçok yeni yerleşik tür tanıtıldı. Hızlıca bunların üzerinden geçelim. PHP 8.0'da statik dönüş türü mevcuttur. Bir fonksiyonun, o fonksiyonun çağrıldığı sınıfı döndürdüğünü belirtir. Bu self'ten farklıdır çünkü one her zaman ana sınıfa atıfta bulunurken static aynı zamanda alt sınıfı da gösterebilir:

 


abstract class Parent
{
    public static function make(): static
    {
        //
    }
}
class Child extends Parent
{
    //
}
$child = Child::make();

 

Bu örnekte, statik analiz araçları ve IDE'ler artık $child'in Child'ın bir örneği olduğunu biliyor. Eğer self bir dönüş türü olarak kullanılsaydı, bunun Parent'in bir örneği olduğunu düşünürlerdi. Ayrıca statik anahtar kelime ile statik dönüş türünün iki farklı kavram olduğunu unutmayın; tesadüfen aynı ada sahipler (diğer birçok dilde durum böyle). Ayrıca PHP 7.1'de void dönüş türü de eklenmiştir. Bir fonksiyondan hiçbir şey döndürülmediğini kontrol eder. void'in bir birleşim halinde birleştirilemeyeceğini unutmayın. Daha sonra, PHP 8.0'dan itibaren mevcut olan karma tür var. karışık ipucu "herhangi bir şey" yazmak için kullanılabilir.

Bu

array|bool|callable|int|float|null|object|resource|string 

birleşiminin kısaltmasıdır. Karışık'ı boşluğun zıttı olarak düşünün. PHP 8.1'de never türü eklendi. Never ve void arasındaki fark, void'in işlevin hiçbir şey döndürmediğini, asla aslında hiçbir şey döndürmeyeceğini göstermesidir. Bu, bir işlev çıkışı çağırdığında veya her zaman bir istisna attığında meydana gelebilir. Statik analizörlerin daha detaylı analizler yapması için kullanışlı bir türdür. null ve false türleri PHP 8.2'nin yayınlanmasından önce yalnızca birleşim türü içinde kullanılabiliyordu. Bu türlerin özellik, parametre veya dönüş türü olarak kullanılması önemli bir hataya neden oldu. Bu PHP sürümünden başlayarak, bu türler bağımsız türler olarak kullanılabilir ve gerçek bir tür eklenmiştir. ?null, false|bool, true|bool, false|bool veya false|true türü oluşturmanın imkansız olduğunu unutmayın, bu bir hatayla sonuçlanacaktır. Muhtemelen yeni eklenen null, true veya false türlerini o kadar sık kullanmayacaksınız. Bunları esas olarak eksiksiz bir tip sistemi oluşturmak ve yerleşik yöntemlerdeki bazı tutarsızlıkları düzeltmek için ekledik. Bu yeni türleri kullanmanın ilginç bir örneği şu şekilde olabilir:


class User {
    function isAdmin(): bool{}
}
class Admin extends User
{
    function isAdmin(): true
    {
        return true;
    }
}
class Employee extends User
{
    function isAdmin(): false
    {
        return false;
    }
}

 

 

Enums:

Bunlardan bahsetmek için bu bölümün sonuna kadar bekledim ama bu onların önemini azaltmıyor: PHP 8.1 aslında enum türlerini destekliyor! Şuna benziyorlar:

enum Status: string {
 case Draft = 'Draft';
 case Published = 'Published';
 case Archived = 'Archived';
}

 

Generics:

Bugün PHP'nin tip sisteminde hala önemli bir özellik eksik: jenerikler ne yazık ki henüz dil tarafından desteklenmiyorlar. Dikkat edilmesi gereken önemli bir nokta, statik analizörlerin bunları desteklemesidir; dolayısıyla jenerikler birinci sınıf bir dil özelliği olmasa da, bunları bugün statik analiz için kullanabiliriz.

0 Yorum


Önerilen Yorumlar

Görüntülenecek yorum yok.

Misafir
Yorum ekle...

×   Zengin metin olarak yapıştırıldı.   Bunun yerine düz metin olarak yapıştır

  Yalnızca 75 emojiye izin verilir.

×   Bağlantınız otomatik olarak gömüldü.   Bunun yerine bağlantı olarak görüntüle

×   Önceki içeriğiniz geri yüklendi.   Düzenleyiciyi temizle

×   Görüntüleri doğrudan yapıştıramazsınız. URL'den resim yükleyin veya ekleyin.

×
×
  • Create New...