15 vybraných dotazů k pohovoru pro PHP backend, seskupených podle úrovně seniority. Použijte je k zopakování základů, praktických kompromisů a uvažování na seniorské úrovni pro produkční prostředí.
Spustit AI pohovor PHPNení vyžadována platební karta. K dispozici je 1 bezplatná relace.
1Jak typový systém PHP rozlišuje slabou (weak) a striktní (strict) typizaci a co se změní, když je povoleno `declare(strict_types=1)`?
PHP je ve výchozím nastavení slabě/koercitivně typované pro deklarace skalárních typů: když funkce deklaruje skalární parametry nebo návratové typy, PHP může převést kompatibilní hodnoty, jako jsou číselné řetězce, na čísla namísto jejich odmítnutí. `declare(strict_types=1)` činí kontrolu skalárních typů striktní pro daný soubor, takže nekompatibilní skalární hodnoty nejsou převáděny a je vyhozena výjimka `TypeError`. Direktiva je omezena na soubor: pro argumenty uživatelem definovaných funkcí, soubor volajícího řídí striktnost; pro příkazy `return` řídí striktnost návratové hodnoty soubor, kde je funkce definována. Nezpůsobuje, že by PHP bylo globálně striktní, ani nevypíná všechny konverze v jazyce.
<?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
2Vysvětlete rozdíl mezi `==` a `===` v PHP, včetně úskalí s numerickými řetězci, poli, objekty a `in_array()`.
`==` provádí volné porovnání, takže PHP může před porovnáním „žonglovat“ s typy; `===` provádí striktní porovnání, které vyžaduje stejnou hodnotu i stejný typ bez koercice. Numerické řetězce jsou častým úskalím, protože mohou být ve volných porovnáních považovány za čísla. Pole a objekty mají svá vlastní pravidla porovnání: pole jsou porovnávána podle klíčů a hodnot, zatímco `==` u objektů porovnává objekty stejné třídy podle vlastností a `===` u objektů vyžaduje přesně stejnou instanci. Funkce `in_array()` ve výchozím nastavení používá volné porovnání, takže pokud se musí shodovat typ, předejte jako třetí argument `true`.
3Jak se liší `mixed`, `void` a `never` jako návratové typy v PHP a jaké záruky poskytují?
Jako návratové typy poskytují `mixed`, `void` a `never` různé záruky.
`mixed` znamená, že funkce může vrátit jakoukoli hodnotu, včetně `null`.
`void` znamená, že funkce není určena k vrácení smysluplné hodnoty; může použít `return;`, ale nemůže vrátit výraz.
`never` znamená, že funkce se normálně nevrátí volajícímu: musí vždy vyhodit výjimku, ukončit běh programu nebo jiným způsobem ukončit tok řízení. `never` je silnější než `void`, protože provádění nemůže pokračovat po normálním volání funkce 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';
4Co je `iterable` v PHP a jak se vztahuje k `array`, `Traversable`, `Iterator` a `Generator`?
V PHP je `iterable` typová deklarace znamenající „`array` nebo objekt implementující `Traversable`“. `Traversable` je základní marker rozhraní pro objekty, které lze použít s `foreach`; uživatelský kód (userland code) normálně implementuje `Iterator` nebo `IteratorAggregate`, které rozšiřují `Traversable`. `Generator` je objekt vrácený z generátorové funkce pomocí `yield`; implementuje `Iterator`, takže je také `Traversable` a je akceptován všude, kde je vyžadován `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());
5Jaká jsou pravidla viditelnosti `public`, `protected` a `private` v PHP a jak interagují s dědičností a traity?
V PHP jsou `public` členové dostupní odkudkoli, `protected` členové jsou dostupní z deklarující třídy a podtříd a `private` členové jsou dostupní pouze ze třídy, která je deklaruje. V dědičnosti mohou podtřídy používat zděděné `public` a `protected` členy, ale `private` členové rodičovské třídy nejsou dostupné jako zděděné API. Při přepisování metod potomek obecně nemůže snižovat viditelnost; například `public` metoda rodičovské třídy musí zůstat `public`. Traity jsou začleněny do používající třídy a členové traitů se řídí pravidly viditelnosti PHP. Viditelnost metod traitu lze také upravit nebo aliasovat pomocí `as` při importu 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
6Jak fungují traity v PHP a jak se řeší konflikty, když více traitů definuje stejnou metodu?
Traity jsou mechanismem PHP pro horizontální znovupoužitelnost kódu: třída může použít jeden nebo více traitů a členové traitu jsou začleněni do třídy. Traity nejsou samostatné třídy a nelze je přímo instanciovat. Pokud více traitů poskytuje metody se stejným názvem, PHP vyžaduje explicitní řešení konfliktů. Operátor `insteadof` vybírá, která metoda traitu se použije pro konfliktní název. Operátor `as` může vytvořit alias pro metodu traitu a také může změnit viditelnost importované metody. Metody třídy přepíší metody traitu a metody traitu mají přednost před zděděnými metodami rodičovské třídy.
<?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
7Co jsou to výčtové typy (enums) v PHP, včetně čistých (pure) a podložených (backed) výčtových typů, a jak zlepšují doménové modelování?
Výčtové typy (enums) v PHP definují uzavřenou množinu pojmenovaných případů pro daný typ, jako jsou stavy objednávek nebo uživatelské role. Čisté výčtové typy (pure enums) mají pouze pojmenované případy; podložené výčtové typy (backed enums) dávají každému případu unikátní řetězcovou nebo celočíselnou hodnotu, což je užitečné pro ukládání, API nebo interoperabilitu. Výčtové typy zlepšují doménové modelování tím, že nahrazují křehké raw řetězce, celá čísla nebo konstanty typově bezpečnými, samodokumentujícími hodnotami. Výčtové typy v PHP mohou mít také metody a implementovat rozhraní; všechny výčtové typy podporují výpis svých případů a podložené výčtové typy podporují převod na a z jejich skalárních hodnot.
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);
8Jak skalární, nullabilní, typové sjednocení (union), typový průnik (intersection) a DNF (Disjunctive Normal Form) typy vyjadřují kontrakty PHP API (Application Programming Interface) a kdy by se měl který používat?
Typové deklarace PHP popisují hodnoty, které API (Application Programming Interface) přijímá nebo vrací. Skalární typy jako `int`, `float`, `string` a `bool` se používají, když se očekává přesně jeden primitivní typ. Nullabilní typy, psané `?T` nebo `T|null`, umožňují buď hodnotu typu `T`, nebo `null`. Typové sjednocení (union types) jako `A|B` umožňuje jednu z několika alternativ. Typový průnik (intersection types) jako `A&B` vyžaduje, aby hodnota splňovala všechny uvedené typy, obvykle více rozhraní. Typy DNF (Disjunctive Normal Form) kombinují sjednocení a průniky, například `(A&B)|C`, k vyjádření složitějších kontraktů. Každý by měl být použit, když odpovídá skutečnému slibu API, čímž se předejde zbytečně širokým nebo matoucím typům.
<?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
}
9Jaký je rozdíl mezi `callable`, `Closure`, `first-class callables`, vyvolatelnými objekty (invokable objects) a `callable` poli v PHP?
`callable` je široký typ/kontrakt pro jakoukoli hodnotu, kterou PHP může vyvolat, jako je řetězec s názvem funkce, `Closure`, vyvolatelný objekt (invokable object) nebo pole reference na metodu, jako je `[$object, 'method']` nebo `[ClassName::class, 'method']`. `Closure` je konkrétní objekt anonymní funkce a je sama `callable`. Syntaxe prvotřídních vyvolatelných objektů (first-class callables), jako například `$obj->method(...)` nebo `strlen(...)`, vytváří `Closure` z `callable` výrazu. Vyvolatelný objekt je `callable`, protože definuje `__invoke()`. `Callable` pole jsou tradiční dvoupříručné (two-element) forma reference na metodu. Funkce `is_callable()` může být použita ke kontrole `callable` za běhu.
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;
}
10Jak PHP zpracovává přetečení celého čísla, přesnost čísel s plovoucí desetinnou čárkou a přesné desetinné výpočty?
Celočíselné typy v PHP mají velikost závislou na platformě, s limity vystavenými konstantami jako `PHP_INT_MAX`. PHP automaticky neudržuje nativní celočíselné výsledky jako celá čísla s libovolnou přesností po přetečení; přetékající celočíselná aritmetika se může stát `float` nebo jinak ztratit přesnou celočíselnou sémantiku. Typy `float` v PHP jsou binární hodnoty s plovoucí desetinnou čárkou, takže mnoho desetinných zlomků, jako například `0.1`, je pouze přibližných. Pro přesné desetinné výpočty nebo výpočty s penězi se vyhněte typům `float` a použijte přesný přístup, například menší celočíselné jednotky, kde je to vhodné, nebo nástroje pro desetinná čísla s pevnou/libovolnou přesností, jako je BCMath. Knihovna GMP je užitečná pro celočíselnou aritmetiku s libovolnou přesností.
11Vysvětlete PHP reference (`&`), kopírování při zápisu (copy-on-write) a jak mohou reference vytvářet neočekávané vedlejší efekty.
PHP proměnné jsou interně reprezentovány jako `zvals`. Pro hodnoty, jako jsou pole a řetězce, PHP používá kopírování při zápisu (copy-on-write): přiřazení `$b = $a` obvykle sdílí stejnou základní hodnotu, dokud není jedna z proměnných modifikována, v takovém případě ji PHP oddělí/zkopíruje. Reference vytvořená pomocí `&` dělá z proměnných aliasy ke stejnému kontejneru, takže změna jedné proměnné změní i druhou a může obejít chování oddělování, které lidé očekávají od běžného přiřazení. Předávání referencí (`passing by reference`) umožňuje funkci modifikovat proměnnou volajícího. Reference mohou způsobit překvapivé vedlejší efekty, zejména u `foreach ($array as &$value)`: proměnná cyklu zůstane referencí na poslední prvek i po dokončení cyklu, takže její pozdější opětovné použití může náhodně přepsat tento prvek, pokud ji `unset($value)` nezrušíte.
12Jak v PHP fungují handle k objektům, přiřazení, klonování (clone), metoda `__clone`, mělká kopie (shallow copy) a hluboká kopie (deep copy)?
V PHP proměnná objektu ukládá handle (odkaz) na objekt, nikoli celou kopii objektu. Přiřazení `$b = $a` zkopíruje handle, takže obě proměnné odkazují na tentýž objekt; změna objektu prostřednictvím kterékoli proměnné je viditelná i prostřednictvím té druhé, a to bez použití `&`. Příkaz `clone $a` vytvoří novou instanci objektu s mělkou kopií vlastností původního objektu. Pokud je vlastnost sama objektem, vlastnost klonovaného objektu stále odkazuje na tentýž vnořený objekt, pokud jej explicitně neklonujete. Metoda `__clone()` je volána na novém objektu po mělké kopii a slouží k resetování ID, odpojení zdrojů nebo provedení hluboké kopie vnořených objektů. Hluboká kopie znamená rekurzivní kopírování vnořených měnitelných objektů, které by neměly být sdíleny.
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;
13Jak byste řešili vyčerpání databázových připojení a navrhli správu připojení pro PHP-FPM (FastCGI Process Manager) a dlouhotrvající pracovníky?
Pro řešení vyčerpání připojení bych porovnal limity databázových připojení a aktuální relace s celkovou paralelností PHP: každý aktivní podřízený proces PHP-FPM může držet své vlastní databázové připojení a více aplikačních poolů, hostitelů, cron úloh, pracovníků front, migrací a nasazení násobí poptávku. Zkontroloval bych databázové pohledy relací, stavy připojení, relace v transakci, která nic nedělá (idle-in-transaction), pomalé dotazy, čekání na zámky, chybové protokoly, metriky poolu a nedávné změny provozu nebo počtu pracovníků. Pro PHP-FPM udržujte `max_children` a paralelnost na hostitele v rámci databázového rozpočtu, používejte krátké transakce/připojení s rozsahem požadavku, provádějte commit nebo rollback ihned a vyhýbejte se držení připojení během pomalých externích volání. Perzistentní připojení PDO (PHP Data Objects) mohou snížit režii připojení, ale mohou svázat mnoho nečinných připojení na proces FPM a zachovat stav relace, takže vyžadují opatrnost. Pooler nebo proxy, jako je PgBouncer, může multiplexovat mnoho PHP klientů na méně připojení k databázovému serveru, pokud je aplikace kompatibilní. Dlouhotrvající pracovníci potřebují explicitní správu životního cyklu připojení: kontroly stavu (health checks), opětovné připojení po časových limitech nečinnosti/restartech/přepnutí při selhání (failover)/rozvětvení (forks), resetování stavu relace a opakování pouze bezpečných nebo idempotentních úloh.
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.
14Jak byste v PHP řešili čtecí repliky, rozdělení čtení/zápisu (read/write splitting), zpoždění replikace (replication lag) a přepnutí primární databáze (failover)?
Zpracování čtecích replik a přepnutí na záložní systém (failover) v PHP vyžaduje rozdělení čtecího a zapisovacího provozu, zmírnění zpoždění replikace a řízení přerušení připojení během přepnutí. Na úrovni aplikace konfigurace databáze nebo middleware směruje čtecí dotazy (`SELECT`) na čtecí repliky a zapisovací dotazy (`INSERT`, `UPDATE`, `DELETE`) na primární instanci (alternativně spravováno pomocí databázových proxy, jako je ProxySQL nebo AWS RDS Proxy). Protože je replikace asynchronní, zpoždění replikace může způsobit zastaralá čtení. Pro zaručení konzistence 'read-your-writes' (čtení vlastních zápisů) by aplikace měla směřovat čtení v rámci aktivní transakce na primární instanci a dočasně svázat následující čtení s primární instancí pro konkrétní uživatelskou relaci nebo životní cyklus požadavku bezprostředně po zápisu. Během přepnutí primární instance moderní databázové clustery povýší repliku a aktualizují DNS/proxy koncové body. V PHP musí vrstva připojení zachytit ztrátu připojení a chyby pouze pro čtení, zahodit zastaralé PDO sockety a aplikovat řízené opakování s exponenciálním zpožděním (exponential backoff).
15Jak se bezpečně nasazují migrace databáze a změny schématu s nulovou nebo minimální odstávkou v PHP aplikacích?
Nasazení migrací databáze s nulovou odstávkou v PHP aplikacích vyžaduje oddělení vývoje schématu od nasazení kódu aplikace pomocí vzoru rozšiřování a stahování (Expand and Contract / Parallel Run) spolu s praktikami neblokujícího online DDL (Data Definition Language). Ve fázi rozšiřování (Expand) se aplikují zpětně kompatibilní změny schématu (např. přidání volitelných (nullable) sloupců, nových tabulek nebo souběžných indexů pomocí nástrojů jako `pt-online-schema-change`, `gh-ost` nebo nativní online DDL v MySQL/PostgreSQL). Aplikace je pak nasazena tak, aby podporovala duální zápis (dual-writing – zápis do starých i nových struktur schématu), zatímco čtení probíhá z obou. Historické záznamy jsou poté asynchronně doplňovány v omezených dávkách na pozadí, aby se zabránilo uzamčení tabulek nebo vyvolání zpoždění replikace (replication lag). Ve fázi stahování (Contract), jakmile je ověřena konzistence dat a čtení je plně přepnuto na novou strukturu, nasazení kódu odstraní odkazy na staré schéma. Nakonec čisticí migrace odstraní zastaralé sloupce nebo tabulky. Bezpečný rollback vyžaduje, aby každý mezilehlý stav schématu zůstal plně kompatibilní s předchozími i nadcházejícími verzemi aplikace během postupného nasazení (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