Подготовка за интервю за PHP

Въпроси за интервю по PHP

15 подбрани въпроса за интервю за PHP бекенд, групирани по ниво на старшинство. Използвайте ги, за да прегледате основите, практическите компромиси и начина на мислене на Senior ниво за производство.

Започнете AI интервю за PHPНе се изисква кредитна карта. Налична е 1 безплатна сесия.
Практика за техническо интервю на английски езикРежим, в който хора, за които английският не е роден език, могат да се упражняват за преминаване на технически интервюта.

Въпроси за Junior ниво

1Как системата от типове на PHP разграничава слаба и строга типизация и какво се променя, когато е активирана `declare(strict_types=1)`?

PHP е слабо/коерцитивно по подразбиране за скаларни декларации на типове: когато една функция декларира скаларни типове параметри или върнати стойности, PHP може да преобразува съвместими стойности като числови низове в числа, вместо да ги отхвърля. `declare(strict_types=1)` прави проверката на скаларните типове строга за този файл, така че несъвместимите скаларни стойности не се коерцират и се хвърля `TypeError`. Директивата е с обхват на файла: за аргументи на дефинирани от потребителя функции, файлът на извикващия контролира строгостта; за `return` изрази, файлът, където е дефинирана функцията, контролира строгостта на върнатата стойност. Тя не прави PHP глобално строг или не деактивира всички преобразувания в езика.

<?php
// lib.php
function takesInt(int $x): int {
    return $x;
}

// weak.php, no strict_types
echo takesInt('123'); // coerces string to int

// strict.php
declare(strict_types=1);
echo takesInt('123'); // TypeError
Опитайте да отговорите на този въпрос с треньор с ИИ

2Обяснете разликата между `==` и `===` в PHP, включително подводни камъни при числови низове, масиви, обекти и `in_array()`.

`==` извършва нестриктно сравнение (loose comparison), така че PHP може да преобразува типове, преди да сравни; `===` извършва стриктно сравнение (strict comparison), изисквайки както еднаква стойност, така и еднакъв тип без принудително преобразуване. Числовите низове са чест подводен камък, защото могат да бъдат третирани като числа при нестриктни сравнения. Масивите и обектите имат свои собствени правила за сравнение: масивите се сравняват по ключове и стойности, докато `==` за обекти сравнява обекти от един и същ клас по свойства, а `===` за обекти изисква точно същия екземпляр. `in_array()` използва нестриктно сравнение по подразбиране, така че подайте `true` като трети аргумент, когато типът трябва да съвпада.

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
Опитайте да отговорите на този въпрос с треньор с ИИ

3По какво се различават типовете за връщана стойност `mixed`, `void` и `never` в PHP и какви гаранции предоставят?

Като типове за връщана стойност, `mixed`, `void` и `never` дават различни обещания. `mixed` означава, че функцията може да върне всякаква стойност, включително `null`. `void` означава, че функцията не е предназначена да връща смислена стойност; тя може да използва `return;`, но не може да върне израз. `never` означава, че функцията не се връща нормално към своя извикващ: тя винаги трябва да хвърли изключение, да излезе или по друг начин да прекрати потока на изпълнение. `never` е по-силен от `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`?

В PHP, `iterable` е декларация на тип, която означава „масив (`array`) или обект, имплементиращ `Traversable`“. `Traversable` е базовият маркерен интерфейс за обекти, които могат да бъдат използвани от `foreach`; потребителският код обикновено имплементира `Iterator` или `IteratorAggregate`, които разширяват `Traversable`. `Generator` е обектът, върнат от функция генератор, използваща `yield`; той имплементира `Iterator`, следователно също е `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 и как си взаимодействат с наследяването и трейтовете?

В PHP, public членовете са достъпни отвсякъде, protected членовете са достъпни от деклариращия клас и подкласовете, а private членовете са достъпни само от класа, който ги декларира. При наследяването, подкласовете могат да използват наследените public и protected членове, но private членовете на родителския клас не са достъпни като наследен API. При презаписване на методи, дъщерен клас обикновено не може да намали видимостта; например, public метод на родител трябва да остане public. Трейтовете се включват в използващия клас и членовете на трейта следват правилата за видимост на PHP. Видимостта на методите на трейта може също да бъде адаптирана или алиасирана с `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Как работят трейтовете (traits) в PHP и как се разрешават конфликтите, когато множество трейтове дефинират един и същ метод?

