Przygotowanie do rozmowy kwalifikacyjnej z PHP

Pytania na rozmowę z PHP

15 wybranych pytań rekrutacyjnych dla programistów backendowych PHP, pogrupowanych według poziomu doświadczenia. Wykorzystaj je do powtórzenia podstaw, rozważenia praktycznych kompromisów i wnioskowania na poziomie seniora w kontekście produkcyjnym.

Rozpocznij wywiad AI z PHPKarta kredytowa nie jest wymagana. Dostępna 1 darmowa sesja.
Trening technicznych rozmów rekrutacyjnych po angielskuTryb dla osób, dla których angielski nie jest językiem ojczystym, do ćwiczenia rozmów technicznych.

Pytania dla Juniorów

1W jaki sposób system typów PHP rozróżnia typowanie słabe i ścisłe, oraz co się zmienia po włączeniu `declare(strict_types=1)`?

PHP jest domyślnie słaby/koercyjny dla deklaracji typów skalarnych: gdy funkcja deklaruje skalarne typy parametrów lub zwracanych wartości, PHP może konwertować zgodne wartości, takie jak ciągi numeryczne, na liczby zamiast je odrzucać. `declare(strict_types=1)` sprawia, że sprawdzanie typów skalarnych staje się ścisłe dla danego pliku, więc niezgodne wartości skalarne nie są koercyjnie konwertowane i zgłaszany jest `TypeError`. Dyrektywa ma zakres pliku: dla argumentów funkcji zdefiniowanych przez użytkownika plik wywołujący kontroluje ścisłość; dla instrukcji `return` plik, w którym funkcja jest zdefiniowana, kontroluje ścisłość wartości zwracanej. Nie sprawia to, że PHP staje się globalnie ścisły ani nie wyłącza wszystkich konwersji w języku.

<?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
Odpowiedz na to pytanie z trenerem AI

2Wyjaśnij różnicę między `==` a `===` w PHP, w tym pułapki związane z ciągami znaków liczbowych, tablicami, obiektami i funkcją `in_array()`.

`==` wykonuje luźne porównanie (loose comparison), więc PHP może dokonać żonglerki typami (type juggling) przed porównaniem; `===` wykonuje ścisłe porównanie (strict comparison), wymagając zarówno tej samej wartości, jak i tego samego typu, bez konwersji. Ciągi znaków liczbowych (numeric strings) to częsta pułapka, ponieważ mogą być traktowane jako liczby w luźnych porównaniach. Tablice i obiekty mają własne zasady porównywania: tablice są porównywane według kluczy i wartości, podczas gdy operator `==` dla obiektów porównuje obiekty tej samej klasy według właściwości, a operator `===` dla obiektów wymaga dokładnie tej samej instancji. Funkcja `in_array()` domyślnie używa luźnego porównania, więc należy przekazać `true` jako trzeci argument, gdy typy muszą się zgadzać.

<?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
Odpowiedz na to pytanie z trenerem AI

3Czym różnią się `mixed`, `void` i `never` jako typy zwracane w PHP i jakie gwarancje zapewniają?

Jako typy zwracane, `mixed`, `void` i `never` zapewniają różne obietnice. `mixed` oznacza, że funkcja może zwrócić dowolną wartość, włączając w to `null`. `void` oznacza, że funkcja nie jest przeznaczona do zwracania znaczącej wartości; może używać `return;`, ale nie może zwracać wyrażenia. `never` oznacza, że funkcja nie wraca normalnie do swojego wywołującego: musi zawsze rzucić wyjątek, zakończyć działanie lub w inny sposób przerwać przepływ sterowania. `never` jest silniejszy niż `void`, ponieważ wykonanie nie może być kontynuowane po normalnym wywołaniu funkcji typu `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';
Odpowiedz na to pytanie z trenerem AI

4Czym jest `iterable` w PHP i jak odnosi się do `array`, `Traversable`, `Iterator` i `Generator`?

W PHP, `iterable` to deklaracja typu oznaczająca „`array` lub obiekt implementujący `Traversable`”. `Traversable` jest podstawowym interfejsem znacznikowym (marker interface) dla obiektów, które mogą być używane z `foreach`; kod użytkownika (userland code) zazwyczaj implementuje `Iterator` lub `IteratorAggregate`, które rozszerzają `Traversable`. `Generator` to obiekt zwracany przez funkcję generatora za pomocą `yield`; implementuje on `Iterator`, więc jest również `Traversable` i akceptowany wszędzie tam, gdzie wymagane jest `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());
Odpowiedz na to pytanie z trenerem AI

