Nette PHPStan Kuralları
PHPStan kuralları PHPStan'e Nette kodunu anlamayı öğretir; böylece statik çözümleme kesin tipler çıkarır ve daha az yanlış pozitif bildirir.
Uzantıyı kurmanız yeter; PHPStan örneğin daha önce yalnızca bir hata gördüğü yerde bir bileşenin tipini tanıyacak:
class HomePresenter extends Presenter
{
protected function createComponentMenu(): MenuControl
{
return new MenuControl;
}
public function renderDefault(): void
{
$menu = $this['menu']; // PHPStan artık MenuControl çıkarıyor
$menu->setActive('home'); // bilinmeyen metot uyarısı yok
}
}
Kurulum
Bu uzantı, kodunuzdaki mantıksal hataları siz onu çalıştırmadan önce saptayan PHPStan statik çözümleyicisinin üzerine kurulur. Onu henüz kullanmıyorsanız Composer ile kurun:
composer require --dev phpstan/phpstan
Çözümlenecek dizinleri ve kural düzeyini belirten bir phpstan.neon yapılandırma dosyası oluşturun:
parameters:
paths:
- app
level: 8
PHPStan sonra şu komutla çalıştırılır:
vendor/bin/phpstan analyse
Kapsamlı belgeleri PHPStan web sitesinde bulabilirsiniz.
Sonra uzantının kendisini kurun:
composer require --dev nette/phpstan-rules
Gereksinimler: PHP 8.1 ya da daha yenisi ve PHPStan 2.2+.
PHPStan'in uzantıyı kullanabilmesi için onun etkinleştirilmesi gerekir. Ya bunu sizin için yapan phpstan/extension-installer paketini kurun ya da uzantıyı
phpstan.neon dosyanıza elle ekleyin:
includes:
- vendor/nette/phpstan-rules/extension.neon
Denetimlerin çoğu ek bir ayar olmadan çalışır. Yalnızca Assets bölümü
phpstan.neon içinde küçük bir yapılandırma bloğuna gereksinim duyar (aşağıda anlatılıyor). Bu sayfada
gösterilen tüm yapılandırmanın phpstan.neon dosyasına ait olduğuna, uygulamanızın common.neon
dosyasına ya da diğer Nette DI yapılandırma dosyalarına ait olmadığına dikkat edin.
Yerel PHP Fonksiyonları
Pek çok yerel PHP fonksiyonu, hata değeri yalnızca modern kodda pratikte gerçekleşemeyecek koşullarda ortaya çıksa da
string|false ya da array|null gibi bir dönüş tipi bildirir: aklı başında bir dosya sisteminde
getcwd() başarısız olması, JSON_THROW_ON_ERROR olmadan json_encode() başarısız
olması, derleme zamanı sabit bir kalıpla preg_split() başarısız olması vb. Uzantı bu dönüş tiplerinin
olanaksız parçalarını kaldırır, böylece PHPStan sizden gerçekleşemeyecek hataları ele almanızı istemeyi bırakır.
Tam liste extension-php.neon dosyasındadır.
Çalışma zamanı tip doğrulama closure'ları
Bir dizinin bildirilen tipte öğeler içerdiğini çalışma zamanında denetlemek için yaygın bir PHP deyimi, spread operatörüyle çağrılan, tipli değişken argümanlı bir closure kullanır:
/** @param string[] $items */
public function setItems(array $items): void
{
(function (string ...$items) {})(...$items);
}
PHP, spread edilen her argümanda string tipini zorunlu kılar ve herhangi bir öğe string değilse
TypeError fırlatır. Closure gövdesi boştur, ifade yalnızca yan etkisi için vardır. PHPStan normalde
expr.resultUnused bildirirdi; bu kural kalıbı tanır ve sessiz kalır.
Application
Presenter'larda redirect(), forward() ya da sendJson() gibi metotlar
Nette\Application\AbortException fırlatarak çalışmayı sonlandırır. Böyle bir çağrıyı bir try
içine sarar ve onu geniş bir catch (\Throwable) veya catch (\Exception) ile yakalarsanız,
yönlendirmeyi kazayla yutarsınız. Uzantı sizi buna karşı uyarır:
try {
$this->redirect('Homepage:');
} catch (\Throwable $e) { // hata: AbortException'ı yutar
Debugger::log($e);
}
Düzeltme, istisnayı yeniden fırlatmak ya da onu geniş catch'ten önce ayrı bir dala ayırmaktır:
try {
$this->redirect('Homepage:');
} catch (Nette\Application\AbortException $e) {
throw $e;
} catch (\Throwable $e) {
Debugger::log($e);
}
Assets
phpstan.neon dosyasında (Nette DI yapılandırmanızda değil), PHPStan'in genel Asset tipini somut
bir varlık sınıfına daraltabilmesi için mapper ID'lerinin mapper sınıflarına eşlemesini yapılandırın:
parameters:
nette:
assets:
mapping:
default: file # Nette\Assets\FilesystemMapper
images: file
vite: vite # Nette\Assets\ViteMapper
custom: App\MyMapper # herhangi bir FQCN
file ve vite değerleri, yerleşik FilesystemMapper ve ViteMapper
sınıfları için kısayollardır. Başka herhangi bir değer, özel bir mapper'ın tam nitelikli sınıf adı sayılır.
Yapılandırmadan sonra:
Registry::getMapper('vite')metoduMapperyerineViteMapperdöndürür.Registry::getAsset('default:logo.png')metoduImageAssetdöndürür.tryGetAsset()iseImageAsset|nulldöndürür.FilesystemMapper::getAsset('button.js')veViteMapper::getAsset()aynı şekilde daraltılır.
Bileşen Modeli
Container::getComponent() ve Container::offsetGet() (yani $this['name']) metotlarının
dönüş tipini, aynı sınıfta bildirilen createComponent<Name>() factory metotlarına dayanarak
daraltır.
class HomePresenter extends Presenter
{
protected function createComponentMenu(): MenuControl
{
return new MenuControl;
}
public function renderDefault(): void
{
$menu = $this->getComponent('menu'); // MenuControl
$menu = $this['menu']; // MenuControl
}
}
Eşleşen bir factory yoksa ya da bileşen adı derleme zamanı bir dize değilse, getComponent() ve
$this['name'] dönüş tipi değişmeden kalır, yani genel IComponent olur.
Dependency Injection
#[Nette\DI\Attributes\Inject] niteliğiyle işaretli özellikler, nesne oluşturulduktan sonra bağımlılık
enjeksiyonuyla doldurulur. PHPStan bu yüzden onları başlatılmamış olarak bildirirdi; uzantı ise onları yazılmış ve
başlatılmış sayar:
class HomePresenter extends Presenter
{
#[Inject]
public CartFacade $cart; // başlatılmamış özellik hatası yok
}
Formlar
$form->addText('name', …), $form->addSelect(…) ve benzerleri, $form['name']
(ya da $form->getComponent('name')) erişimiyle aynı fonksiyon veya metotta çağrıldığında, uzantı erişim
tipini karşılık gelen addXxx() çağrısından çıkarır:
public function createComponentSignInForm(): Form
{
$form = new Form;
$form->addText('username', 'Username');
$form->addPassword('password', 'Password');
$form['username']; // TextInput
$form['password']; // TextInput (Password bir alt sınıftır)
return $form;
}
Erişim, formun oluşturulduğu metottan başka bir metottan da çalışır. Onu createComponentSignInForm()
factory'sinde kurup denetimlerine başka yerde eriştiğinizde, uzantı atamayı factory'ye dek izler ve eşleşen
addXxx() çağrısını bulur:
public function renderDefault(): void
{
$form = $this['signInForm']; // createComponentSignInForm() çözülür
$form['username']; // TextInput
// doğrudan zincirlenmiş erişim de çalışır
$this['signInForm']['username']; // TextInput
$this['signInForm-username']; // TextInput
}
Eşleşen bir addXxx() çağrısı bulunamazsa, uzantı tıpkı Bileşen Modeli uzantısı gibi
createComponent<Name>() factory aramasına geri düşer.
Olay işleyici özellikleri
Formlar veriyi, callback'in parametresinde bildirilen tipe zorlar; bu ister stdClass, ister array,
ister özel bir DTO olsun. Dolayısıyla veri parametresi bildirilen array|object birleşiminden daha dar olan bir
callback, çalışma zamanında geçerlidir:
$form->onSuccess[] = function (Form $form, MyDto $data): void {
// …
};
PHPStan normalde, MyDto tipi array|object birleşiminden daha dar olduğu için
assign.propertyType bildirirdi. Kural bu hatayı Form::$onSuccess, $onError,
$onSubmit, $onRender, Container::$onValidate, SubmitButton::$onClick ve
$onInvalidClick üzerinde bastırır.
Schema
Expect::array() metodunun dönüş tipini, bildirilen Structure|Type birleşiminden argümana
dayanarak daraltır:
Expect::array(); // Type
Expect::array(['name' => Expect::string()]); // Structure (tüm değerler Schema)
Expect::array(['name' => Expect::string(), 'x']); // Structure|Type (karışık Schema ve Schema olmayan)
Argüman Schema ile Schema olmayan değerleri karıştırdığında, bildirilen birleşim korunur.
Tester
PHPStan, Tester\Assert çağrılarından sonra tip daraltmayı anlar. Desteklenen metotlar: null(),
notNull(), true(), false(), truthy(), falsey(),
same(), notSame(), type().
function process(?User $user): void
{
Assert::notNull($user);
$user->getName(); // "null üzerinde çağrıldı" uyarısı yok
}
Void callback olarak ok fonksiyonları
Tester'ın test() ve Assert::exception() metotları Closure(): void olarak tiplenmiş
callback'ler kabul eder, ama fn () => throw new MyException gibi ok fonksiyonları aktarmak yaygındır. Bir ok
fonksiyonunun her zaman bir dönüş değeri vardır; PHPStan bunu normalde tip uyuşmazlığı olarak işaretlerdi. Kural bu
hatayı şu fonksiyon ve metotlar için bastırır: test(), testException(), testNoError(),
Tester\Assert::exception(), Tester\Assert::throws(), Tester\Assert::error(),
Tester\Assert::noError().
Utils
Strings::match() ve matchAll(): sabit bir kalıp için dönüş tipi doğrudan düzenli
ifadeden, yani yakalama gruplarından (adlandırılmış ve isteğe bağlı olanlar dahil) çıkarılır.
captureOffset, unmatchedAsNull bayrakları ve matchAll() için ayrıca
patternOrder ile lazy bayrakları ortaya çıkan şekle yansır:
Strings::match($s, '#(\d+)-(\w+)#'); // array{non-falsy-string, decimal-int-string, non-empty-string}|null
Strings::match($s, '#(?<id>\d+)#'); // array{0: non-empty-string, id: decimal-int-string, 1: decimal-int-string}|null
Strings::matchAll($s, '#(\w+)#'); // list<array{string, non-empty-string}>
Sabit olmayan bir kalıp için (ve split() metodu için) şekil yalnızca bayraklardan çıkarılır.
Strings::replace(): değiştirme bir callback olduğunda, onun $matches parametresinin tipi
aynı düzenli ifadeden çıkarılır:
Strings::replace($s, '#(\d+)#', function (array $m) {
return $m[1]; // $m, array{non-empty-string, decimal-int-string} tipindedir
});
match() sonrası özne daraltma: if (Strings::match($s, …)) içinde aranan $s
dizesi de kalıba göre, örneğin non-empty-string tipine daraltılır.
Kalıp doğrulama: match(), matchAll(), split() ya da replace()
metoduna aktarılan geçersiz bir düzenli ifade, çalışma zamanında değil çözümleme sırasında bildirilir.
Arrays::invoke() ve Arrays::invokeMethod() bildirilen array yerine,
callable / metot dönüş tipinden oluşan bir dizi döndürür.
Helpers::falseToNull() dönüş tipini, false değerini kaldırıp null ekleyerek
daraltır. Böylece string|false tipi string|null olur.
Html sihirli metotları: $el->setClass(…), $el->addData(…),
$el->getHref() ve benzerleri @method açıklamaları olmadan çözülür. setXxx() ve
addXxx() metotları static (akıcı API), getXxx() ise mixed döndürür.