Трейтовете са механизмът на PHP за хоризонтално преизползване на код: един клас може да използва един или повече трейтове и членовете на трейта се включват в класа. Трейтовете не са самостоятелни класове и не могат да бъдат инстанцирани директно. Ако множество трейтове предоставят методи с едно и също име, PHP изисква изрично разрешаване на конфликти. Операторът `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);
Опитайте да отговорите на този въпрос с треньор с ИИ

Въпроси за Middle ниво

8Как скаларните типове, типовете, допускащи `null`, типовете обединение, типовете сечение и DNF типовете изразяват договорите на PHP API (Application Programming Interface) и кога трябва да се използва всеки от тях?

Декларациите на типове в PHP описват стойностите, които даден API (програмен интерфейс) приема или връща. Скаларни типове като `int`, `float`, `string` и `bool` се използват, когато се очаква точно един примитивен вид. Типовете, допускащи `null`, записани като `?T` или `T|null`, позволяват или стойност от тип `T`, или `null`. Типовете обединение (union types) като `A|B` позволяват една от няколко алтернативи. Типовете сечение (intersection types) като `A&B` изискват стойността да удовлетворява всички изброени типове, често множество интерфейси. DNF (Дизюнктивна нормална форма) типовете комбинират обединения и сечения, например `(A&B)|C`, за да изразят по-сложни договори. Всеки от тях трябва да се използва, когато съответства на реалното обещание на API, като се избягват ненужно широки или объркващи типове.

<?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`, `first-class callables`, `invokable objects` и `callable arrays` в PHP?

`callable` е широк тип/договор за всяка стойност, която PHP може да извика, като низ (string) с име на функция, `Closure`, `invokable object` или масив за референция към метод като `[$object, 'method']` или `[ClassName::class, 'method']`. `Closure` е конкретен обект на анонимна функция и сама по себе си е `callable`. Синтаксисът `first-class callable`, като `$obj->method(...)` или `strlen(...)`, създава `Closure` от израз, който може да се извика. `Invokable object` е `callable`, защото дефинира метода `__invoke()`. `Callable arrays` са традиционната двуелементна форма за референция към метод. Функцията `is_callable()` може да се използва за проверка на извикваемостта по време на изпълнение.

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 е полезен за целочислена аритметика с произволна точност.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Опитайте да отговорите на този въпрос с треньор с ИИ

11Обяснете PHP референциите (&), копирането при запис (copy-on-write) и как референциите могат да създадат неочаквани странични ефекти.

PHP променливите се представят вътрешно чрез `zval`s. За стойности като масиви и низове, PHP използва механизъм "копиране при запис" (copy-on-write): присвояването `$b = $a` обикновено споделя една и съща базова стойност, докато една от променливите не бъде променена, в който момент PHP я разделя/копира. Референция, създадена с `&`, прави променливите псевдоними към един и същ контейнер, така че промяната на едната променя и другата и може да заобиколи поведението на разделяне, което хората очакват от нормално присвояване. Предаването по референция позволява на функция да модифицира променливата на извикващия. Референциите могат да причинят изненадващи странични ефекти, особено при `foreach ($array as &$value)`: променливата на цикъла остава референция към последния елемент след цикъла, така че повторното ѝ използване по-късно може случайно да презапише този елемент, освен ако не използвате `unset($value)`.

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
Опитайте да отговорите на този въпрос с треньор с ИИ

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;
Опитайте да отговорите на този въпрос с треньор с ИИ

Въпроси за Senior ниво

13Как бихте отстранявали проблеми с изчерпването на връзките към базата данни и бихте проектирали управлението на връзките за PHP-FPM и дълготрайни работници?