5Jakie są zasady widoczności `public`, `protected` i `private` w PHP i jak współdziałają z dziedziczeniem i traitami?

W PHP składowe `public` są dostępne z dowolnego miejsca, składowe `protected` są dostępne z klasy deklarującej i podklas, a składowe `private` są dostępne tylko z klasy, która je deklaruje. W przypadku dziedziczenia, podklasy mogą używać odziedziczonych składowych `public` i `protected`, ale prywatne składowe klasy nadrzędnej nie są dostępne jako dziedziczone API. Przy nadpisywaniu metod, klasa potomna zazwyczaj nie może zmniejszyć widoczności; na przykład, publiczna metoda klasy nadrzędnej musi pozostać publiczna. Traity są komponowane do klasy używającej, a składowe traitu podlegają zasadom widoczności PHP. Widoczność metod traitu może być również adaptowana lub aliasowana za pomocą `as` podczas importowania traitu.

<?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
Odpowiedz na to pytanie z trenerem AI

6Jak działają cechy (traits) w PHP i w jaki sposób rozwiązywane są konflikty, gdy wiele cech definiuje tę samą metodę?

Cechy (traits) to mechanizm PHP służący do horyzontalnego ponownego wykorzystania kodu: klasa może używać jednej lub więcej cech, a elementy cechy są włączane do klasy. Cechy nie są samodzielnymi klasami i nie mogą być bezpośrednio instancjonowane. Jeśli wiele cech dostarcza metody o tej samej nazwie, PHP wymaga jawnego rozwiązania konfliktu. Operator `insteadof` wybiera, która metoda cechy zostanie użyta dla kolidującej nazwy. Operator `as` może stworzyć alias dla metody cechy, a także zmienić widoczność zaimportowanej metody. Metody klas nadpisują metody cech, a metody cech mają pierwszeństwo przed odziedziczonymi metodami klasy nadrzędnej.

<?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
Odpowiedz na to pytanie z trenerem AI

7Czym są enumy (typy wyliczeniowe) w PHP, w tym czyste enumy i enumy ze wspieranymi wartościami (backed enums), i jak poprawiają one modelowanie dziedziny (domain modeling)?

Enumy (typy wyliczeniowe) w PHP definiują zamknięty zbiór nazwanych przypadków dla danego typu, takich jak statusy zamówień lub role użytkowników. Czyste enumy mają tylko nazwane przypadki; enumy ze wspieranymi wartościami (backed enums) przypisują każdemu przypadkowi unikalną wartość ciągu znaków (string) lub liczbę całkowitą (int), co jest przydatne do przechowywania, użycia w API (Application Programming Interface) lub interoperacyjności. Enumy poprawiają modelowanie dziedziny (domain modeling) poprzez zastępowanie wrażliwych surowych ciągów znaków, liczb całkowitych lub stałych wartościami bezpiecznymi typowo i samodokumentującymi się. Enumy w PHP mogą również mieć metody i implementować interfejsy; wszystkie enumy obsługują listowanie swoich przypadków, a enumy ze wspieranymi wartościami obsługują konwersję do i z ich wartości skalarnych.

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);
Odpowiedz na to pytanie z trenerem AI

Pytania dla Midów

8Jak typy skalarne, dopuszczające wartość `null`, sumy, przecięcia i DNF (Disjunctive Normal Form) wyrażają kontrakty API (Application Programming Interface) w PHP i kiedy należy używać każdego z nich?

