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') metodu Mapper yerine ViteMapper döndürür.
  • Registry::getAsset('default:logo.png') metodu ImageAsset döndürür. tryGetAsset() ise ImageAsset|null döndürür.
  • FilesystemMapper::getAsset('button.js') ve ViteMapper::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.