24 β Static analysis va avtomatik sifat¶
β¬ οΈ Oldingi: 23 β Pest, integratsiya, coverage va mutation Β· π README Β· Keyingi: 25 β Hexagonal va Clean arxitektura β‘οΈ
Bu bobda: test "kod ishlaydimi?" degan savolga javob beradi β lekin u faqat siz yozgan stsenariy bo'yicha kodni ishga tushiradi. Siz unutgan yo'l,
nullkelib qoladigan tarmoq, noto'g'ri tur uzatilgan chaqiriq β bularning hech biri test yozmasangiz tutilmaydi. Static analysis boshqacha ishlaydi: u kodni umuman ishga tushirmasdan, har bir o'zgaruvchining turini va har bir yo'lni tahlil qilib, runtime'gacha bug topadi. Bu bobda PHPStan ni 0..max darajalar bo'yicha o'rganamiz, baseline bilan eski loyihaga bosqichma-bosqich kiritamiz,neonconfig'ni tushunamiz; Psalm ni qisqa solishtiramiz; generics ni@template/@param T/@return TPHPDoc orqali yozib, PHPStan buni qanday tekshirishini real run bilan ko'rsatamiz (tip-xavfsizCollection). So'ng Rector bilan avtomatik refaktoring va PHP versiya migratsiyasi (eskiprivate $x+ qo'l bilan tayinlash → constructor promotion), PHP-CS-Fixer bilan PSR-12 stilini avtomatik tuzatish, Composer scripts bilan lint+stan+test+cs-fix ni bittacomposer checkga ulash va nihoyat CI darvozasi β bu vositalarni pull-request darvozasiga ulab, xato bo'lsa merge'ni bloklash. Hamma vosita bu mashinadacomposerbilan o'rnatilib haqiqatan ishga tushirildi β chiqishlar ko'chirib qo'yilgan, jumladan ataylab kiritilgan xatolar ham.
Nega static analysis? Test yetarli emasligi¶
23-bobgacha biz testga ko'p kuch sarfladik: PHPUnit, Pest, integratsiya, coverage va mutation. Test kuchli β lekin uning bitta tug'ma cheklovi bor: test kodni faqat siz ataylab chaqirgan holatda ishga tushiradi. Agar siz null keladigan holatga test yozmagan bo'lsangiz, o'sha yo'ldagi bug abadiy yashirin qoladi.
Mana klassik misol. ?string (ya'ni "string yoki null") qabul qiladigan metod, lekin ichida null ni hisobga olmaslik:
<?php
declare(strict_types=1);
function domain(?string $email): string
{
// β $email null bo'lishi mumkin - strpos(null, ...) PHP 8.4 da TypeError
$pos = strpos($email, '@');
return substr($email, $pos + 1);
}
echo domain('oqil@example.com'), "\n"; // ishlaydi: example.com
echo domain(null), "\n"; // PORTLAYDI: TypeError
Bu kod domain('oqil@example.com') bilan bemalol ishlaydi β agar siz testda faqat shu holatni yozsangiz, yashil ko'rasiz. Lekin domain(null) chaqirilgan zahoti PHP 8.4 strpos(): Argument #1 ($haystack) must be of type string, null given deb portlaydi. Test buni faqat siz null holatiga test yozsangiz tutadi. Static analysis esa kodga qarab'oq, har bir tarmoqni tip jihatidan tekshirib, hech qanday test yozmasdan aytadi: "bu yerga null kelishi mumkin, lekin strpos string kutadi".
Farqni shunday tushuning:
- Test β kodni ishga tushiradi va natijani solishtiradi. "Bu kirish → bu chiqish" degan misollarni tekshiradi. Yo'q test = yo'q kafolat.
- Static analysis β kodni ishga tushirmaydi. Manba matnini o'qib, har bir o'zgaruvchining mumkin bo'lgan turlar to'plamini hisoblaydi (type inference), har bir yo'lni (
if/else/match) kuzatadi (flow analysis) va tip qoidasi buzilgan joyni belgilaydi.
Ular raqobatchi emas, sherik: test "men o'ylagan holat to'g'ri ishlaydimi?" ni, static analysis "men unutgan holatda tip buzilmaydimi?" ni tekshiradi. Professional loyihada ikkalasi ham CI darvozasida turadi.
Static analysis ishga tushirmasdan ushlaydigan tipik xatolar:
- Null xavfsizligi β
?Tturdagi qiymatninull-ni kutmaydigan funksiyaga uzatish (yuqoridagi misol). - Noto'g'ri argument turi β
intkutilgan joygastringuzatish (declare(strict_types=1)runtime'da tutadi, lekin static analysis kodni ishga tushirmasdan, har bir chaqiriqni tekshiradi). - Mavjud bo'lmagan metod/xususiyat β
$user->emial(typo) yoki olib tashlangan metodni chaqirish. - Yetib bo'lmaydigan (dead) kod β
returndan keyingi qator, hech qachontruebo'lmaydiganif. - Noto'g'ri qaytuvchi tur β
stringqaytarishi e'lon qilingan, lekin ba'zi yo'lda hech narsa qaytarmaydigan funksiya.
Boshlovchidan ko'prik. Bu bob toza kod prinsiplari va testing ning tabiiy davomi. U yerda "kodni qatlamga ajrat, mas'uliyatni bo'l" deyildi; bu yerda shu intizomni mashina majburlaydi.
PHPStan: o'rnatish va birinchi ishga tushirish¶
PHPStan β PHP uchun eng keng tarqalgan static analyzer. Composer bilan dev-bog'liqlik sifatida o'rnatiladi (real loyihaga tegmaydi, faqat ishlab chiqishda kerak):
Bu mashinada o'rnatildi β versiya:
Endi tahlil qiladigan kod kerak. src/User.php ni yarataylik β boshida toza ko'rinadi, lekin ichida null xatosi bor:
<?php
declare(strict_types=1);
namespace App;
final class User
{
public function __construct(
public readonly string $name,
public readonly ?string $email = null,
) {}
public function emailDomain(): string
{
// β $this->email ?string - null bo'lsa strpos va substr xato beradi
$pos = strpos($this->email, '@');
return substr($this->email, $pos + 1);
}
}
Tahlilni ishga tushirish β analyse (yoki qisqa analyze) buyrug'i. --level (yoki -l) qat'iylik darajasini beradi:
Haqiqiy chiqish (level 0):
Diqqat: level 0 da xato topilmadi β null muammosi o'tib ketdi. Endi darajani 8 ga ko'taramiz:
Haqiqiy chiqish (level 8):
------ ------------------------------------------------------------------------------
Line User.php
------ ------------------------------------------------------------------------------
17 Parameter #1 $haystack of function strpos expects string, string|null given.
πͺͺ argument.type
19 Parameter #1 $string of function substr expects string, string|null given.
------ ------------------------------------------------------------------------------
[ERROR] Found 2 errors
Mana β kod umuman ishga tushirilmadi, lekin PHPStan ikkita aniq xatoni ko'rsatdi: strpos va substr ga string|null uzatilmoqda, ular esa string kutadi. Bu aynan biz boshda ko'rgan null portlashining ildizi. Buyruq exit code != 0 qaytaradi β CI uni "muvaffaqiyatsiz" deb tushunadi (buni quyida CI darvozasida ishlatamiz).
Xatoni tuzatish β null tekshiruvini qo'shish, shunda PHPStan null yo'qolganini type narrowing orqali ko'radi:
public function emailDomain(): string
{
if ($this->email === null) {
throw new \LogicException('email yoq');
}
// bu yerdan keyin PHPStan $this->email ni string deb biladi (null narrow qilindi)
$pos = strpos($this->email, '@');
return $pos === false ? '' : substr($this->email, $pos + 1);
}
$this->email === null shartidan keyin PHPStan $this->email endi sof string ekanini biladi β bu flow-sensitive tahlil. Endi level 8 da ham [OK] No errors.
Levellar: 0 dan max gacha pog'ona¶
PHPStan ataylab bosqichli: 0 dan 9 (va max β eng yuqori barqaror darajaga taxallus) gacha. Har daraja oldingisining ustiga yangi tekshiruv qo'shadi. G'oya β eski loyiha ham 0 dan boshlab asta-sekin ko'tariladi, hammasini bir kunda emas.
| Level | Nimani qo'shadi (taxminan) |
|---|---|
| 0 | Asosiy: mavjud bo'lmagan class/metod/funksiya, noto'g'ri argument soni |
| 1 | Aniqlanmagan o'zgaruvchilar, $this mavjud bo'lmagan kontekst |
| 2 | Noma'lum metodlar (har joyda, nafaqat aniq turlarda) |
| 3 | Qaytuvchi turlar, xususiyat (property) tayinlashlari |
| 4 | Yetib bo'lmaydigan kod (dead code), o'lik if shoxlari |
| 5 | Argument turlari (uzatilgan tur e'lon bilan mosligini tekshiradi) |
| 6 | Yetishmayotgan tiplar β har bir array/parametr tipga ega bo'lsin (PHPDoc talab) |
| 7 | Union turlarning qisman noto'g'ri ishlatilishi |
| 8 | Nullable β null bo'lishi mumkin qiymatni null-siz ishlatish (bizning misol) |
| 9 | mixed ni qat'iy: mixed ustida hech qanday amal bemalol o'tmaydi |
Amaliy maslahat. Yangi loyihada darrov
max(yokilevel: 9) dan boshlang β kod kam, tuzatish arzon. Eski (legacy) loyihada esa o'rnatilgan darajadan (masalan 5) boshlang, baseline oling (quyida) va vaqt o'tib darajani bittadan ko'taring. Levelni--level=8flagi yoki config faylda belgilash mumkin β config afzal, chunki butun jamoa va CI bir xil darajani ishlatadi.
neon config fayli¶
Har safar --level=8 --paths=src ... yozish charchatadi va jamoada xilma-xil bo'ladi. PHPStan sozlamalarni phpstan.neon (yoki phpstan.neon.dist) faylidan oladi. NEON β YAML'ga juda o'xshash, lekin PHP ekotizimida (Nette) ishlatiladigan format; tab yoki probel bilan otstup (chekinish) qabul qiladi:
parameters:
level: 8
paths:
- src
excludePaths:
- src/Generated
ignoreErrors:
- '#Access to an undefined property App\\Legacy#'
levelβ daraja (flag o'rnini bosadi).pathsβ qaysi kataloglarni tahlil qilish.excludePathsβ generatsiya qilingan/uchinchi-tomon kodni chetlab o'tish.ignoreErrorsβ aniq xato xabarlarini (regex bilan) jim qildirish. Lekin buni qo'l bilan emas, ko'pincha baseline orqali boshqarish afzal (quyida).
.dist qo'shimchasi konvensiya: phpstan.neon.dist git'ga kiradi (jamoa standarti), phpstan.neon esa lokal override sifatida (git'da e'tiborsiz). Config bo'lsa, buyruq oddiy bo'ladi:
PHPStan avtomatik phpstan.neon ni topadi va undagi level/paths ni ishlatadi.
Baseline: eski loyihani bosqichma-bosqich qutqarish¶
Eng katta amaliy muammo: siz 200 000 qatorli eski loyihada level: 8 ni yoqasiz va PHPStan 4000 ta xato chiqaradi. Hammasini bugun tuzatib bo'lmaydi β lekin yangi kod toza bo'lishini xohlaysiz. Yechim β baseline: mavjud xatolarni "ma'lum qarz" sifatida muzlatib qo'yish, shunda PHPStan ularni jim o'tadi, lekin yangi xato paydo bo'lsa darvoza qizaradi.
Baseline ni generatsiya qilish:
Bu mashinada src/User.php (ikki xatoli) uchun ishga tushirdik. Haqiqiy chiqish:
Hosil bo'lgan phpstan-baseline.neon (real chiqish):
parameters:
ignoreErrors:
-
message: '#^Parameter \#1 \$haystack of function strpos expects string, string\|null given\.$#'
identifier: argument.type
count: 1
path: src/User.php
-
message: '#^Parameter \#1 \$string of function substr expects string, string\|null given\.$#'
identifier: argument.type
count: 1
path: src/User.php
Endi config'ga baseline'ni includes orqali ulaymiz:
Va qayta ishga tushiramiz. Haqiqiy chiqish:
Eski ikki xato endi "ma'lum qarz" sifatida muzlatildi. Lekin muhimi: agar siz yangi kodda null xatosi qilsangiz, u baseline'da yo'q β PHPStan uni darrov qizil qiladi. Shunday qilib loyiha regresssiyaga qarshi himoyalanadi, eski qarzni esa "boy-scout rule" bilan asta-sekin (har tekkan faylda) kamaytirasiz va vaqti-vaqti bilan baseline'ni qayta generatsiya qilib siqasiz.
Muhim qoida. Baseline β vaqtinchalik vosita, abadiy yashash joyi emas. Uni faqat eski qarz uchun ishlating; yangi xatoni baseline'ga qo'shib "yashirish" β o'zingizni aldash. Baseline hajmi faqat kamayishi kerak.
Psalm: qisqacha solishtirish¶
Psalm (Vimeo'dan) β PHPStan'ga o'xshash, lekin tarixan biroz boshqacha urg'ulangan static analyzer. O'rnatish va ishga tushirish konseptual jihatdan bir xil:
composer require --dev vimeo/psalm
vendor/bin/psalm --init # psalm.xml yaratadi va boshlang'ich darajani topadi
vendor/bin/psalm
PHPStan level (0..max) ishlatsa, Psalm errorLevel (8..1, teskari β 1 eng qat'iy) ishlatadi. Psalm tarixan ikki narsa bilan ajralib turardi: kuchli @psalm-* annotatsiyalari (masalan @psalm-immutable, @psalm-pure, taint analysis β foydalanuvchi kiritmasi qayerga oqayotganini kuzatish, xavfsizlik uchun) va --alter bilan ba'zi avtomatik tuzatish. Bugun ikkala vosita ham ko'p jihatdan bir-biriga yetib oldi; tanlov ko'pincha jamoa odati va ekotizim (masalan ishlatayotgan framework qaysi biriga ko'proq qoida beradi) bilan belgilanadi.
Amaliy tavsiya. Bitta loyihada bittasini tanlang (ikkalasini birga yuritish ovora). Yangi PHP loyihasida PHPStan ko'pincha standart tanlovdir β kengroq plagin ekotizimi (
phpstan/phpstan-strict-rules, framework-maxsus plaginlar) tufayli. Bu bobning qolgan misollari PHPStan'da, lekin g'oyalar Psalm'ga ham bevosita o'tadi.
Generics PHPDoc orqali: @template, @param T, @return T¶
PHP sintaksisida hali "haqiqiy" generics yo'q β Collection<User> deb kodda yozolmaysiz. Lekin static analyzer generics'ni PHPDoc annotatsiyalari orqali tushunadi. Ya'ni PHP runtime'i @template ni e'tiborsiz qoldiradi (oddiy izoh), lekin PHPStan/Psalm uni o'qib, tip-xavfsizlikni majburlaydi. Bu β "tip-xavfsiz konteyner" muammosini sintaksisni o'zgartirmasdan hal qiladi.
Tushuncha vs mexanika. Generics nima, kovariantlik/kontravariantlik (variance) nega kerakligi β bularning chuqur nazariyasi foydalanuvchining TypeScript kitobi da (generics va variance) batafsil yoritilgan; PHP'da
readonly/Value Object kontekstida variance 06-bobda ko'rilgan. Bu yerda biz faqat PHP@templateMEXANIKASI va PHPStan buni qanday tekshirishi ga e'tibor beramiz β nazariyani takrorlamaymiz.
Asosiy uchta annotatsiya:
@template Tβ class (yoki funksiya) "tip-parametr"Tqabul qilishini e'lon qiladi.Tβ joy egasi (placeholder), aniq tur emas.@param T $xβ bu metod argumentiTturida bo'lishi kerak.@return Tβ bu metodTturini qaytaradi.
Tip-xavfsiz Collection ni quramiz. Kod PHP uchun oddiy β runtime hech narsa majburlamaydi; PHPStan uchun esa har bir add/first tip jihatidan bog'langan:
<?php
declare(strict_types=1);
namespace App;
/**
* @template T
*/
final class TypedCollection
{
/** @var list<T> */
private array $items = [];
/** @param T $item */
public function add(mixed $item): void
{
$this->items[] = $item;
}
/** @return T */
public function first(): mixed
{
return $this->items[0];
}
}
Endi uni User bilan parametrlaymiz. @var TypedCollection<User> PHPStan'ga "bu yerda T = User" deydi:
final class GenericsDemo
{
public function run(): void
{
/** @var TypedCollection<User> $users */
$users = new TypedCollection();
$users->add(new User('Oqil', 'oqil@example.com'));
// β User kutilgan joyga string berildi
$users->add('men string man');
$u = $users->first();
echo $u->name; // PHPStan: $u -> User deb biladi (T = User), shuning uchun ->name to'g'ri
}
}
vendor/bin/phpstan analyse src --level=8. Haqiqiy chiqish:
Nega butun
srcni tahlil qilamiz, bitta faylni emas? Agar faqatphpstan analyse src/TypedCollection.phpdesangiz, PHPStan faylni alohida (izolyatsiyada) tekshiradi vaApp\Userklassini topolmaydi β natijadaclass.notFoundshovqini chiqib,$u->namega ham noto'g'ri shikoyat bo'ladi.srcni butunligicha berib, PHPStan'gaUserni ko'rsatamiz; shunda u faqat haqiqiy tip-xatoni βadd('men string man')ni β ushlaydi.
------ ---------------------------------------------------------------------------------------------------
Line TypedCollection.php
------ ---------------------------------------------------------------------------------------------------
37 Parameter #1 $item of method App\TypedCollection<App\User>::add() expects App\User, string given.
πͺͺ argument.type
------ ---------------------------------------------------------------------------------------------------
[ERROR] Found 1 error
Mana eng muhim natija: PHPStan add() ning xato xabarida App\TypedCollection<App\User>::add() deb yozdi va "expects App\User, string given" dedi. Ya'ni u T ni User ga bog'lab, add('men string man') ni xato deb tutdi β runtime esa bu kodni bemalol ishga tushirardi (string ham massivga qo'shilaverardi). Bu aynan generics'ning qiymati: kompilyatsiya (tahlil) vaqtida konteyner ichidagi tur buzilishini ushlash.
Va e'tibor bering: $u = $users->first() dan keyin $u->name ga shikoyat yo'q β chunki PHPStan first() ning @return T annotatsiyasini T = User bilan yechib, $u ni User deb biladi, User'da esa name mavjud. Generics'siz first() mixed qaytarardi va ->name ga level 9'da shikoyat bo'lardi. Demak @template mexanikasi ikki tomonga ishlaydi: kirishda noto'g'ri turni rad etadi, chiqishda to'g'ri turni qaytaradi.
arrayham generic.list<T>,array<int, User>,array<string, Money>β bular ham PHPStan generics annotatsiyalari. Level 6 dan boshlab PHPStan "yalang'och"array(tipsiz) uchun shikoyat qiladi va sizdanarray<...>shaklini talab qiladi β bu kollektsiyalar ustida xatolarni ancha kamaytiradi.
Rector: avtomatik refaktoring va versiya migratsiyasi¶
PHPStan muammoni ko'rsatadi, lekin tuzatishni sizga qoldiradi. Rector esa avtomatik tuzatadi β u kodni AST (abstrakt sintaksis daraxti) darajasida o'zgartirib, butun loyihadagi yuzlab faylni bir buyruq bilan refaktoring qiladi. Ikki asosiy ishlatilishi bor:
- PHP versiya migratsiyasi β eski sintaksisni yangiga ko'chirish (7.x → 8.x):
array()→[], switch →match, qo'l bilan property tayinlash → constructor promotion, va h.k. - Code quality refaktoring β dead code olib tashlash, soddalashtirish, zamonaviy iboralar.
O'rnatish:
Bu mashinada o'rnatildi: rector/rector (2.4.5). Eski uslubdagi legacy/Order.php ni olaylik β private $x e'loni + konstruktorda qo'l bilan tayinlash, declare(strict_types) yo'q:
<?php
namespace Legacy;
class Order
{
private $status;
private $total;
public function __construct($status, $total)
{
$this->status = $status;
$this->total = $total;
}
public function describe()
{
if ($this->status == 'new') {
$label = 'Yangi';
} elseif ($this->status == 'paid') {
$label = 'Tolangan';
} else {
$label = 'Nomalum';
}
return $label . ': ' . $this->total;
}
}
Konfiguratsiya β rector.php (PHP fayl, neon emas). withSets bilan rule-set (qoidalar to'plami) tanlanadi:
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/legacy'])
->withSets([
LevelSetList::UP_TO_PHP_84, // 8.4 gacha barcha migratsiya qoidalari
])
->withPreparedSets(
deadCode: true, // o'lik kod
codeQuality: true, // soddalashtirishlar
);
UP_TO_PHP_84 β "PHP 8.4 gacha barcha versiya yangilanishlari" rule-set. Avval --dry-run bilan ishga tushiramiz β bu hech narsani o'zgartirmaydi, faqat nima o'zgarishini diff sifatida ko'rsatadi (CI'da xuddi shu rejim ishlatiladi):
Haqiqiy chiqish:
1) legacy/Order.php:1
---------- begin diff ----------
@@ Line 1 @@
<?php
+declare(strict_types=1);
+
namespace Legacy;
class Order
{
- private $status;
- private $total;
-
- public function __construct($status, $total)
+ public function __construct(private $status, private $total)
{
- $this->status = $status;
- $this->total = $total;
}
public function describe()
----------- end diff -----------
Applied rules:
* ClassPropertyAssignToConstructorPromotionRector
* SafeDeclareStrictTypesRector
[OK] 1 file would have been changed (dry-run) by Rector
Rector ikki qoidani qo'lladi: SafeDeclareStrictTypesRector (declare(strict_types=1) qo'shdi) va ClassPropertyAssignToConstructorPromotionRector (alohida property e'lonlari + qo'l bilan tayinlashni constructor promotion ga aylantirdi). Tasavvur qiling β 500 ta klass bor loyihada bu o'zgarishni qo'lda qilish kunlar oladi; Rector buni soniyalarda, xatosiz qiladi.
Diff ma'qul bo'lsa, --dry-run ni olib tashlab haqiqatan qo'llaysiz:
Rector ish jarayoni. Doim avval
--dry-runda ko'ring, keyin git'da toza holatda (commit'siz o'zgarish yo'q) qo'llang va darrov testlarni ishga tushiring. Rector ishonchli, lekin u sizning kodingizni o'zgartiryapti βgit diffni ko'zdan kechiring. Katta migratsiyani bir set bo'yicha (masalan avvalUP_TO_PHP_81, keyin82...) bosqichma-bosqich qiling, har bosqichdan keyin test.
PHP-CS-Fixer: PSR-12 kod stilini avtomatlashtirish¶
PHPStan mantiqiy xatoni, Rector sintaktik modernizatsiyani qiladi. Kod stili (probel, qavs joyi, chekinish) esa uchinchi muammo β u xatolik emas, lekin jamoada bir xil bo'lishi kerak. Buni qo'lda baholash ("sen yangi qatorga qavs qo'ymapsan") β vaqt isrofi va janjal manbai. PHP-CS-Fixer PSR-12 (va boshqa) standartni avtomatik majburlaydi.
PSR ko'prigi. PSR-1/PSR-12 kod stili standartlari 10-bobda yoritilgan β PHP-FIG ning "umumiy tili". Bu yerda biz shu standartni avtomatik tuzatadigan vositani ko'ramiz, standartni qayta o'rgatmaymiz.
phpcs/phpcbf(PHP_CodeSniffer) ham xuddi shu vazifani bajaradi βphpcstekshiradi,phpcbftuzatadi; tanlov odat masalasi.
O'rnatish:
O'rnatilgan versiya: friendsofphp/php-cs-fixer (v3.95.5). Konfiguratsiya β .php-cs-fixer.dist.php:
<?php
declare(strict_types=1);
$finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/src');
return (new PhpCsFixer\Config())
->setRiskyAllowed(true)
->setRules([
'@PSR12' => true, // PSR-12 to'plami
'array_syntax' => ['syntax' => 'short'], // array() -> []
'no_unused_imports' => true, // ishlatilmagan use o'chsin
'ordered_imports' => true, // use'lar alifbo tartibida
])
->setFinder($finder);
Stili buzilgan style/messy.php (ortiqcha probel, noto'g'ri qavs joyi, yopishgan operatorlar):
<?php
namespace Style;
class Calculator {
public function add($a,$b){
return $a+$b ;
}
public function sub( $a, $b )
{
return $a - $b;
}
}
CI'da har doim --dry-run --diff ishlatiladi β fayllarni o'zgartirmaydi, faqat nima tuzatilishini ko'rsatadi va stil noto'g'ri bo'lsa exit code != 0 qaytaradi:
Haqiqiy chiqish (diff):
1) style\messy.php
--- style/messy.php
+++ style/messy.php
@@ -1,11 +1,15 @@
<?php
+
namespace Style;
-class Calculator {
- public function add($a,$b){
- return $a+$b ;
+
+class Calculator
+{
+ public function add($a, $b)
+ {
+ return $a + $b ;
}
- public function sub( $a, $b )
- {
+ public function sub($a, $b)
+ {
return $a - $b;
- }
+ }
}
Found 1 of 1 files that can be fixed
PHP-CS-Fixer PSR-12 bo'yicha tuzatishlarni topdi: <?php dan keyin bo'sh qator, class nomidan ortiqcha probelni olib tashlash, ochuvchi qavsni yangi qatorga (metod uchun), argumentlar orasiga probel, chekinishni 4 probelga keltirish. --dry-run ni olib tashlasangiz β bularni haqiqatan tuzatadi:
Muhit o'zgaruvchisi. PHP-CS-Fixer ba'zan o'rnatilgan PHP versiyasidan shikoyat qilishi mumkin (
PHP needs to be a minimum...yoki tajriba ogohlantirishi). BuniPHP_CS_FIXER_IGNORE_ENV=1muhit o'zgaruvchisi bilan o'tkazib yuborish mumkin β biz bu bobdagi runlarda aynan shuni qo'lladik.
Composer scripts: hammasini bitta buyruqqa ulash¶
Bizda endi to'rtta vosita bor, har biri o'z buyrug'i bilan. Dasturchi har commit'dan oldin to'rttasini eslab, qo'lda yozishi kerakmi? Yo'q β Composer scripts ularni nomli buyruqlarga ulaydi. composer.json ga scripts bo'limini qo'shamiz:
{
"name": "demo/ch24",
"require-dev": {
"phpstan/phpstan": "^2.2",
"rector/rector": "^2.4",
"friendsofphp/php-cs-fixer": "^3.95"
},
"scripts": {
"stan": "phpstan analyse",
"cs": "php-cs-fixer fix --dry-run --diff",
"cs-fix": "php-cs-fixer fix",
"rector": "rector process --dry-run",
"check": [
"@cs",
"@stan"
]
}
}
Diqqat qiling: check β massiv, va ichidagi @cs/@stan boshqa scriptlarga havola (@ prefiksi). Composer ularni ketma-ket bajaradi va birortasi yiqilsa to'xtaydi (fail-fast). Real loyihada check ichiga @stan, test (phpunit), va xohlasangiz rector --dry-run ham qo'shiladi. Endi dasturchi faqat bitta narsani biladi:
Buni bu mashinada ishga tushirdik (style/messy.php stili buzuq edi). Haqiqiy chiqish:
1) style\messy.php
--- style/messy.php
+++ style/messy.php
@@ -1,11 +1,15 @@
<?php
+
namespace Style;
-class Calculator {
...
Found 1 of 1 files that can be fixed
Va eng muhimi β exit kod: composer check natijada exit code = 8 (noldan farqli) qaytardi, chunki birinchi qadam (@cs) stil buzilganini topdi va to'xtadi. @stan umuman ishga tushmadi β fail-fast: arzon tekshiruv (stil) yiqilgach, qimmatroq tahlilgacha bormaydi. Mana shu noldan-farqli exit kod CI darvozasining "qizil" signaliga aylanadi.
Lokalda bir xil, CI'da bir xil.
composer checkning butun qiymati β dasturchi lokalda ham, CI ham AYNAN shu buyruqni ishlatadi. Shuning uchun "menda ishlayapti, lekin CI yiqildi" muammosi yo'qoladi: ikkalasi bir xil vositani, bir xil config bilan chaqiradi. Pre-commit hook (masalanhuskyyokicaptainhook) bilancomposer checkni har commit'dan oldin avtomatik chaqirib qo'yish mumkin.
CI darvozasi: PHP sifat-pipeline mazmuni¶
Oxirgi bo'g'in β bu vositalarni pull-request darvozasiga ulash. G'oya oddiy: har PR uchun CI bu vositalarni ishga tushiradi; birortasi yiqilsa (exit != 0), merge tugmasi bloklanadi (GitHub'da "branch protection" + "required status checks"). Shunday qilib sifat past kod mainga umuman tusha olmaydi.
Mexanika vs mazmun. GitHub Actions ning MEXANIKASI β
on/jobs/steps/matrixsintaksisi, runner'lar, secret'lar, artifact'lar β bularning hammasi foydalanuvchining Git va GitHub kitobi (CI/CD bo'limi) da chuqur yoritilgan. Bu yerda biz YAML mexanikasini takrorlamaymiz; faqat PHP-ga xos pipeline MAZMUNIga β qaysi qadamlar, qaysi tartibda β e'tibor beramiz.
PHP sifat-pipeline'ining mazmuni (qadamlar tartibi):
composer installβ bog'liqliklarni o'rnatish (--prefer-dist --no-progress, CI'da odatda--no-devEMAS, chunki dev-vositalar kerak).php-cs-fixer fix --dry-runβ stil (eng arzon, oldinda yiqilsin).phpstan analyseβ static analysis (testdan oldin: tip xatosi ko'p bugni testsiz ham tutadi).phpunit/pestβ testlar (xulq-atvor isboti, ko'pincha--coveragebilan).infectionβ mutation testing (eng qimmat, oxirida; testlarning sifatini o'lchaydi).
Quyida PHP-ga xos jobs.steps mazmuni (YAML mexanikasi Git kitobida β bu yerda faqat PHP qadamlari ko'rinishi uchun):
# .github/workflows/ci.yml dan PHP-ga xos QADAMLAR (mexanika -> Git kitobi)
jobs:
quality:
strategy:
matrix:
php: ['8.3', '8.4'] # bir necha PHP versiyasida sinash
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php }}
coverage: pcov # coverage uchun pcov/xdebug
- run: composer install --prefer-dist --no-progress
- run: vendor/bin/php-cs-fixer fix --dry-run --diff # 1. stil
- run: vendor/bin/phpstan analyse # 2. tip
- run: vendor/bin/phpunit --coverage-text # 3. test + coverage
- run: vendor/bin/infection --min-msi=80 # 4. mutation
Matritsa (matrix) β PHP'ga xos muhim nuqta: kutubxona yoki ilova bir necha PHP versiyasida (masalan 8.3 va 8.4) ishlashi kerak. matrix.php har versiya uchun alohida pipeline ishga tushiradi β shunda "8.4'da ishlaydi, 8.3'da sinmaydi" degan kafolat olasiz. Bu ayniqsa kutubxona mualliflari uchun zarur: foydalanuvchilar turli PHP versiyalarida.
Coverage/mutation va muhit.
phpunit --coveragevainfectionXdebug yoki pcov kerak qiladi (kodni qatorma-qator kuzatish uchun). Ushbu yozuv tayyorlangan mashinada bu kengaytmalar yo'q β shuning uchun yuqoridagi--coveragevainfectionqadamlari ko'rsatuv uchun (config + kutilgan CI chiqishi). Lekinphp-cs-fixer,phpstanva oddiyphpunit/pesttestlari Xdebug'siz to'liq ishlaydi β bu bobdagi barcha PHPStan/Rector/PHP-CS-Fixer chiqishlari haqiqiy run. Coverage/mutation pipeline'ini lokal Xdebug bilan yoki CI'da ishlatasiz (CI'dacoverage: pcovnisetup-phpo'rnatadi). Mutation va coverage tafsilotlari 23-bobda.
Darvoza qanday "bloklaydi"? Har run: qadam yiqilsa (exit != 0), Actions o'sha jobni "failed" deb belgilaydi. GitHub repo sozlamasida quality job'ni required status check qilib qo'ysangiz, merge tugmasi o'chadi β kod tuzatilmaguncha mainga tusha olmaydi. Bu β "sifat darvozasi" ning aniq ma'nosi.
Hammasini birga: minimal sifat to'plami¶
Yangi PHP loyihasi uchun amaliy boshlang'ich to'plam, hammasi shu bobda ko'rilgan:
{
"require-dev": {
"phpstan/phpstan": "^2.2",
"rector/rector": "^2.4",
"friendsofphp/php-cs-fixer": "^3.95",
"phpunit/phpunit": "^11.0"
},
"scripts": {
"cs": "php-cs-fixer fix --dry-run --diff",
"cs-fix": "php-cs-fixer fix",
"stan": "phpstan analyse",
"rector": "rector process --dry-run",
"test": "phpunit",
"check": ["@cs", "@stan", "@test"]
}
}
Dasturchi ish jarayoni:
- Kod yozadi.
composer cs-fixβ stilni avtomatik tuzatadi (qo'l aralashmaydi).composer checkβ stil + tip + test, lokalda. Yashilmi β push.- CI xuddi shu
composer checkni (plus coverage/mutation) bir necha PHP versiyasida ishga tushiradi. - Hammasi yashil → merge ruxsat; biror qizil → merge bloklangan.
Bu β junior va senior chizig'i: junior "kod ishladi, push qildim" deydi; senior kod ishlashidan oldin mashina uni tekshirishini, va o'sha tekshiruv darvoza ekanini ta'minlaydi. Sifat β odamning intizomiga emas, avtomatlashtirishga tayanadi.
Mashqlar¶
Oson¶
- Level pog'onasi. Quyidagi funksiyani faylga saqlang va
phpstan analyse --level=0, keyin--level=8bilan ishga tushiring. Qaysi darajada xato chiqadi va nega?
<?php
declare(strict_types=1);
function uzunlik(?string $s): int
{
return strlen($s); // $s null bo'lsa-chi?
}
-
Composer script.
composer.jsongalintnomli script qo'shing, uphp-cs-fixer fix --dry-runni chaqirsin. Keyinstanham qo'shing va ikkalasiniverifynomli script massivida (["@lint", "@stan"]) ulang. -
neon config.
phpstan.neonfaylini yozing:level: 6, faqatapp/valib/kataloglarini tahlil qilsin,app/Generatedni chetlab o'tsin.
O'rta¶
-
Generics tuzatish. Yuqoridagi
TypedCollectiongamapmetodi qo'shing:@template U, kirishcallable(T): U, qaytishTypedCollection<U>. PHPStan'da xatosiz bo'lsin. (Maslahat: yangi kollektsiya yaratib, har elementni o'tkazib qo'shing.) -
Baseline. Ikkita tip xatosi bor faylni yozing (masalan ikkita joyda
?intniintkutilgan funksiyaga uzating).--generate-baselinebilan baseline yarating, config'ga ulang, qayta run qiling βNo errorsbo'lishini ko'ring. Endi uchinchi xato qo'shing va PHPStan faqat yangisini ko'rsatishini tasdiqlang. -
Rector dry-run. Eski sintaksisli fayl yozing:
array('a' => 1),if/elseifzanjiri va tipsiz konstruktor.rector.phpdaUP_TO_PHP_84set bilan--dry-runishga tushiring va diff'da qaysi qoidalar qo'llanishini sanang.
Qiyin¶
-
To'liq darvoza. Kichik loyiha qiling (
src/ichida 2-3 klass),composer checkscript'ini["@cs", "@stan", "@test"]qilib sozlang. Ataylab bitta klassda PHPStan xatosi qoldiring vacomposer checkningexit code != 0qaytarishini, hamda@stanyiqilgach@testishga tushmasligini tasdiqlang. Keyin tuzatib, yashil bo'lishini ko'ring. -
CI mazmuni. PHP kutubxona uchun (
8.3va8.4matritsasi) sifat-pipeline qadamlar tartibini yozing va har qadam nega aynan shu o'rinda turishini bir jumla bilan asoslang. (Mexanika emas β mazmun: nega cs oldinda, nega infection oxirida.)
Yechim β 1
Level 0 da [OK] No errors chiqadi β level 0 null-xavfsizlikni tekshirmaydi. Level 8 da xato chiqadi:
Chunki ?string null bo'lishi mumkin, strlen esa string kutadi. Tuzatish: if ($s === null) return 0; qo'shing, shunda level 8 ham toza bo'ladi.
Yechim β 2
{
"scripts": {
"lint": "php-cs-fixer fix --dry-run",
"stan": "phpstan analyse",
"verify": ["@lint", "@stan"]
}
}
composer verify ketma-ket lint keyin stan ni ishga tushiradi; lint yiqilsa stan ga bormaydi (fail-fast).
Yechim β 3
excludePaths ostidagi yo'l paths ichida bo'lsa ham tahlildan chiqariladi β generatsiya qilingan kod uchun zarur.
Yechim β 4
/**
* @template U
* @param callable(T): U $fn
* @return TypedCollection<U>
*/
public function map(callable $fn): self
{
$natija = new self(); // @var TypedCollection<U>
foreach ($this->items as $item) {
$natija->add($fn($item));
}
return $natija;
}
PHPStan $fn($item) chaqiruvida $item ni T, natijani U deb biladi; add($fn($item)) U ni yangi TypedCollection<U> ga qo'shadi. Foydalanishda: $nomlar = $users->map(fn(User $u): string => $u->name); → PHPStan $nomlar ni TypedCollection<string> deb biladi. self qaytarish tipini PHPStan @return TypedCollection<U> annotatsiyasi bilan aniqlashtiradi.
Yechim β 5
<?php
declare(strict_types=1);
function ikki(?int $a): int { return $a + 1; } // β 1
function uch(?int $b): int { return $b * 2; } // β 2
vendor/bin/phpstan analyse --level=8 --generate-baseline → "Baseline generated with 2 errors". Config'ga includes: [phpstan-baseline.neon] qo'shilsa, run No errors beradi. Endi uchinchi xatoni qo'shsangiz:
PHPStan faqat uchinchi xatoni ko'rsatadi (birinchi ikkitasi baseline'da muzlatilgan) β bu aynan regressiyaga qarshi himoya: eski qarz jim, yangi xato darrov ushlanadi.
Yechim β 6
rector.php:
<?php
declare(strict_types=1);
use Rector\Config\RectorConfig;
use Rector\Set\ValueObject\LevelSetList;
return RectorConfig::configure()
->withPaths([__DIR__ . '/legacy'])
->withSets([LevelSetList::UP_TO_PHP_84])
->withPreparedSets(codeQuality: true);
vendor/bin/rector process --dry-run qo'llaydigan qoidalar (taxminan): LongArrayToShortArrayRector (array() → []), ClassPropertyAssignToConstructorPromotionRector (tipsiz konstruktor → promotion), va if/elseif ni match/early return ga aylantiradigan code-quality qoidalari. Diff'da "Applied rules:" ro'yxati aniq qoidalarni sanaydi. Doim --dry-run da ko'rib, keyin haqiqatan qo'llang va test qiling.
Yechim β 7
{
"scripts": {
"cs": "php-cs-fixer fix --dry-run --diff",
"stan": "phpstan analyse",
"test": "phpunit",
"check": ["@cs", "@stan", "@test"]
}
}
Bitta klassda ataylab PHPStan xatosi (?string ni null-siz ishlatish) qoldiring. composer check:
- @cs o'tadi (stil toza),
- @stan yiqiladi (exit != 0) → Composer to'xtaydi,
- @test umuman ishga tushmaydi (chiqishda PHPUnit chiqishi yo'qligidan bilinadi).
composer check ning umumiy exit kodi noldan farqli → CI darvozasi qizil. Xatoni tuzatgach (null tekshiruvi), uch qadam ham yashil → exit 0 → merge ruxsat. Bu fail-fast tartibni isbotlaydi: arzon tekshiruv (cs) oldinda, qimmat (test) keyinda, biror qadam yiqilsa keyingilari behuda ishlamaydi.
Yechim β 8
Qadamlar tartibi va asoslari:
composer installβ boshqa hamma qadam bog'liqliklarsiz ishlamaydi.php-cs-fixer --dry-runβ eng arzon (soniyalar), shuning uchun oldinda: stil buzuq bo'lsa, qimmat qadamlargacha yetmasdan tez yiqilsin.phpstan analyseβ testdan oldin, chunki tip xatosi (null, noto'g'ri tur) ko'p bugni test yozmasdan tutadi va tez ishlaydi; testdan oldin yiqitsa, test resursini tejaydi.phpunit/pestβ xulq-atvorni isbotlaydi; static analiz tutolmaydigan mantiqiy xatolarni (noto'g'ri hisob-kitob) bu tutadi.infectionβ eng qimmat (har mutatsiya uchun test to'plamini qayta ishga tushiradi), shuning uchun oxirida; u testlarning o'zini sinaydi (MSI), ya'ni faqat testlar yashil bo'lgachgina ma'noli.
Matritsa (8.3, 8.4) β kutubxona ikkala versiyada ham sinishi shart; har versiya alohida pipeline. Umumiy tamoyil: arzon va keng tekshiruv oldinda, qimmat va chuqur tekshiruv keyinda β fail-fast bilan resurs va vaqt tejaladi.
Xulosa va keyingisi¶
Bu bobda kodni ishga tushirmasdan sifatini ta'minlaydigan to'plamni qurib chiqdik:
- Static analysis test bilan raqobat emas, sherik: test "men o'ylagan holat to'g'rimi?", analiz "men unutgan null/tur holatida sinmaydimi?".
- PHPStan β level 0..max pog'onasi,
neonconfig, va baseline bilan eski loyihani regressiyadan himoyalab, qarzni asta-sekin kamaytirish. Psalm β o'xshash muqobil. - Generics
@template/@param T/@return TPHPDoc orqali: PHP runtime e'tiborsiz qoldiradi, lekin PHPStan tip-xavfsizCollectionni majburlaydi (real run:add('string')→ xato). - Rector β avtomatik refaktoring va PHP versiya migratsiyasi (promotion,
strict_types, eski sintaksis); doim--dry-run→ test. - PHP-CS-Fixer β PSR-12 stilini avtomatik;
--dry-run --diffCI'da,fixlokalda. - Composer scripts
composer checkga ulab, lokal va CI bitta buyruqni ishlatadi. - CI darvozasi β bu vositalarni PR'ga ulab, qizil bo'lsa merge bloklanadi; matritsa bilan bir necha PHP versiyasida sinash.
Sifat to'plami (Wave 4) tugadi β testdan static analysis va avtomatik darvozagacha. Keyingisi: arxitektura, performance va async (Wave 5) β OPcache/JIT, Redis bilan keshlash, va asinxron PHP. Sifat darvozasi sizni "tez yozish" emas, xotirjam yozish ga olib chiqadi: mashina sizdan oldin tekshiradi, siz mantiqqa e'tibor berasiz.
β¬ οΈ Oldingi: 23 β Pest, integratsiya, coverage va mutation Β· π README Β· Keyingi: 25 β Hexagonal va Clean arxitektura β‘οΈ