Deklaracje typów w PHP opisują wartości, które API akceptuje lub zwraca. Typy skalarne, takie jak `int`, `float`, `string` i `bool`, są używane, gdy oczekiwany jest dokładnie jeden typ prymitywny. Typy dopuszczające wartość `null` (nullable types), zapisywane jako `?T` lub `T|null`, pozwalają na wartość typu `T` lub `null`. Typy sum (union types), takie jak `A|B`, pozwalają na jedną z kilku alternatyw. Typy przecięć (intersection types), takie jak `A&B`, wymagają, aby wartość spełniała wszystkie wymienione typy, zazwyczaj wiele interfejsów. Typy DNF (Disjunctive Normal Form) łączą sumy i przecięcia, na przykład `(A&B)|C`, aby wyrażać bardziej złożone kontrakty. Każdy z nich powinien być używany, gdy odpowiada rzeczywistej obietnicy API, unikając niepotrzebnie ogólnych lub mylących typów.

<?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
}
Odpowiedz na to pytanie z trenerem AI

9Jaka jest różnica między `callable`, `Closure`, wywoływalnymi obiektami pierwszej klasy (first-class callables), obiektami wywoływalnymi (invokable objects) i tablicami wywoływalnymi (callable arrays) w PHP?

`callable` to szeroki typ/kontrakt dla dowolnej wartości, którą PHP może wywołać, takiej jak ciąg znaków z nazwą funkcji, `Closure`, obiekt wywoływalny lub tablica referencji do metody, np. `[$object, 'method']` lub `[ClassName::class, 'method']`. `Closure` to konkretny obiekt funkcji anonimowej i sam w sobie jest wywoływalny. Składnia wywoływalnych obiektów pierwszej klasy (first-class callable), np. `$obj->method(...)` lub `strlen(...)`, tworzy `Closure` z wyrażenia wywoływalnego. Obiekt wywoływalny jest wywoływalny, ponieważ definiuje `__invoke()`. Tablice wywoływalne (callable arrays) to tradycyjna dwuelementowa forma referencji do metody. Funkcja `is_callable()` może być użyta do sprawdzenia możliwości wywołania w czasie wykonania.

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;
}
Odpowiedz na to pytanie z trenerem AI

10Jak PHP (Hypertext Preprocessor) obsługuje przepełnienie liczby całkowitej (integer overflow), precyzję liczb zmiennoprzecinkowych (floating-point precision) i dokładne obliczenia dziesiętne?

Liczby całkowite w PHP mają rozmiar zależny od platformy, z ograniczeniami ujawnionymi przez stałe takie jak `PHP_INT_MAX`. PHP nie zachowuje automatycznie natywnych wyników całkowitych jako liczb całkowitych o dowolnej precyzji po przepełnieniu; arytmetyka liczb całkowitych powodująca przepełnienie może stać się `float` lub w inny sposób stracić dokładną semantykę liczb całkowitych. Liczby zmiennoprzecinkowe (floats) w PHP są binarnymi wartościami zmiennoprzecinkowymi, dlatego wiele ułamków dziesiętnych, takich jak `0.1`, jest przybliżonych. Do dokładnych obliczeń dziesiętnych lub pieniężnych należy unikać liczb zmiennoprzecinkowych i stosować precyzyjne podejście, takie jak całkowite jednostki pomniejsze (np. grosze zamiast złotówek), gdy jest to właściwe, lub narzędzia do obliczeń dziesiętnych o stałej/dowolnej precyzji, takie jak BCMath. GMP jest przydatne do arytmetyki liczb całkowitych o dowolnej precyzji.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Odpowiedz na to pytanie z trenerem AI

11Wyjaśnij referencje (&) w PHP, kopiowanie przy zapisie (copy-on-write) oraz to, jak referencje mogą prowadzić do nieoczekiwanych skutków ubocznych.