За да отстраня проблеми с изчерпването на връзките, бих сравнил лимитите за връзки към базата данни и текущите сесии с общата PHP конкурентност: всяко активно PHP-FPM дете може да поддържа собствена връзка към базата данни, а множество пулове от приложения, хостове, cron задачи, работници за опашки, миграции и разгръщания увеличават търсенето. Бих проверил изгледи на сесиите на базата данни, състояния на връзките, неактивни сесии в транзакция, бавни заявки, изчаквания за заключвания, регистрационни файлове за грешки, метрики на пула и скорошни промени в трафика или броя на работниците. За PHP-FPM поддържайте `max_children` и конкурентността на хост в рамките на бюджета на базата данни, използвайте кратки транзакции/връзки с обхват на заявката, извършвайте или отменяйте незабавно и избягвайте да задържате връзки по време на бавни външни извиквания. Постоянните PDO връзки могат да намалят разходите за свързване, но могат да закрепят много неактивни връзки за всеки FPM процес и да запазят състоянието на сесията, така че изискват внимание. Пулър или прокси като PgBouncer може да мултиплексира много PHP клиенти към по-малко връзки на сървъра на базата данни, когато приложението е съвместимо. Дълготрайните работници се нуждаят от изрично управление на жизнения цикъл на връзката: проверки на изправността, повторно свързване след изчаквания за неактивност/рестартирания/отказване/разклоняване, нулиране на състоянието на сесията и повторен опит само за безопасна или идемпотентна работа.

Example:
- 6 application servers
- 40 PHP-FPM max_children per server
- 2 separate FPM pools that can hit the database

Potential concurrent PHP request DB connections = 6 * 40 * 2 = 480

If PostgreSQL max_connections is 300 and queues/cron/admin tools also connect,
connection exhaustion is expected during traffic spikes.
Опитайте да отговорите на този въпрос с треньор с ИИ

14Как бихте се справили с реплики за четене (read replicas), разделяне на операциите четене/запис (read/write splitting), забавяне на репликацията (replication lag) и превключване към резервен основен сървър на база данни (primary database failover) в PHP?

Управлението на реплики за четене и превключване към резервен сървър в PHP изисква разделяне на трафика за четене и запис, смекчаване на забавянето на репликацията и управление на прекъсванията на връзката по време на превключване към резервен сървър. На ниво приложение, конфигурацията на базата данни или междинният софтуер (middleware) насочва заявки за четене (`SELECT`) към реплики за четене и заявки за запис (`INSERT`, `UPDATE`, `DELETE`) към основния инстанс (алтернативно управлявани чрез проксита за база данни като ProxySQL или AWS RDS Proxy). Тъй като репликацията е асинхронна, забавянето на репликацията може да доведе до остарели четения. За да се гарантира консистентност "прочетете вашите записи" (read-your-writes), приложението трябва да насочва четения в рамките на активна транзакция към основния, и временно да насочва последващи четения към основния за конкретна потребителска сесия или жизнен цикъл на заявка веднага след запис. По време на превключване на основния сървър, съвременните клъстери на бази данни издигат реплика и актуализират DNS/прокси крайните точки. В PHP, слоят за свързване трябва да прихваща загуба на връзка и грешки само за четене, да прекъсва остарели PDO сокети и да прилага контролирани повторни опити с експоненциално забавяне.

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
Опитайте да отговорите на този въпрос с треньор с ИИ

15Как се разгръщат безопасно миграциите на база данни и промените в схемата с нулев или минимален престой в PHP приложения?

Разгръщането на миграции на база данни с нулев престой в PHP приложения изисква разделяне на еволюцията на схемата от разгръщанията на кода на приложението, като се използва моделът Expand and Contract (паралелно изпълнение) заедно с неблокиращи онлайн DDL (език за дефиниране на данни) практики. Във фазата Expand се прилагат обратно съвместими промени в схемата (напр. добавяне на колони, позволяващи NULL стойности, нови таблици или едновременни индекси с помощта на инструменти като `pt-online-schema-change`, `gh-ost` или собствена онлайн DDL в MySQL/PostgreSQL). След това приложението се разгръща, за да поддържа двойно записване (записване както в старите, така и в новите структури на схемата), докато чете от една от двете. Историческите записи след това се попълват асинхронно в ограничени фонови партиди, за да се предотврати заключване на таблици или предизвикване на забавяне на репликацията. Във фазата Contract, след като целостта на данните е валидирана и четенията са изцяло превключени към новата структура, разгръщане на код премахва препратките към старата схема. Накрая, миграция за почистване премахва остарели колони или таблици. Безопасният откат изисква всяко междинно състояние на схемата да остане напълно съвместимо както с предишните, така и с предстоящите версии на приложението по време на поетапни разгръщания.

<?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
Опитайте да отговорите на този въпрос с треньор с ИИ