Ժամանակակից PHP
PHP 8.x-ը տիպավորված է, արագ և արտահայտիչ՝ enum-ներ, readonly, match, attribute-ներ և Composer։
30 վայրկյանում
PHP-ն server-side լեզու է՝ ստեղծված վեբի համար. հարցում է գալիս, PHP script-ը կառուցում է պատասխանը, և դրանից հետո ամեն ինչ մաքրվում է։ Ժամանակակից PHP-ն (8.x) շատ է տարբերվում իր համբավից. այն ունի strict տիպեր, enum-ներ, անփոփոխ օբյեկտներ և just-in-time կոմպիլյատոր։ Composer-ով՝ փաթեթների համար, և PSR ստանդարտներով՝ ընդհանուր պայմանավորվածությունների համար, այն աշխատեցնում է Laravel-ի և Symfony-ի պես framework-ներ և վեբի զգալի մասը։
Ինչու եմ օգտագործում
- Իմ backend-ների մեծ մասը կառուցել եմ PHP-ով և Laravel-ով՝ 2020-ի իմ առաջին production համակարգից մինչև այսօրվա պատվերների, e-commerce և CRM հարթակները, որովհետև այն փոքր թիմին թույլ է տալիս արագ թողարկել մեծ ու հուսալի API։
- Ինձ դուր է գալիս PHP-ի share-nothing հարցումային մոդելը. յուրաքանչյուր հարցում սկսվում է մաքուր, ուստի մեկ վատ հարցումը չի կարող հիշողություն կամ վիճակ «արտահոսել» հաջորդի մեջ։
- Ժամանակակից հնարավորությունները՝ enum-ներ, readonly class-ներ և տիպավորված property-ներ, թույլ են տալիս գրել domain կոդ, որը նույնքան բացահայտ է, որքան TypeScript-ը։
- Hosting-ը հեշտ է և էժան. nginx-ը PHP-FPM-ի հետ աշխատում է ցանկացած Linux սերվերի վրա, ինչը համապատասխանում է այն նախագծերին, որոնք իմ հաճախորդներն իրականում ունեն։
Ինչպես է աշխատում
01Ինչպես է PHP-ն աշխատեցնում քո կոդը. PHP-FPM և հարցման կյանքի ցիկլը
Production-ում PHP-ի մեծ մասն աշխատում է PHP-FPM-ի (FastCGI Process Manager) տակ։ Պատկերացրու այն որպես աշխատողների թիմ, որոնք սպասում են առաջադրանքների։ nginx-ը ստանում է HTTP հարցումը, ստատիկ ֆայլերն ինքն է տալիս, իսկ PHP հարցումները FastCGI-ով փոխանցում է FPM-ին։ Master պրոցեսը կառավարում է worker պրոցեսների pool-ը. ազատ worker-ը վերցնում է հարցումը, աշխատեցնում է քո մուտքային script-ը (Laravel-ի համար՝ public/index.php), հետ է ուղարկում պատասխանը և հետո դեն է նետում հարցման բոլոր օբյեկտներն ու փոփոխականները։ Սա share-nothing մոդելն է. հարցումների միջև ոչինչ չի պահպանվում, բացի նրանից, ինչ պահում ես պրոցեսից դուրս (տվյալների բազա, Redis, ֆայլեր)։ OPcache-ը քո ֆայլերի կոմպիլյացված bytecode-ը պահում է shared memory-ում, որպեսզի PHP-ն ամեն հարցման ժամանակ նորից չparse անի սորս կոդը։ Pool-ի չափը (pm.max_children) սահմանափակում է, թե քանի հարցում կարող է միաժամանակ աշխատել, ուստի այն հարմարեցրու RAM-ին։ Երկարակյաց runtime-ները, ինչպես FrankenPHP worker mode-ը, RoadRunner-ը կամ Swoole-ը (Laravel Octane), հավելվածը պահում են բեռնված հիշողության մեջ՝ լրացուցիչ արագության համար, բայց այդ դեպքում պետք է խուսափես հարցումների միջև վիճակի արտահոսքից։
; /etc/php/8.4/fpm/pool.d/app.conf (example)
[app]
user = app_user
group = app_user
listen = /run/php/php8.4-fpm-app.sock
pm = dynamic
pm.max_children = 20 ; roughly free RAM / average worker size
pm.start_servers = 4
pm.min_spare_servers = 2
pm.max_spare_servers = 6
pm.max_requests = 500 ; recycle workers to contain slow leaks
; php.ini (production)
; opcache.enable=1
; opcache.validate_timestamps=0 ; reload FPM after each deploy
02Տիպեր և PHP 8-ի հնարավորությունները, որոնք պետք է օգտագործես
Յուրաքանչյուր ֆայլ սկսիր declare(strict_types=1);-ով, որպեսզի PHP-ն հրաժարվի "5"-ը լուռ 5-ի փոխակերպելուց, երբ ֆունկցիան int է սպասում։ Տիպավորիր ամեն ինչ՝ պարամետրեր, վերադարձվող արժեքներ և property-ներ, այդ թվում union տիպեր (int|string), nullable տիպեր (?string) և never այն ֆունկցիաների համար, որոնք միշտ exception են նետում։ PHP 8.0-ն բերեց match (խիստ, expression-ի վրա հիմնված switch, որը exception է նետում, եթե ոչինչ չի համընկնում), named argument-ներ, nullsafe օպերատոր ?-> և constructor property promotion։ PHP 8.1-ն ավելացրեց enum-ներ, readonly property-ներ և first-class callable-ներ, ինչպես strlen(...)։ PHP 8.2-ն ավելացրեց readonly class-ներ, 8.3-ը՝ տիպավորված class constant-ներ և #[\Override] attribute-ը, 8.4-ը՝ property hook-եր և ասիմետրիկ տեսանելիություն (public private(set)), իսկ 8.5-ը (2025-ի նոյեմբեր)՝ pipe օպերատոր |>, clone-with և #[\NoDiscard]։ PHP 8.6-ը նախատեսված է 2026-ի նոյեմբերի վերջին՝ partial function application-ով։
<?php
declare(strict_types=1);
final readonly class Money
{
public function __construct(
public int $amount, // minor units, e.g. cents
public string $currency,
) {}
public function add(Money $other): self
{
if ($other->currency !== $this->currency) {
throw new InvalidArgumentException('Currency mismatch');
}
return new self($this->amount + $other->amount, $this->currency);
}
}
03Enum-ներ, match և attribute-ներ
Enum-ները magic string-երը և class constant-ները փոխարինում են արժեքների փակ բազմությամբ։ Backed enum-ը (enum OrderStatus: string) յուրաքանչյուր case համապատասխանեցնում է արժեքի, որը կարող ես պահել տվյալների բազայում, իսկ հետ փոխակերպելու համար ունես OrderStatus::from('paid'), որը անվավեր մուտքի դեպքում exception է նետում, կամ tryFrom, որը վերադարձնում է null։ Enum-ները կարող են ունենալ մեթոդներ և իրականացնել interface-ներ, ուստի status-ի մասին կանոնները ապրում են հենց status-ի կողքին։ match-ը բնականորեն զուգակցվում է enum-ների հետ. այն համեմատում է խիստ, վերադարձնում է արժեք և նետում է UnhandledMatchError, եթե որևէ case բացակայում է, այնպես որ չմշակված նոր status-ը ձախողվում է բարձրաձայն, ոչ թե լուռ։ Attribute-ները (#[...]) կառուցվածքային մետատվյալներ են, որոնք կցվում են class-ներին, մեթոդներին կամ property-ներին և կարդացվում են reflection-ով։ Framework-ները դրանք օգտագործում են route-ների, վալիդացիայի, event listener-ների և dependency injection-ի համար՝ փոխարինելով փխրուն docblock annotation-ներին։
<?php
enum OrderStatus: string
{
case Pending = 'pending';
case Paid = 'paid';
case Cancelled = 'cancelled';
public function label(): string
{
return match ($this) {
self::Pending => 'Waiting for payment',
self::Paid => 'Paid',
self::Cancelled => 'Cancelled',
};
}
}
$status = OrderStatus::from($row['status']); // throws on invalid value
echo $status->label();
04Composer և autoloading
Composer-ը PHP-ի փաթեթների կառավարիչն ու autoloader-ն է։ composer.json-ը թվարկում է քո dependency-ները տարբերակների սահմանափակումներով (^12.0 նշանակում է «ցանկացած 12.x»), իսկ composer.lock-ը գրանցում է տեղադրված ճշգրիտ տարբերակները։ Commit արա երկու ֆայլն էլ, որպեսզի յուրաքանչյուր ծրագրավորող և յուրաքանչյուր սերվեր տեղադրի նույն կոդը։ Autoloading-ը նշանակում է, որ քո class-ների համար երբեք require չես գրում. PSR-4-ի համաձայն App\Billing\Invoice namespace-ը համապատասխանում է src/Billing/Invoice.php ֆայլին, և Composer-ը այն բեռնում է առաջին օգտագործման ժամանակ։ Production-ում տեղադրիր --no-dev-ով՝ թեստավորման գործիքները բաց թողնելու համար, և --optimize-autoloader-ով (կամ --classmap-authoritative-ով)՝ արագ class map կառուցելու համար։ Պարբերաբար կամ CI-ում աշխատեցրու composer audit՝ տեղադրված փաթեթները հայտնի անվտանգության advisory-ների դեմ ստուգելու համար, և թարմացրու composer update package/name-ով՝ ամեն ինչ միանգամից թարմացնելու փոխարեն։
{
"require": {
"php": "^8.3",
"laravel/framework": "^13.0"
},
"require-dev": {
"phpunit/phpunit": "^12.0"
},
"autoload": {
"psr-4": { "App\\": "src/" }
},
"config": { "sort-packages": true }
}
// Production install:
// composer install --no-dev --optimize-autoloader --no-interaction
05PSR ստանդարտներ և կոդի որակի գործիքներ
PSR-ները PHP Standards Recommendations են՝ PHP-FIG-ից, որը framework-ների հեղինակների խումբ է։ Դրանք թույլ են տալիս տարբեր հեղինակների գրադարաններին համատեղելի լինել։ Ամենահաճախ հանդիպողները՝ PSR-4 (autoloading), PSR-3 (logger interface), PSR-7 և PSR-17 (HTTP request և response օբյեկտներ), PSR-15 (middleware), PSR-11 (dependency-injection container-ներ), PSR-14 (event-ներ) և PSR-18 (HTTP client)։ Ֆորմատավորման համար PSR-12-ին փոխարինել է PER Coding Style-ը, որը գործիքները կիրառում են ավտոմատ։ Ժամանակակից PHP նախագիծը սովորաբար ավելացնում է ստատիկ անալիզ PHPStan-ով կամ Psalm-ով՝ CI-ում բարձր մակարդակով, որովհետև դրանք բռնում են տիպային սխալներ, որոնք PHP-ն ինքը հաղորդում է միայն runtime-ում։ Ավելացրու ավտոմատ formatter (PHP-CS-Fixer կամ Laravel Pint) և Rector՝ PHP-ի և framework-ի տարբերակների միջև ավտոմատ upgrade-ների համար։ Սրանցով PHP-ն ամենօրյա ապահովության առումով մոտ է կոմպիլյացվող լեզվին։
# Install quality tools as dev dependencies
composer require --dev phpstan/phpstan laravel/pint rector/rector
# Check types (raise the level over time)
vendor/bin/phpstan analyse src --level=8
# Format code to the project style
vendor/bin/pint
# Preview automated upgrades (e.g. to new PHP syntax)
vendor/bin/rector process src --dry-run
# Check dependencies for known vulnerabilities
composer audit
06Production խորհուրդներ
PHP-ն պահիր աջակցվող տարբերակի վրա. յուրաքանչյուր release ստանում է երկու տարի ակտիվ աջակցություն և ևս երկու տարի անվտանգության ուղղումներ, ուստի ստուգիր php.net/supported-versions-ը և պլանավորիր upgrade-ները՝ արտակարգ իրավիճակի չսպասելով։ Production-ում անջատիր display_errors-ը և սխալները log արա. օգտատերերին stack trace ցույց տալը բացահայտում է ճանապարհներ և գաղտնիքներ։ Միացրու OPcache-ը, և քանի որ validate_timestamps=0 նշանակում է, որ PHP-ն չի նկատի նոր ֆայլերը, յուրաքանչյուր deploy-ի ժամանակ reload արա PHP-FPM-ը։ Յուրաքանչյուր կայքի համար աշխատեցրու առանձին FPM pool իր սեփական համակարգային օգտատիրոջ տակ, որպեսզի կոտրված կայքը չկարողանա կարդալ ուրիշի ֆայլերը։ Երբեք SQL մի կառուցիր տողերի միացմամբ. օգտագործիր prepared statement-ներ կամ քո ORM-ի query builder-ը։ Գաղտնաբառերը hash արա password_hash()-ով (bcrypt կամ Argon2id), երբեք md5-ով կամ sha1-ով։ Դանդաղ աշխատանքը՝ email-ներ, PDF-ներ և երրորդ կողմի API կանչեր, տեղափոխիր queue worker-ների մոտ, որպեսզի վեբ հարցումները մնան արագ։
Օգտագործեք, երբ
- Վեբ հավելվածներ և REST API-ներ, հատկապես Laravel-ով կամ Symfony-ով, որտեղ արտադրողականությունն ու հսկայական էկոհամակարգը կարևոր են։
- Կոնտենտային կայքեր, e-commerce, CRM-ներ և ադմին մասով ծանր բիզնես համակարգեր։
- Նախագծեր, որոնց պետք է էժան, պարզ hosting ստանդարտ Linux սերվերների վրա կամ shared և managed hosting։
- Թիմեր, որոնք ուզում են հասուն, «ամեն ինչ ներառված» framework՝ queue-ներով, scheduling-ով, auth-ով և ORM-ով հենց տուփից։
Մի՛ օգտագործեք, երբ
- Երկարակյաց կապեր շատ մեծ մասշտաբով (հազարավոր WebSocket-ներ մեկ սերվերի վրա). Node.js-ը, Go-ն կամ Elixir-ն ավելի հարմար են, կամ PHP-ի կողքին hosted սերվիս, ինչպես Pusher-ը։
- CPU-ով ծանր աշխատանք, ինչպես վիդեոյի մշակումը կամ machine learning-ը, որտեղ Python-ը, Go-ն կամ Rust-ը ունեն ավելի լավ գրադարաններ և արտադրողականություն։
- Թիմ, որն արդեն լավ գիտի այլ backend stack. լավագույն լեզուն հաճախ այն է, որը թիմը կարող է սպասարկել։
Այլընտրանքներ
- Node.js (NestJS, Express, Fastify)
Մեկ լեզու frontend-ում և backend-ում, ուժեղ է realtime և I/O-ով ծանր սերվիսների համար։
- Python (Django, FastAPI)
Գերազանց է տվյալների, AI-ի և scripting-ով ծանր backend-ների համար. Django-ն նույնքան «ամեն ինչ ներառված» է, որքան Laravel-ը։
- Go
Կոմպիլյացվող, շատ արագ և պարզ deploy՝ որպես մեկ binary. սովորական CRUD հավելվածների համար ավելի շատ կոդ է պետք գրել։
- Ruby on Rails
Laravel-ի նման convention-over-configuration արտադրողականություն, բայց շատ տարածաշրջաններում ավելի փոքր աշխատաշուկա։
Լավագույն պրակտիկաներ
- Ֆայլերը սկսիր declare(strict_types=1)-ով և տիպավորիր յուրաքանչյուր պարամետր, վերադարձվող արժեք և property։
- Status-ների և տիպերի համար օգտագործիր backed enum-ներ, իսկ դրանք համապատասխանեցնելու համար՝ match, magic string-երի փոխարեն։
- Value object-ները (Money, Email, Address) դարձրու readonly, որպեսզի դրանք պատահաբար չփոխվեն։
- Commit արա composer.lock-ը, production-ում տեղադրիր --no-dev --optimize-autoloader-ով և CI-ում աշխատեցրու composer audit։
- Յուրաքանչյուր pull request-ի վրա աշխատեցրու PHPStan կամ Psalm բարձր մակարդակով և ավտոմատ formatter։
- Մնա աջակցվող PHP տարբերակի վրա և upgrade արա մեկ minor տարբերակ առ մեկ՝ Rector-ի օգնությամբ։
- PHP-FPM pool-երի չափը հարմարեցրու հասանելի հիշողությանը և յուրաքանչյուր կայք աշխատեցրու իր սեփական համակարգային օգտատիրոջ տակ։
Տարածված սխալներ
- Production-ում display_errors-ը միացված թողնելը և stack trace-ներ, ֆայլերի ճանապարհներ կամ credentials բացահայտելը։
- Օգտատիրոջ մուտքը SQL-ի մեջ միացնելը՝ prepared statement-ների փոխարեն։
- Deploy-ից հետո PHP-FPM-ը reload անել մոռանալը, երբ OPcache-ի timestamp validation-ը անջատված է, և հետո զարմանալը, թե ինչու է հին կոդը դեռ աշխատում։
- pm.max_children-ը RAM-ի թույլատրածից շատ բարձր դնելը, ինչի պատճառով սերվերը բեռի տակ swap է անում և դանդաղում։
- Գումարը float-ով պահելը՝ minor unit-ներով integer-ի կամ decimal տիպի փոխարեն։
Խորը վերլուծություն
07OPcache, preloading և JIT՝ ներսից
PHP-ն յուրաքանչյուր source ֆայլ կոմպիլյացնում է opcode-ների՝ այն հրահանգների, որոնք կատարում է իր վիրտուալ մեքենան։ OPcache-ը այդ opcode-ները, ինչպես նաև interned string-ները, պահում է shared memory-ում, որն օգտագործում են բոլոր FPM worker-ները, այնպես որ ֆայլը կոմպիլյացվում է մեկ անգամ, ոչ թե ամեն request-ի ժամանակ։ Եթե cache-ը լցվում է, PHP-ն սկսում է վերակոմպիլյացնել, և արագությունը լուռ ընկնում է, ուստի opcache.memory_consumption-ը (լռելյայն 128 MB) և opcache.max_accelerated_files-ը (լռելյայն 10000) չափավորիր ըստ քո կոդային բազայի և hit rate-ին ու վատնված հիշողությանը հետևիր opcache_get_status()-ով։ Preloading-ը (opcache.preload, PHP 7.4+, Windows-ում չկա) սերվերի մեկնարկին աշխատեցնում է script, որը ընտրված class-ները մշտապես բեռնում է հիշողության մեջ՝ խնայելով ամեն request-ի linking-ի ծախսը. այդ ֆայլերի ցանկացած փոփոխություն պահանջում է FPM-ի լրիվ restart։ JIT-ը «տաք» opcode-ները կոմպիլյացնում է մեքենայական կոդի։ PHP 8.4-ից այն կարգավորվում է opcache.jit-ով (լռելյայն disable) և opcache.jit_buffer_size-ով (լռելյայն 64M), և դրա հիշողությունը գումարվում է OPcache-ի հիշողությանը։ Տիպիկ I/O-bound web հավելվածների համար JIT-ը քիչ բան է տալիս, որովհետև ժամանակը ծախսվում է տվյալների բազային սպասելու վրա. այն փայլում է CPU-ծանր աշխատանքում, օրինակ՝ նկարների մաթեմատիկայում կամ parser-ներում։
; php.ini - production OPcache tuning (example values)
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0 ; reload FPM on deploy
; opcache.preload=/var/www/app/preload.php
; opcache.preload_user=app_user
; Only for CPU-heavy workloads; measure before and after
; opcache.jit=tracing
; opcache.jit_buffer_size=64M
realpath_cache_size=4096K
realpath_cache_ttl=600
08Երկարակյաց worker-ներ՝ Octane, FrankenPHP և state-ի արտահոսքեր
Worker-mode runtime-ները framework-ը boot են անում մեկ անգամ, հետո հազարավոր request-ներ սպասարկում նույն պրոցեսից։ Սա վերացնում է bootstrapping-ի ծախսը և կարող է բազմապատկել throughput-ը, բայց կոտրում է share-nothing երաշխիքը, որի վրա նախկինում հենվում էիր։ Ամեն ինչ, ինչ պահված է static property-ում, container-ում կապված singleton-ում կամ գլոբալ զանգվածում, հիմա «ապրում է» մինչև հաջորդ request-ը, որը կարող է լինել այլ օգտատիրոջինը։ Տիպիկ բագեր՝ service, որը ընթացիկ tenant-ին կամ օգտատիրոջը cache է անում property-ում, static զանգված, որը անվերջ մեծանում է և հիշողություն արտահոսեցնում, մեկ request-ի համար փոխված կոնֆիգուրացիոն արժեք, որը այդպես էլ չի վերականգնվում։ Worker mode-ի կանոններ. request-ին հատուկ տվյալները ամեն անգամ վերցրու request-ից՝ պահելու փոխարեն, request-scoped service-ները գրանցիր որպես scoped binding-ներ, որոնք մաքրվում են request-ների միջև, խուսափիր static փոփոխվող state-ից և թույլ տուր runtime-ին որոշակի քանակով request-ներից հետո worker-ները վերագործարկել (Octane-ի --max-requests)՝ որպես ապահովության ցանց։ Load test արա երկու օգտատիրոջով զուգահեռ և ստուգիր, որ ոչ մեկը մյուսի տվյալները չի տեսնում։
<?php
// Dangerous in worker mode: state survives between requests
final class CurrentTenant
{
private static ?int $id = null; // leaks to the next request
public static function set(int $id): void { self::$id = $id; }
}
// Safer: derive it from the request every time
final class TenantResolver
{
public function __construct(private readonly Request $request) {}
public function id(): int
{
return (int) $this->request->attributes->get('tenant_id');
}
}
// Laravel: $this->app->scoped(TenantResolver::class); // reset per request
09Անվտանգությունը ներսից՝ session-ներ, hashing և unserialize
PHP-ի անվտանգության մի շարք խնդիրներ գալիս են լռելյայն կարգավորումներից և legacy ֆունկցիաներից, ոչ թե քո տրամաբանությունից։ Session-ներ. սահմանիր session.cookie_secure, session.cookie_httponly և session.cookie_samesite=Lax, միացրու session.use_strict_mode, որպեսզի PHP-ն մերժի իր չստեղծած session id-ները, և login-ից հետո id-ն վերագեներացրու session_regenerate_id(true)-ով՝ session fixation-ը կանխելու համար։ Գաղտնաբառեր. password_hash՝ PASSWORD_DEFAULT-ով (ներկայումս bcrypt) կամ PASSWORD_ARGON2ID-ով, ստուգված password_verify-ով, որը համեմատում է constant time-ում. հաջող login-ից հետո կանչիր password_needs_rehash՝ հին hash-երը աննկատ թարմացնելու համար։ Երբեք անվստահելի տվյալներ մի փոխանցիր unserialize()-ին. հատուկ կազմված մուտքը կարող է ստեղծել կամայական class-ների օբյեկտներ և գործարկել դրանց magic մեթոդները. սա կոչվում է object injection և կարող է հանգեցնել remote code execution-ի։ Փոխարենը օգտագործիր json_decode, կամ գոնե unserialize՝ allowed_classes-ը false դրած։ Գաղտնիքներն ու HMAC ստորագրությունները համեմատիր hash_equals-ով, ոչ թե ===-ով, որպեսզի timing-ի միջոցով տեղեկություն չարտահոսի։ Վերջապես, disable_functions-ը և open_basedir-ը պահիր որպես defence in depth, ոչ թե որպես հիմնական պաշտպանություն։
<?php
// Login: verify, then upgrade the hash if the algorithm or cost changed
if (password_verify($password, $user->password_hash)) {
if (password_needs_rehash($user->password_hash, PASSWORD_ARGON2ID)) {
$user->password_hash = password_hash($password, PASSWORD_ARGON2ID);
$user->save();
}
session_regenerate_id(true); // prevent session fixation
}
// Webhook signature: constant-time comparison
$expected = hash_hmac('sha256', $payload, $secret);
if (!hash_equals($expected, $signatureHeader)) {
http_response_code(401);
exit;
}
$data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); // not unserialize()
10Concurrency և profiling՝ Fiber-ներ, FPM status և slow log-եր
Մեկ PHP request-ն աշխատում է մեկ thread-ի վրա, բայց կան գործիքներ միաժամանակ ավելին անելու համար։ Fiber-ները (PHP 8.1) թեթև, cooperative կերպով պլանավորվող կոդի բլոկներ են, որոնք կարող են դադարել և շարունակվել. դրանք հազվադեպ ես ուղղակիորեն օգտագործում, բայց Revolt, AMPHP և ReactPHP գրադարանները դրանց վրա event loop-եր են կառուցում, որպեսզի մեկ պրոցեսը կարողանա միաժամանակ սպասել բազմաթիվ ցանցային կանչերի։ Դասական web հավելվածների համար concurrency-ն սովորաբար նշանակում է զուգահեռ HTTP կանչեր (Guzzle promise-ներ կամ Laravel-ի Http::pool) և queue worker-ներ ֆոնային job-երի համար։ Երբ կայքը դանդաղ է, նայիր՝ նախքան ենթադրելը։ FPM status էջը (pm.status_path) ցույց է տալիս ակտիվ, պարապ և հերթում սպասող պրոցեսները. աճող listen queue-ն նշանակում է, որ pool-ը շատ փոքր է կամ request-ները շատ դանդաղ են։ request_slowlog_timeout-ը գրում է սահմանից երկար աշխատող ցանկացած request-ի stack trace-ը, որն ուղիղ ցույց է տալիս արգելափակող կանչը։ Ավելի խորը profiling-ի համար օգտագործիր sampling profiler, օրինակ՝ SPX, Blackfire կամ Tideways, կամ Xdebug-ի profiler-ը միայն development-ում, որովհետև այն խիստ դանդաղեցնում է PHP-ն։
; FPM pool: observability
pm.status_path = /fpm-status ; restrict to localhost in nginx
request_slowlog_timeout = 5s
slowlog = /var/log/php-fpm/app-slow.log
request_terminate_timeout = 60s ; kill runaway requests
# Read the status page from the server itself
curl -s "http://127.0.0.1/fpm-status?full" | head -20
# Look at: listen queue, max listen queue, max children reached
Իրական իրավիճակներ
Deploy-ից հետո դեռ հին կոդն է սպասարկվում
Խնդիր
Ուղղումը deploy արվեց, դիսկի վրայի ֆայլերը ճիշտ են, բայց օգտատերերը դեռ տեսնում են հին վարքագիծը։ Laravel-ի cache-ի մաքրումը չօգնեց, և խնդիրը անհետանում է միայն այն ժամանակ, երբ ինչ-որ մեկը վերագործարկում է սերվերը։
Ինչպես լուծել
Սա OPcache-ն է՝ opcache.validate_timestamps=0-ով. PHP-ն երբեք չի ստուգում՝ արդյոք ֆայլերը փոխվել են, ուստի շարունակում է սպասարկել հին կոմպիլյացված opcode-ները։ FPM reload-ը դարձրու deploy script-ի մաս՝ նոր release-ը տեղում լինելուց հետո. systemctl reload php8.4-fpm, որը worker-ները փոխարինում է նրբորեն՝ առանց request-ներ կորցնելու։ Symlink-ի վրա հիմնված release-ների դեպքում նաև համոզվիր, որ nginx-ը որպես SCRIPT_FILENAME և DOCUMENT_ROOT փոխանցում է $realpath_root, այլապես realpath cache-ը կարող է շարունակել resolve անել հին release-ի պանակը։ Ավելացրու version endpoint կամ response header՝ git commit-ով, և ստուգիր այն deploy pipeline-ում, որպեսզի հնացած release-ը հայտնաբերվի ավտոմատ, ոչ թե օգտատերերի կողմից։
Սերվերը swap է անում, և պիկային ժամերին request-ները timeout են լինում
Խնդիր
Ճաշի ժամին կայքը դանդաղում է, սերվերը սկսում է swap անել, և nginx-ը գրանցում է 502 և 504 սխալներ։ FPM pool-ը կարգավորված էր pm.max_children = 100-ով՝ «ապահովության համար», 4 GB մեքենայի վրա։
Ինչպես լուծել
Չափիր worker-ի իրական չափը. ps -o rss -C php-fpm8.4-ը ցույց է տալիս յուրաքանչյուր worker-ի հիշողությունը. ենթադրենք՝ միջինը 60 MB է։ Տվյալների բազայից, Redis-ից և OS-ից հետո մնացած մոտ 2.5 GB-ի դեպքում անվտանգ առավելագույնը մոտ 40 worker է, ոչ թե 100։ Համապատասխանաբար իջեցրու pm.max_children-ը, սահմանիր pm.max_requests՝ հիշողություն արտահոսեցնող worker-ները վերագործարկելու համար, և միացրու pm.status_path՝ listen queue-ին հետևելու համար։ Հետո միացրու slow log-ը՝ գտնելու այն request-ները, որոնք worker-ները ամենաերկարն են պահում, հաճախ արտաքին API կանչեր կամ հաշվետվությունների export-ներ, և տեղափոխիր դրանք queue-ներ։ Եթե դրանից հետո էլ հերթը մեծանում է, պատասխանը ավելի շատ CPU-ն կամ ևս մեկ app սերվերն է, ոչ թե ավելի շատ worker-ները։
Մեծ հավելվածի թարմացում PHP 8.1-ից 8.4
Խնդիր
PHP 8.1-ը այլևս անվտանգության աջակցություն չունի, իսկ կոդային բազան ունի հարյուր հազարավոր տողեր, որոշ հին package-ներ և քիչ թեստեր հին մոդուլներում։ Բիզնեսը չի կարող իրեն թույլ տալ կոտրված checkout։
Ինչպես լուծել
Թարմացրու մեկ minor տարբերակ միաժամանակ։ Նախ աշխատեցրու composer why-not php 8.4՝ թարմացումն արգելափակող package-ները գտնելու և դրանք թարմացնելու համար։ Թիրախային տարբերակը ավելացրու CI-ում որպես matrix-ի երկրորդ գրառում, որպեսզի թեստերն աշխատեն երկու տարբերակներում։ Աշխատեցրու Rector-ը PHP 8.2, 8.3 և 8.4 կանոնների հավաքածուներով dry-run ռեժիմում, review արա փոփոխությունները և կիրառիր դրանք փոքր pull request-ներով։ Աշխատեցրու PHPStan-ը քո ընթացիկ level-ով նոր տարբերակի վրա և staging-ում log-երի deprecation ծանուցումները դիտարկիր որպես սխալներ։ Նախ deploy արա մեկ սերվերի կամ canary-ի վրա, հետևիր սխալների մակարդակին, հետո ամբողջությամբ տարածիր և հին տարբերակը հեռացրու CI-ից։
Արագ ուղեցույց
- Ցույց տալ PHP տարբերակը և բեռնված ini ֆայլերը
php -v && php --ini- Ստուգել OPcache-ի կարգավիճակը CLI config-ից
php -i | grep -E 'opcache.(enable|jit|memory)'- Թեստավորել FPM config-ը, հետո նրբորեն reload անել
sudo php-fpm8.4 -t && sudo systemctl reload php8.4-fpm- Չափել FPM worker-ների հիշողությունը
ps -o rss=,cmd= -C php-fpm8.4 | sort -n | tail- Ինչո՞ւ չեմ կարող թարմացնել
composer why-not php 8.4- Գտնել հնացած ուղիղ կախվածությունները
composer outdated --direct- Production տեղադրում
composer install --no-dev --classmap-authoritative --no-interaction- Կախվածությունների անվտանգության ստուգում
composer audit- Արտաքին տվյալների ապահով decode
json_decode($body, true, 512, JSON_THROW_ON_ERROR)- Գաղտնիքների constant-time համեմատում
hash_equals($expected, $given)
Իմ իրական նախագծերում
- Laravel API-ներ եմ թողարկել ամեն ինչի վրա՝ PHP 7.3-ից և Laravel 8-ից (իմ առաջին production պորտալը 2020-ին) մինչև PHP 8.3 և Laravel 13 մարզասրահների ցանցի CRM-ում։
- Կենդանիների ապրանքների հարթակը օգտագործում է մոտ 50 PHP enum և մոտ 110 service class, գները մոդելավորված են որպես value object-ներ, և կա 95 backend թեստային ֆայլ։
- WordPress խանութը Laravel տեղափոխելիս պահպանեցի համատեղելիությունը հին գաղտնաբառերի hash-երի հետ, որպեսզի գոյություն ունեցող հաճախորդները կարողանան մուտք գործել առանց պարտադիր reset-ի։
- Իմ կառավարած սերվերներում PHP-ն աշխատում է PHP-FPM-ի տակ nginx-ի հետևում, queue worker-ները՝ supervisor-ի տակ, իսկ scheduler-ը՝ cron-ով։
Հարցեր և պատասխաններ
Q1Ինչպե՞ս է PHP-FPM-ը մշակում հարցումը, և ինչո՞ւ է PHP-ն դրանից հետո «ամեն ինչ մոռանում»։
nginx-ը ընդունում է կապը և PHP հարցումները FastCGI-ով փոխանցում է PHP-FPM-ին։ FPM-ի master պրոցեսը պահում է worker-ների pool. ազատ worker-ը աշխատեցնում է մուտքային script-ը, կառուցում է պատասխանը և վերադարձնում այն։ Երբ հարցումն ավարտվում է, PHP-ն ազատում է իր ստեղծած բոլոր օբյեկտներն ու փոփոխականները։ Այս share-nothing մոդելը առավելություն է. հիշողության արտահոսքերը և հնացած վիճակը չեն կարող կուտակվել, իսկ crash-ը ազդում է միայն մեկ հարցման վրա։ Գինը framework-ի bootstrap-ն է յուրաքանչյուր հարցման ժամանակ, որը OPcache-ը նվազեցնում է՝ cache անելով կոմպիլյացված bytecode-ը։ Մշտական վիճակն ապրում է PHP-ից դուրս՝ տվյալների բազայում, Redis-ում կամ session store-ում։ Worker-mode runtime-ները, ինչպես Octane-ը կամ FrankenPHP-ն, այդ ապահովությունը փոխանակում են արագության հետ։
Q2Ի՞նչ փոխվեց PHP 8-ում, որ այն դարձավ «ժամանակակից» լեզու։
Մի քանի բան միասին։ Տիպերը դարձան իսկապես օգտակար՝ union, nullable, mixed, never, տիպավորված property-ներ և class constant-ներ, որոնք պարտադրվում են strict_types-ով։ Լեզուն ստացավ արտահայտիչ հնարավորություններ՝ match expression-ներ, named argument-ներ, constructor promotion, nullsafe օպերատոր, enum-ներ 8.1-ում, readonly property-ներ և class-ներ, property hook-եր և ասիմետրիկ տեսանելիություն 8.4-ում և pipe օպերատոր 8.5-ում։ Attribute-ները մետատվյալների համար փոխարինեցին docblock annotation-ներին։ Արտադրողականությունը բարելավվեց JIT կոմպիլյատորի և շարժիչի շարունակական աշխատանքի շնորհիվ։ Լեզվի շուրջ Composer-ը, PSR ստանդարտները, PHPStan-ը և Rector-ը տալիս են գործիքային միջավայր, որը համեմատելի է TypeScript-ի հետ։ 2010-ի tutorial-ների PHP-ն և այսօրվա PHP-ն աշխատանքի համար շատ տարբեր լեզուներ են։
Q3Ո՞րն է match-ի և switch-ի տարբերությունը։
match-ը expression է, ուստի վերադարձնում է արժեք, որը կարող ես անմիջապես վերագրել։ Այն համեմատում է խիստ նույնությամբ (===), այնպես որ '1'-ը չի համընկնում 1-ի հետ, ինչը վերացնում է switch-ի բագերի մի ամբողջ դաս։ Յուրաքանչյուր ճյուղ մեկ expression է՝ առանց fall-through-ի, ուստի չես կարող break մոռանալ։ Ամենակարևորը՝ եթե ոչ մի ճյուղ չի համընկնում և default չկա, match-ը նետում է UnhandledMatchError՝ լուռ ոչինչ չանելու փոխարեն։ Enum-ների հետ համադրությամբ դա նշանակում է, որ նոր case ավելացնելն ու այն մշակել մոռանալը թեստերում ձախողվում է բարձրաձայն։ switch-ը դեռ հազվադեպ օգտագործում եմ՝ side effect-ներով բազմահրամանային ճյուղերի համար, բայց արժեքներ համապատասխանեցնելու համար իմ լռելյայն ընտրությունը match-ն է։
Q4Ինչո՞ւ commit անել composer.lock-ը, և ո՞րն է install-ի և update-ի տարբերությունը։
composer.json-ը հայտարարում է ընդունելի տարբերակների միջակայքեր, իսկ composer.lock-ը գրանցում է լուծված ճշգրիտ տարբերակները և դրանց hash-երը։ composer install-ը կարդում է lock ֆայլը և տեղադրում հենց այդ տարբերակները, այնպես որ յուրաքանչյուր ծրագրավորող, CI run և սերվեր ստանում է նույն կոդը։ composer update-ը անտեսում է lock-ը, լուծում է միջակայքերով թույլատրված ամենանոր տարբերակները և վերագրում lock ֆայլը։ Այսպիսով, update-ը աշխատեցնում ես գիտակցաբար, իդեալում՝ մեկ փաթեթ առ մեկ, review ես անում փոփոխությունները և commit անում նոր lock-ը. deploy-ներն ու CI-ը միշտ աշխատեցնում են միայն install։ Առանց commit արված lock ֆայլի՝ մեկ շաբաթ տարբերությամբ deploy արված երկու սերվեր կարող են աշխատեցնել գրադարանների տարբեր տարբերակներ։
Q5Ինչպե՞ս ես PHP կոդային բազան պաշտպանում տիպային սխալներից առանց կոմպիլյատորի։
Շերտավոր ստուգումներով։ Նախ՝ declare(strict_types=1)-ը և տիպերի ամբողջական հայտարարությունները ստիպում են PHP-ին TypeError նետել՝ արժեքները լուռ փոխակերպելու փոխարեն։ Երկրորդ՝ ստատիկ անալիզը PHPStan-ով կամ Psalm-ով, որը CI-ում աշխատում է բարձր մակարդակով, կարդում է ամբողջ կոդային բազան և գտնում է սխալ տիպեր, չսահմանված մեթոդներ և անհնար պայմաններ մինչև deploy-ը։ Docblock-ներում generic-ները (array<int, Order>, Collection<User>) անալիզատորին ավելի շատ ինֆորմացիա են տալիս։ Երրորդ՝ enum-ները և readonly value object-ները դժվարացնում են անվավեր վիճակների արտահայտումը։ Վերջապես, թեստերը ծածկում են այն վարքը, որը տիպերը չեն կարող նկարագրել։ Միասին սա շատ մոտ է կոմպիլյացվող լեզվի երաշխիքներին։
Սենիոր մակարդակի հարցեր
S1Ե՞րբ կտեղափոխես PHP հավելվածը Octane-ի կամ FrankenPHP-ի նման worker-mode runtime-ի, և որո՞նք են ռիսկերը։
Դա դիտարկում եմ, երբ profiling-ը ցույց է տալիս, որ պատասխանի ժամանակի մեծ մասը framework-ի bootstrapping-ն է, ոչ թե տվյալների բազան կամ արտաքին API-ները, և երբ տրաֆիկը բավական բարձր է, որ լրացուցիչ throughput-ը նշանակություն ունենա, օրինակ՝ կարճ request-ներով ծանրաբեռնված API։ Այդ դեպքում մի քանի անգամ աճը իրատեսական է։ Ռիսկերը state-ի մասին են. static property-ները, ընթացիկ օգտատիրոջը կամ tenant-ին պահող singleton-ները և հիշողության մեջ cache-երը հիմա պահպանվում են request-ների միջև, ինչը կարող է տվյալներ արտահոսեցնել հաճախորդների միջև կամ հիշողությունը մեծացնել, մինչև պրոցեսը մեռնի։ Որոշ package-ներ դրա համար գրված չեն։ Ուստի անցնելուց առաջ ես ստուգում եմ container binding-ները, request-ին հատուկ service-ները նշում եմ որպես scoped, ավելացնում եմ max-requests սահմանափակում և աշխատեցնում եմ զուգահեռ load test-եր տարբեր օգտատերերով։ Եթե request-ներում գերիշխում է դանդաղ SQL-ը, query-ների ուղղումը շատ ավելի մեծ օգուտ է տալիս՝ շատ ավելի փոքր ռիսկով։
S2Ինչպե՞ս ես չափավորում և կարգավորում PHP-FPM-ը սերվերի համար, և ո՞ր pm ռեժիմն ես ընտրում։
Սկսում եմ հիշողությունից, ոչ թե ենթադրություններից։ Չափիր worker-ի միջին resident memory-ն իրական տրաֆիկի տակ, հանիր այն, ինչ պետք է OS-ին, տվյալների բազային, Redis-ին և այլ ծառայություններին, և բաժանիր. դա pm.max_children-ի առաստաղն է։ Կայուն տրաֆիկով նվիրված app սերվերի համար pm = static-ը խուսափում է fork-ի ծախսերից և կանխատեսելի հիշողություն է տալիս։ Մի քանի կայք հյուրընկալող սերվերի համար pm = dynamic-ը կամ ondemand-ը հիշողություն են խնայում հանգիստ pool-երի համար՝ պիկերի ժամանակ worker-ներ ստեղծելու գնով։ Սահմանում եմ pm.max_requests, որպեսզի դանդաղ արտահոսքերը սահմանափակվեն, միացնում եմ pm.status_path՝ listen queue-ին և max children reached-ին հետևելու համար, և slow log-ով գտնում եմ այն request-ները, որոնք worker-ները չափից երկար են պահում։ Եթե worker-ները հիմնականում I/O են սպասում, լուծումը սովորաբար queue-ներն են կամ caching-ը, ոչ թե ավելի մեծ pool-ը։
S3Ինչպե՞ս կնախագծես PHP կոդային բազան, որպեսզի այն դիմանա PHP-ի և framework-ի բազմաթիվ թարմացումների։
Բիզնես պատճառներով փոխվող կոդը առանձին պահելով այն կոդից, որը փոխվում է տեխնիկական պատճառներով։ Domain-ի տրամաբանությունն ապրում է պարզ, տիպավորված PHP class-ներում, value object-ներում և enum-ներում՝ հնարավորինս քիչ framework կանչերով, այնպես որ framework-ի թարմացումը հիմնականում դիպչում է controller-ներին, կոնֆիգուրացիային և ենթակառուցվածքային adapter-ներին։ Կախվածությունները քիչ են և ընտրված ըստ սպասարկման պատմության, որովհետև լքված package-ը հաճախ թարմացման իրական արգելքն է։ Բարձր level-ի static analysis-ը և կրիտիկական հոսքերի շուրջ ավտոմատ թեստերը մեծ ավտոմատ փոփոխություններն ապահով են դարձնում, իսկ Rector-ը syntax-ի միգրացիաների մեծ մասը կիրառում է մեխանիկորեն։ CI-ը վաղ է աշխատում PHP-ի հաջորդ տարբերակի վրա, այնպես որ deprecation-ները երևում են սխալ դառնալուց ամիսներ առաջ։ Փոխզիջումը սկզբում մի փոքր ավելի շատ կառուցվածքն է, որն իրեն արդարացնում է առաջին իսկ major տարբերակի գալու պահին։
Բառարան
- Opcode
- PHP վիրտուալ մեքենայի ցածր մակարդակի հրահանգ, որը ստացվում է source ֆայլի կոմպիլյացիայից։
- OPcache
- Կոմպիլյացված opcode-ների shared-memory cache, որպեսզի PHP-ն ամեն request-ի ժամանակ ֆայլերը չվերակոմպիլյացնի։
- Preloading
- Ընտրված class-ների բեռնում հիշողության մեջ մեկ անգամ՝ սերվերի մեկնարկին, որպեսզի ամեն request կարողանա դրանք անմիջապես օգտագործել։
- JIT
- Just-in-time կոմպիլյատոր, որը հաճախ կատարվող opcode-ները վերածում է native մեքենայական կոդի. ամենաշատն օգնում է CPU-bound աշխատանքին։
- Share-nothing
- Յուրաքանչյուր request սկսվում է մաքուր հիշողությամբ, և արտաքին պահոցից բացի ոչինչ չի անցնում հաջորդին։
- Worker mode
- Boot արված հավելվածի աշխատեցում երկարակյաց պրոցեսում, որը սպասարկում է բազմաթիվ request-ներ, ինչպես անում են Octane-ը և FrankenPHP-ն։
- Fiber
- Դադարեցվող և շարունակվող կոդի բլոկ (PHP 8.1), որն ասինխրոն գրադարաններն օգտագործում են event loop-եր կառուցելու համար։
- Object injection
- Հարձակում, որի ժամանակ հատուկ կազմված serialized մուտքը ստիպում է unserialize-ին ստեղծել օբյեկտներ, որոնց magic մեթոդները վնասակար կոդ են աշխատեցնում։
- Session fixation
- Հարձակվողը login-ից առաջ սահմանում է զոհի session id-ն. login-ից հետո id-ի վերագեներացումը կանխում է դա։
- Listen queue
- Ազատ FPM worker-ի սպասող request-ներ. աճող հերթը նշանակում է, որ pool-ը շատ փոքր է կամ շատ դանդաղ։
Շարունակեք սովորել
Պրոդուկտի գաղափար ունե՞ք։Եկեք ստեղծենք այն։
Պատմեք ձեր գաղափարի, պրոդուկտի կամ թիմի մասին։ Սովորաբար պատասխանում եմ մեկ աշխատանքային օրվա ընթացքում։