Zmienne PHP są wewnętrznie reprezentowane przez zvals. W przypadku wartości takich jak tablice i ciągi znaków, PHP stosuje kopiowanie przy zapisie (copy-on-write): przypisanie `$b = $a` zazwyczaj współdzieli tę samą podstawową wartość, dopóki jedna ze zmiennych nie zostanie zmodyfikowana, w którym to momencie PHP ją rozdziela/kopiuje. Referencja utworzona za pomocą `&` sprawia, że zmienne stają się aliasami do tego samego kontenera, więc zmiana jednej zmienia drugą i może omijać zachowanie rozdzielenia, którego ludzie oczekują po normalnym przypisaniu. Przekazywanie przez referencję pozwala funkcji modyfikować zmienną wywołującego. Referencje mogą powodować zaskakujące skutki uboczne, zwłaszcza z `foreach ($array as &$value)`: zmienna pętli pozostaje referencją do ostatniego elementu po zakończeniu pętli, więc ponowne jej użycie później może przypadkowo nadpisać ten element, chyba że wykonasz `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);
Odpowiedz na to pytanie z trenerem AI

12Jak działają uchwyty obiektów, przypisanie, klonowanie (`clone`), metoda magiczna `__clone`, płytka kopia i głęboka kopia w PHP?

W PHP zmienna obiektu przechowuje uchwyt do obiektu, a nie pełną kopię obiektu. Przypisanie `$b = $a` kopiuje uchwyt, więc obie zmienne odwołują się do tego samego obiektu; modyfikacja obiektu za pośrednictwem którejkolwiek zmiennej jest widoczna przez drugą, bez używania `&`. `clone $a` tworzy nową instancję obiektu z płytką kopią właściwości oryginalnego obiektu. Jeśli właściwość jest sama w sobie obiektem, właściwość sklonowanego obiektu nadal wskazuje na ten sam zagnieżdżony obiekt, chyba że jawnie go sklonujesz. Metoda `__clone()` jest wywoływana na nowym obiekcie po płytkiej kopii i jest miejscem do resetowania identyfikatorów, odłączania zasobów lub wykonywania głębokiej kopii zagnieżdżonych obiektów. Głęboka kopia oznacza rekurencyjne kopiowanie zagnieżdżonych, mutowalnych obiektów, które nie powinny być współdzielone.

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;
Odpowiedz na to pytanie z trenerem AI

Pytania dla Seniorów

13Jak rozwiązałbyś problem wyczerpania puli połączeń do bazy danych i zaprojektowałbyś zarządzanie połączeniami dla PHP-FPM i długo działających workerów?

Aby rozwiązać problem wyczerpania połączeń, porównałbym limity połączeń do bazy danych i bieżące sesje z całkowitą jednoczesnością PHP: każdy aktywny proces potomny PHP-FPM może utrzymywać własne połączenie z bazą danych, a wiele pul aplikacji, hostów, zadań cron, procesów obsługujących kolejki, migracji i wdrożeń zwielokrotnia zapotrzebowanie. Sprawdziłbym widoki sesji bazy danych, stany połączeń, sesje bezczynne w transakcji, wolne zapytania, oczekiwania na blokady, logi błędów, metryki puli oraz ostatnie zmiany w ruchu lub liczbie workerów. Dla PHP-FPM, należy utrzymywać `max_children` i jednoczesność na hosta w ramach budżetu bazy danych, używać krótkotrwałych transakcji/połączeń o zasięgu żądania, zatwierdzać lub wycofywać transakcje szybko i unikać utrzymywania połączeń podczas wolnych wywołań zewnętrznych. Trwałe połączenia PDO (Persistent PDO connections) mogą zredukować narzut związany z nawiązywaniem połączeń, ale mogą przypiąć wiele bezczynnych połączeń do każdego procesu FPM i zachować stan sesji, dlatego wymagają ostrożności. Puler lub proxy, takie jak PgBouncer, może multipleksować wielu klientów PHP na mniejszą liczbę połączeń z serwerem bazy danych, jeśli aplikacja jest kompatybilna. Długo działające workery wymagają jawnego zarządzania cyklem życia połączenia: kontrole zdrowia, ponowne nawiązanie połączenia po przekroczeniu limitu czasu bezczynności/restartach/przełączeniu awaryjnym/forkach, resetowanie stanu sesji i ponawianie tylko bezpiecznych lub idempotentnych operacji.

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.
Odpowiedz na to pytanie z trenerem AI

14Jak obsłużyć repliki odczytu, rozdzielanie odczytów/zapisów, opóźnienie replikacji i przełączanie awaryjne głównej bazy danych w PHP?

Obsługa replik odczytu i przełączania awaryjnego w PHP wymaga rozdzielenia ruchu odczytu i zapisu, łagodzenia opóźnień replikacji oraz zarządzania zakłóceniami połączeń podczas przełączania awaryjnego. Na poziomie aplikacji konfiguracja bazy danych lub oprogramowanie pośredniczące (middleware) kieruje zapytania odczytu (`SELECT`) do replik odczytu, a zapytania zapisu (`INSERT`, `UPDATE`, `DELETE`) do instancji głównej (alternatywnie zarządzane za pośrednictwem serwerów proxy baz danych, takich jak ProxySQL lub AWS RDS Proxy). Ponieważ replikacja jest asynchroniczna, opóźnienie replikacji może powodować nieaktualne odczyty. Aby zagwarantować spójność 'read-your-writes' (odczyt-po-zapisie), aplikacja powinna kierować odczyty w ramach aktywnej transakcji do instancji głównej, a także tymczasowo kierować kolejne odczyty do instancji głównej dla określonej sesji użytkownika lub cyklu życia żądania natychmiast po zapisie. Podczas przełączania awaryjnego instancji głównej, nowoczesne klastry baz danych promują replikę i aktualizują punkty końcowe DNS (Domain Name System)/proxy. W PHP warstwa połączeń musi wyłapywać utraty połączeń i błędy tylko do odczytu, usuwać nieaktualne gniazda PDO (PHP Data Objects) i stosować kontrolowane ponawianie z wykładniczym wycofywaniem.

<?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',
    ],
];
Odpowiedz na to pytanie z trenerem AI

15Jak bezpiecznie wdrożyć migracje baz danych i zmiany schematu z zerowym lub minimalnym przestojem w aplikacjach PHP?

Wdrażanie migracji baz danych z zerowym przestojem w aplikacjach PHP wymaga oddzielenia ewolucji schematu od wdrożeń kodu aplikacji, stosując wzorzec Rozszerz i Zwiń (równoległe działanie) wraz z nieblokującymi praktykami DDL (Data Definition Language) online. W fazie Rozszerzenia stosuje się wstecznie kompatybilne zmiany schematu (np. dodawanie kolumn dopuszczających wartości null, nowych tabel lub indeksów współbieżnych za pomocą narzędzi takich jak `pt-online-schema-change`, `gh-ost` lub natywnych DDL online w MySQL/PostgreSQL). Następnie aplikacja jest wdrażana w celu wspierania podwójnego zapisu (zapisu zarówno do starych, jak i nowych struktur schematu) podczas odczytu z dowolnej z nich. Historyczne rekordy są następnie uzupełniane asynchronicznie w ograniczonych partiach w tle, aby zapobiec blokowaniu tabel lub wywoływaniu opóźnień replikacji. W fazie Zwinięcia, gdy spójność danych zostanie zweryfikowana, a odczyty zostaną całkowicie przełączone na nową strukturę, wdrożenie kodu usuwa odniesienia do starego schematu. Na koniec, migracja czyszcząca usuwa przestarzałe kolumny lub tabele. Bezpieczne wycofanie zmian (rollback) wymaga, aby każdy pośredni stan schematu pozostał w pełni kompatybilny zarówno z poprzednią, jak i nadchodzącą wersją aplikacji podczas stopniowych wdrożeń (rolling releases).

<?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
Odpowiedz na to pytanie z trenerem AI