15 подбрани въпроса за интервю за PHP бекенд, групирани по ниво на старшинство. Използвайте ги, за да прегледате основите, практическите компромиси и начина на мислене на Senior ниво за производство.
1Как PHP различава слаба и строга типизация и какво променя declare(strict_types=1)?
PHP обикновено допуска съвместими скаларни преобразувания за декларирани параметри и връщани типове, например числов низ за параметър int. declare(strict_types=1) включва строги скаларни проверки: несъвместим аргумент или върната стойност предизвиква TypeError, но int остава допустим за float. За аргументите на потребителски функции строгостта се определя от извикващия файл; проверките на връщането — от файла с дефиницията. Извикванията от вътрешни PHP функции имат свои особености и не се управляват просто от strict_types на външния извикващ код. Директивата трябва да стои в началото на PHP файла. Тя не прави PHP статично типизиран и не изключва преобразуванията на операторите в целия език.
// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>
// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>
// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
echo takesInt('123');
} catch (TypeError $e) {
echo 'TypeError';
}
?>
2Обяснете разликата между == и === в PHP, включително капаните с числови низове, масиви, обекти и in_array().
== в PHP прави нестрого сравнение с преобразуване на типовете, когато е приложимо; === изисква съвпадение на типовете и стойностите без скаларно преобразуване. Числови низове могат да се сравняват числово, затова не използвайте нестрого сравнение при изискване за точна идентичност с отчитане на типа. Масивите са равни с ==, когато съдържат същите двойки ключ/стойност според правилата за сравнение, независимо от реда. === изисква допълнително същия ред и типове на ключовете и стойностите. При обекти == сравнява свойствата на обекти от един клас; === изисква същата инстанция. in_array по подразбиране е нестрог; подайте true като трети аргумент за строга проверка. Нестрогото сравнение между числа и низове се промени в PHP 8, затова не разчитайте на исторически примери без посочена версия.
3Как се различават mixed, void и never като връщани типове в PHP и какво гарантират?
mixed допуска всякаква върната стойност, включително null. void заявява, че функцията не връща значима стойност: може да достигне края си или да използва return;, но не може да върне израз. Нормално извикване на void функция се оценява като null. never, наличен от PHP 8.1, означава, че функцията никога не връща нормално управлението на извикващия код. Тя може да хвърли изключение, да прекрати изпълнението с exit или да остане в безкраен цикъл; не е длъжна в крайна сметка да прекрати процеса. Функция never не може да съдържа return. Ключовата разлика е, че void може да завърши нормално и извикващият код да продължи, докато never няма нормално продължение.
<?php
function parse(string $raw): mixed {
return json_decode($raw, true);
}
function logMessage(string $message): void {
error_log($message);
return; // allowed
}
function fail(string $message): never {
throw new RuntimeException($message);
}
logMessage('starting');
fail('stop');
echo 'unreachable';
4Какво е iterable в PHP и как е свързан с array, Traversable, Iterator и Generator?
iterable означава array|Traversable. Generator реализира Iterator, който разширява Traversable, затова удовлетворява iterable. Потребителските класове предоставят Traversable поведение чрез Iterator или IteratorAggregate, вместо директно да реализират Traversable в конкретен клас. Iterator предоставя методи за управление на обхождането; IteratorAggregate предоставя итератор. Генераторна функция с yield връща Generator. Важна разлика: foreach може да обхожда и видимите свойства на обикновен обект без Traversable, но това не прави обекта допустим за параметър, деклариран като iterable.
function printItems(iterable $items): void {
foreach ($items as $item) {
echo $item, PHP_EOL;
}
}
printItems(['a', 'b']);
function letters(): Generator {
yield 'x';
yield 'y';
}
printItems(letters());
5Какви са правилата за видимост public, protected и private в PHP и как взаимодействат с наследяването и trait-овете?
Членовете public са достъпни навсякъде, където има достъп до обекта или класа. protected са достъпни в деклариращия клас и свързаната йерархия от родителски и дъщерни класове, но не от произволен външен код. private са достъпни в деклариращия клас; подкласът не може да използва частен член на родителя като наследен интерфейс. Обикновен предефиниращ метод не може да стеснява наследената видимост, но конструкторите имат специални правила и не са безусловен пример за това правило. Членовете на trait се включват в използващия клас и следват неговите правила за видимост. Адаптацията as може да създава псевдоним, да променя видимостта на метод или и двете; добавянето на псевдоним не премахва оригиналното име.
<?php
class Base {
public function a() {}
protected function b() {}
private function c() {}
}
trait Logs {
public function log(string $message): void {}
}
class Child extends Base {
use Logs { log as protected writeLog; }
public function test(): void {
$this->a(); // OK
$this->b(); // OK
// $this->c(); // Not accessible: private to Base
$this->writeLog('ok'); // OK inside class, protected alias
}
}
$child = new Child();
$child->a();
// $child->b(); // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
6Как работят trait-овете в PHP и как се решават конфликти, когато няколко дефинират един метод?
Trait-овете осигуряват хоризонтално повторно използване на код: класът импортира членовете им с use, а самият trait не може да бъде инстанциран. Метод, деклариран в класа, има предимство пред методите на trait-овете, които имат предимство пред наследените родителски методи. Ако няколко trait-а предоставят един метод и конфликтът не е решен от метод на класа, използвайте insteadof за избор на реализация. Използвайте as за допълнително име и по избор друга видимост. Псевдонимът не преименува и не премахва оригиналния метод. Решаването на колизии и поддържането на ясен публичен интерфейс са отделни отговорности.
<?php
trait JsonLogger {
public function log(string $message): void {
echo "json:$message\n";
}
}
trait FileLogger {
public function log(string $message): void {
echo "file:$message\n";
}
}
class Service {
use JsonLogger, FileLogger {
JsonLogger::log insteadof FileLogger;
FileLogger::log as logToFile;
JsonLogger::log as protected logJson;
}
}
$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
7Какво представляват изброимите типове (enums) в PHP, включително чистите и поддържаните (backed) изброими типове, и как те подобряват моделирането на домейн?
Изброимите типове (enums) в PHP дефинират затворен набор от именувани случаи за даден тип, като например статуси на поръчки или потребителски роли. Чистите изброими типове имат само именувани случаи; поддържаните изброими типове (backed enums) дават на всеки случай уникална низова или цялочислена стойност, полезна за съхранение, програмни интерфейси (API) или оперативна съвместимост. Изброимите типове подобряват моделирането на домейн, като заместват крехки необработени низове, цели числа или константи с типово-безопасни, самодокументиращи се стойности. Изброимите типове в PHP могат също да имат методи и да имплементират интерфейси; всички изброими типове поддържат изброяване на техните случаи, а поддържаните изброими типове поддържат преобразуване към и от техните скаларни стойности.
enum OrderState
{
case Draft;
case Paid;
case Shipped;
}
enum PaymentStatus: string
{
case Pending = 'pending';
case Complete = 'complete';
case Failed = 'failed';
}
$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
8Как скаларните, nullable, обединените, пресечните и DNF типовете изразяват договори на PHP интерфейси и кога се използват?
PHP декларациите описват приеманите и връщаните стойности. Скаларните декларации int, float, string и bool избират категория стойности; допустимостта на съвместими скаларни преобразувания зависи и от strict_types и контекста. ?T или T|null допуска null. Обединенията от PHP 8.0 допускат алтернативи като int|string. Чистите пресичания от PHP 8.1 изискват обектът да удовлетворява всички изброени класове или интерфейси, например Cacheable&JsonSerializable; int&string не е валидно пресичане. DNF типовете (дизюнктивна нормална форма) от PHP 8.2 комбинират алтернативи и пресичания в скоби, например (Cacheable&JsonSerializable)|string. Избирайте най-тесния разбираем договор за валидните входове, без ненужни алтернативи.
<?php
interface Cacheable {}
class Report implements Cacheable, JsonSerializable {
public function jsonSerialize(): mixed { return []; }
}
function findName(int $id): ?string {
return $id > 0 ? 'Alice' : null;
}
function normalize(int|string $id): string {
return (string) $id;
}
function store(Cacheable&JsonSerializable $item): void {
// must satisfy both interfaces
}
function handle((Cacheable&JsonSerializable)|string $input): void {
// either an object satisfying both contracts, or a string
}
9Как се различават callable, Closure, първокласните callable стойности, извикваемите обекти и callable масивите в PHP?
callable описва извикваеми стойности: низове с имена на функции, Closure обекти, препратки към достъпни методи и обекти с __invoke. Closure е конкретен извикваем обект, произхождащ от анонимна функция или от именувана функция/метод, преобразувани в Closure. Синтаксисът за първокласни callable стойности, например strlen(...) или $object->method(...), създава Closure, без да извиква целта. Масивът [$object, 'method'] сочи достъпен инстанционен метод; [ClassName::class, 'method'] изисква подходящ достъпен статичен метод в съвременен PHP. is_callable проверява възможността за извикване в съответния обхват. callable е широк договор, а Closure е конкретно представяне.
class Greeter {
public function hello(string $name): string { return "Hello $name"; }
public function __invoke(string $name): string { return "Hi $name"; }
}
$g = new Greeter();
$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;
foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
echo $cb('Sam'), PHP_EOL;
}
10Как PHP обработва препълване на целочислени стойности (integer overflow), точност на числата с плаваща запетая и точни десетични изчисления?
PHP цели числа са с размер, зависещ от платформата, като лимитите са изложени от константи като `PHP_INT_MAX`. PHP не запазва автоматично резултатите от нативни цели числа като цели числа с произволна точност след препълване; аритметиката с препълващи се цели числа може да стане `float` или да загуби точна целочислена семантика. PHP числата с плаваща запетая са двоични стойности с плаваща запетая, така че много десетични дроби, като `0.1`, са приблизителни. За точни десетични или парични изчисления избягвайте числата с плаваща запетая и използвайте точен подход като по-малки цели единици, когато е подходящо, или инструменти за десетични числа с фиксирана/произволна точност като BCMath. GMP е полезен за целочислена аритметика с произволна точност.
11Обяснете PHP референциите (&), копирането при запис (copy-on-write) и как референциите могат да създадат неочаквани странични ефекти.
PHP променливите се представят вътрешно чрез `zval`s. За стойности като масиви и низове, PHP използва механизъм "копиране при запис" (copy-on-write): присвояването `$b = $a` обикновено споделя една и съща базова стойност, докато една от променливите не бъде променена, в който момент PHP я разделя/копира. Референция, създадена с `&`, прави променливите псевдоними към един и същ контейнер, така че промяната на едната променя и другата и може да заобиколи поведението на разделяне, което хората очакват от нормално присвояване. Предаването по референция позволява на функция да модифицира променливата на извикващия. Референциите могат да причинят изненадващи странични ефекти, особено при `foreach ($array as &$value)`: променливата на цикъла остава референция към последния елемент след цикъла, така че повторното ѝ използване по-късно може случайно да презапише този елемент, освен ако не използвате `unset($value)`.
12Как работят манипулаторите на обекти (object handles), присвояването, `clone`, `__clone`, плиткото копие (shallow copy) и дълбокото копие (deep copy) в PHP?
В PHP променлива от тип обект съхранява манипулатор към обект, а не пълно копие на обекта. Присвояването `$b = $a` копира манипулатора, така че двете променливи препращат към един и същ обект; променянето на обекта чрез която и да е променлива е видимо и през другата, без да се използва `&`. `clone $a` създава нова инстанция на обект с плитко копие на свойствата на оригиналния обект. Ако дадено свойство само по себе си е обект, свойството на клонирания обект все още сочи към същия вложен обект, освен ако изрично не го клонирате. `__clone()` се извиква върху новия обект след плиткото копие и е мястото за нулиране на идентификатори, отделяне на ресурси или извършване на дълбоко копие на вложени обекти. Дълбокото копие означава рекурсивно копиране на вложените изменяеми обекти, които не трябва да се споделят.
class Address {
public function __construct(public string $city) {}
}
class User {
public function __construct(public string $name, public Address $address) {}
public function __clone() {
$this->address = clone $this->address; // make nested object independent
}
}
$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1; // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;
$u3 = clone $u1; // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
13Как бихте отстранявали проблеми с изчерпването на връзките към базата данни и бихте проектирали управлението на връзките за PHP-FPM и дълготрайни работници?
За да отстраня проблеми с изчерпването на връзките, бих сравнил лимитите за връзки към базата данни и текущите сесии с общата PHP конкурентност: всяко активно PHP-FPM дете може да поддържа собствена връзка към базата данни, а множество пулове от приложения, хостове, cron задачи, работници за опашки, миграции и разгръщания увеличават търсенето. Бих проверил изгледи на сесиите на базата данни, състояния на връзките, неактивни сесии в транзакция, бавни заявки, изчаквания за заключвания, регистрационни файлове за грешки, метрики на пула и скорошни промени в трафика или броя на работниците. За PHP-FPM поддържайте `max_children` и конкурентността на хост в рамките на бюджета на базата данни, използвайте кратки транзакции/връзки с обхват на заявката, извършвайте или отменяйте незабавно и избягвайте да задържате връзки по време на бавни външни извиквания. Постоянните PDO връзки могат да намалят разходите за свързване, но могат да закрепят много неактивни връзки за всеки FPM процес и да запазят състоянието на сесията, така че изискват внимание. Пулър или прокси като PgBouncer може да мултиплексира много PHP клиенти към по-малко връзки на сървъра на базата данни, когато приложението е съвместимо. Дълготрайните работници се нуждаят от изрично управление на жизнения цикъл на връзката: проверки на изправността, повторно свързване след изчаквания за неактивност/рестартирания/отказване/разклоняване, нулиране на състоянието на сесията и повторен опит само за безопасна или идемпотентна работа.
Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child
Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
14Как бихте управлявали реплики за четене, разделяне на четене и запис, закъснение на репликацията и превключване при отказ на основната база в PHP?
Разделяйте връзките към основната база и репликите според смисъла на операцията, не само дали SQL започва със SELECT: заключващи четения, записи и транзакции с изискване за консистентността на основната база трябва да използват нея. Транзакции само за четене могат да използват подходяща реплика, ако изискванията им позволяват. Асинхронната репликация може да изостава. sticky в Laravel след запис насочва последващите четения в текущата заявка към записващата връзка; фиксиран период на привързване между заявки само смекчава проблема, без безусловна гаранция за четене на собствените записи. За гаранция четете от основната база или чакайте репликата да приложи необходимата позиция на записа, с ограничено време и преминаване към основната база при изтичане. Прокситата се различават: RDS Proxy има отделни крайни точки за четене и запис и не е универсален автоматичен разпределител на отделни SQL инструкции. При отказ изхвърляйте остарелите връзки и се свързвайте отново с ограничено увеличаване на паузите. Загубен отговор на commit означава неясен резултат: не повтаряйте неидемпотентни записи на сляпо; използвайте ключове за идемпотентност или първо установете резултата.
15Как безопасно се внедряват миграции и промени на схема в PHP приложения без престой или с минимален престой?
Използвайте разширяване и последващо премахване на старите структури, съвместими със старата и новата версия при поетапно внедряване. Първо добавете съвместими структури, после внедрете код за двете представяния. Ако е нужен двоен запис, осигурете консистентност и проектирайте възобновяемо попълване на историческите данни с ограничена скорост, което не презаписва по-нови записи. Проверете копираните данни и превключете четенията, преди да премахнете стария код. Онлайн DDL (език за дефиниране на данни) намалява определени рискове от блокиране, но не винаги е без заключвания: проверете специфичните за базата и версията заключвания, транзакционните ограничения, ресурсите и ефекта върху репликацията. PostgreSQL CREATE INDEX CONCURRENTLY например има ограничения и след неуспех може да остави невалиден индекс. Изтрийте старите колони едва след спирането на старите инстанции и изтичането на необходимия прозорец за връщане. След разрушително премахване връщането към код, който изисква изтритите данни, вече не е автоматично безопасно.
<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
$table->string('full_name')->nullable()->after('name');
});
// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
public function setNameAttribute($value) {
$this->attributes['name'] = $value;
$this->attributes['full_name'] = $value; // Dual write
}
}
// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column