Tip güvenli koleksiyonlar için PHPStan generics mi kullanmalıyım, yoksa her tür için ayrı sınıf mı yazmalıyım?
Soru
Domain katmanımda her yerde tipsiz entity dizileri dolaşıyor — `function process(array $orders)` gibi imzalarla `Order[]` geçiyorum ama PHPStan içeride yanlış tip olduğunda bana hiçbir şey söylemiyor. Runtime generics olmadan compile-time (daha doğrusu analiz zamanı) güvenlik istiyorum. Her entity türü için `OrderCollection`, `InvoiceCollection` gibi elle sınıf yazmak mı doğru, yoksa PHPStan generics ile mi çözmeliyim? Boilerplate ile tip güvenliği arasında nerede durmalıyım?
Cevap
Kısa cevap: Çoğu durum için PHPStan generics (generic docblock’lar ve array şekilleri) kullanın; elle yazılmış concrete koleksiyon sınıfına yalnızca davranış ya da runtime garanti gerektiğinde geçin.
Kısa cevap
Sırf tip için her türe ayrı sınıf yazmak boşa boilerplate’tir. PHP’nin motorunda tip bildirimlerinin nereye kadar gittiğini skaler tip bildirimleri yazısında anlatmıştım; bir dizinin eleman tipi tam da o sınırın dışında kalır ve boşluğu analiz zamanında PHPStan doldurur.
Neden
-
PHP’de runtime generics yok — PHPStan generics tamamen statiktir.
@param list<Order> $orderssize sıfır runtime maliyetiyle analiz zamanında tip güvenliği verir. İstediğiniz tam olarak budur; interpreter hiçbir şey kontrol etmez, PHPStan CI’da yakalar. -
Sadece sınıflar değil, fonksiyonlar da generic olabilir. Tip taşıyan bir yardımcıya ihtiyacınız varsa (
first,mapgibi) sınıf yazmadan fonksiyonu@templateile generic yapabilirsiniz —@param list<T> $itemsalıp@return Tdöndüren bir fonksiyon, tipi çağıran tarafta korur. Bu, koleksiyon etrafındaki çoğu ihtiyacı sınıf açmadan karşılar. -
Bu güvenlik yalnızca PHPStan gerçekten çalıştığı sürece vardır. Annotation’ların runtime karşılığı olmadığı için analiz durduğunda kontrol de durur: annotation’lar çürür ve size sahte güven verir. Concrete alt sınıf ise runtime maliyeti ve boilerplate getirir; ikisinin arasındaki seçim tam olarak burada yapılır.
Ne yapmalı
-
Tipli dizilerle başlayın. Koleksiyonları öylece dolaştırmak için imzalarda
list<Order>veyaarray<int, Order>kullanın. Yeni sınıfa gerek yok; yanlış tip geçen çağrı PHPStan tarafında hemen kırmızıya döner. En ucuz ve en hızlı kazanç budur. -
Davranış istediğinizde tek bir generic Collection yazın.
->map(),->filter(), “boş değil” veya “benzersiz” gibi invariant’lar istiyorsanız@template’li TEK birCollection<T>sınıfı yazın — her tür için ayrı değil. Metotların dönüşü de tip taşıdığı için IDE ve PHPStan zinciri boyunca tipi korur. İskelet şöyle görünür:/** * @template T of object */ final class TypedCollection { /** @var list<T> */ private array $items = []; /** @param T $item */ public function add(object $item): void { $this->items[] = $item; } /** @return list<T> */ public function all(): array { return $this->items; } } /** @var TypedCollection<Order> $orders */ -
Concrete alt sınıfı yalnızca gerçek gerekçeyle açın.
OrderCollection extends Collectionyazmayı ya domain okunabilirliği için ya da veri type-checked sınırınızın dışından (JSON, DB, kullanıcı girdisi) geldiğindeadd()içindeinstanceofile runtime kontrol koymak için yapın. Gerekçesini net koyun. -
PHPStan’ı katı ve zorunlu koşun. Level 10/max koşun (
checkGenericClassInNonGenericObjectTypeayarı PHPStan 2.0’da kaldırıldı, artık açmanıza gerek yok) ve PHPStan’ı CI’da zorunlu tutun. Generics ancak bu koşulda işe yarar.
Sonuç: Ben olsam varsayılan olarak list<Order> docblock’ları + katı PHPStan ile giderdim; map/filter gibi davranış gerektiğinde tek bir generic Collection<T> eklerdim; concrete alt sınıfları yalnızca güven sınırında, dışarıdan gelen veriyi runtime’da doğrulamak gerektiğinde yazardım. Böylece boilerplate’i minimumda tutar, güvenliğin çoğunu neredeyse bedavaya alırsınız. Unutmayın: bu güvenlik yalnızca PHPStan gerçekten çalıştığı sürece vardır; onu CI’da zorunlu tutmazsanız hiçbir annotation sizi korumaz.
Yorumlar
Yorum yapmak için GitHub hesabınızla giriş yapmanız yeterli. Yorumlar GitHub Discussions üzerinde saklanır.