PHP interviewvoorbereiding

PHP interviewvragen

15 geselecteerde PHP backend interviewvragen gegroepeerd op senioriteitsniveau. Gebruik ze om basisprincipes, praktische afwegingen en het redeneren op senior-niveau voor productie te herzien.

Start een PHP AI InterviewGeen creditcard nodig. 1 gratis sessie beschikbaar.
Oefen technische interviews in het EngelsEen modus waarin anderstaligen kunnen oefenen met het afleggen van technische interviews.

Junior vragen

1Hoe onderscheidt PHP zwakke en strikte typering, en wat verandert declare(strict_types=1)?

PHP staat normaal compatibele scalaire conversies toe voor gedeclareerde parameter- en retourtypen, zoals een numerieke string voor een int-parameter. declare(strict_types=1) schakelt strikte scalaire controles in: een incompatibel argument of retourwaarde geeft TypeError, hoewel int nog steeds voor float wordt geaccepteerd. Voor argumenten van gebruikersfuncties bepaalt het aanroepende bestand de strengheid; het definiërende bestand bepaalt de retourcontroles. Aanroepen vanuit interne PHP-functies hebben eigen bijzonderheden en vallen niet simpelweg onder strict_types van de buitenste aanroeper. De richtlijn moet aan het begin van het PHP-bestand staan. Ze maakt PHP niet statisch getypeerd en schakelt operatorconversies niet in de hele taal uit.

// 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';
}
?>
Probeer deze vraag te beantwoorden met een AI-begeleider

2Leg het verschil uit tussen == en === in PHP, inclusief valkuilen met numerieke strings, arrays, objecten en in_array().

== vergelijkt los, met typeconversie waar van toepassing; === vereist overeenkomende typen en waarden zonder scalaire conversie. Numerieke strings kunnen numeriek worden vergeleken, dus vermijd losse vergelijking bij exacte typegevoelige identiteit. Arrays zijn met == gelijk wanneer ze volgens de vergelijkingsregels dezelfde sleutel/waarde-paren bevatten, ongeacht de volgorde. === vereist ook dezelfde volgorde en overeenkomende sleutel- en waardetypen. Object-== vergelijkt eigenschappen van objecten uit dezelfde klasse; === vereist dezelfde instantie. in_array vergelijkt standaard los; geef true als derde argument voor strikte controle. Losse getal/string-vergelijking veranderde in PHP 8, dus vertrouw niet op historische conversievoorbeelden zonder versieaanduiding.

<?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
Probeer deze vraag te beantwoorden met een AI-begeleider

3Hoe verschillen mixed, void en never als PHP-retourtypen en wat garanderen ze?

mixed staat elke retourwaarde toe, inclusief null. void verklaart dat een functie geen betekenisvolle waarde retourneert: ze mag het einde bereiken of return; gebruiken, maar geen expressie retourneren. Een normale aanroep van een void-functie evalueert naar null. never, beschikbaar sinds PHP 8.1, betekent dat de functie nooit normaal terugkeert naar de aanroeper. Ze kan een uitzondering werpen, met exit beëindigen of in een oneindige lus blijven; ze hoeft niet uiteindelijk het proces te beëindigen. Een never-functie mag geen return bevatten. Het verschil is dat void normaal kan eindigen waarna de aanroeper doorgaat, terwijl never geen normale voortzetting heeft.

<?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';
Probeer deze vraag te beantwoorden met een AI-begeleider

4Wat is iterable in PHP en hoe verhoudt het zich tot array, Traversable, Iterator en Generator?

iterable betekent array|Traversable. Generator implementeert Iterator, dat Traversable uitbreidt, en voldoet dus aan iterable. Gebruikersklassen bieden Traversable-gedrag via Iterator of IteratorAggregate, in plaats van Traversable direct in een concrete klasse te implementeren. Iterator biedt methoden die iteratie aansturen; IteratorAggregate levert een iterator. Een generatorfunctie met yield retourneert een Generator. Een belangrijk onderscheid: foreach kan ook zichtbare eigenschappen doorlopen van een gewoon object zonder Traversable, maar daardoor voldoet dat object niet aan een iterable-parameterdeclaratie.

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());
Probeer deze vraag te beantwoorden met een AI-begeleider

5Wat zijn de zichtbaarheidsregels voor public, protected en private in PHP en hoe werken ze met overerving en traits?

public-leden zijn toegankelijk overal waar het object of de klasse toegankelijk is. protected-leden zijn toegankelijk in de declarerende klasse en de gerelateerde boven-/subklassehiërarchie, niet vanuit willekeurige externe code. private-leden zijn toegankelijk binnen hun declarerende klasse; een subklasse kan een privé-lid van de ouder niet als geërfde interface gebruiken. Een gewone overschrijvende methode mag de geërfde zichtbaarheid niet beperken. Constructors hebben echter bijzondere overervingsregels en zijn geen onvoorwaardelijk voorbeeld van die regel. Trait-leden worden in de gebruikende klasse opgenomen en volgen haar zichtbaarheidsregels. as kan een alias maken, de zichtbaarheid van een trait-methode aanpassen of beide; een alias verwijdert de oorspronkelijke naam niet.

<?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
Probeer deze vraag te beantwoorden met een AI-begeleider

6Hoe werken traits in PHP en hoe worden conflicten opgelost wanneer meerdere traits dezelfde methode definiëren?

Traits bieden horizontaal codehergebruik: een klasse importeert leden met use en een trait kan niet zelfstandig worden geïnstantieerd. Een in de klasse gedeclareerde methode gaat vóór trait-methoden, die vóór geërfde oudermethoden gaan. Als meerdere traits dezelfde methode aanbieden en een klassemethode het conflict niet al oplost, kies je de implementatie met insteadof. Gebruik as voor een extra naam en eventueel andere zichtbaarheid. Een alias hernoemt of verwijdert de oorspronkelijke methode niet. Botsingen oplossen en de resulterende publieke interface duidelijk houden zijn aparte verantwoordelijkheden.

<?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
Probeer deze vraag te beantwoorden met een AI-begeleider

7Wat zijn PHP enums, inclusief pure en backed enums, en hoe verbeteren ze domeinmodellering?

PHP enums definiëren een gesloten verzameling van benoemde cases voor een type, zoals orderstatussen of gebruikersrollen. Pure enums hebben alleen benoemde cases; backed enums geven elke case een unieke string- of int-waarde, wat nuttig is voor opslag, API's of interoperabiliteit. Enums verbeteren de domeinmodellering door kwetsbare ruwe strings, integers of constanten te vervangen door typeveilige, zelfdocumenterende waarden. PHP enums kunnen ook methoden hebben en interfaces implementeren; alle enums ondersteunen het oplijsten van hun cases, en backed enums ondersteunen conversie naar en van hun scalaire waarden.

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);
Probeer deze vraag te beantwoorden met een AI-begeleider

Medior vragen

8Hoe drukken scalaire, nullable-, union-, intersection- en DNF-typen PHP-interfacecontracten uit en wanneer gebruik je ze?

PHP-declaraties beschrijven geaccepteerde en geretourneerde waarden. Scalaire declaraties zoals int, float, string en bool kiezen een waardecategorie; of compatibele scalaire conversie is toegestaan, hangt ook af van strict_types en de context. ?T of T|null staat null toe. Unions, beschikbaar sinds PHP 8.0, staan alternatieven toe zoals int|string. Zuivere intersections sinds PHP 8.1 vereisen dat een object aan alle vermelde klasse-/interfacetypen voldoet, zoals Cacheable&JsonSerializable; int&string is ongeldig. DNF-typen (disjunctieve normaalvorm), beschikbaar sinds PHP 8.2, combineren alternatieven en intersections tussen haakjes, zoals (Cacheable&JsonSerializable)|string. Kies het smalste begrijpelijke contract voor geldige invoer zonder onnodige alternatieven.

<?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
}
Probeer deze vraag te beantwoorden met een AI-begeleider

9Wat is het verschil tussen callable, Closure, first-class callables, aanroepbare objecten en callable-arrays in PHP?

callable beschrijft aanroepbare waarden, waaronder functienaamstrings, Closure-objecten, verwijzingen naar toegankelijke methoden en objecten met __invoke. Closure is een concreet aanroepbaar object uit een anonieme functie of een benoemde functie/methode die naar Closure is omgezet. First-class callable-syntax zoals strlen(...) of $object->method(...) maakt een Closure zonder het doel aan te roepen. [$object, 'method'] verwijst naar een toegankelijke instantiemethode; [ClassName::class, 'method'] vereist in modern PHP een geschikte toegankelijke statische methode. is_callable controleert aanroepbaarheid in de relevante scope. callable is een breed contract, Closure een specifieke representatie.

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;
}
Probeer deze vraag te beantwoorden met een AI-begeleider

10Hoe gaat PHP om met integer-overflow, de precisie van floating-pointgetallen en exacte decimaalberekeningen?

PHP integers zijn platform-afhankelijk van grootte, met limieten die worden weergegeven door constanten zoals `PHP_INT_MAX`. PHP behoudt niet automatisch native integer-resultaten als integers met willekeurige precisie na een overflow; overlopende integer-arithmetiek kan `float` worden of anderszins exacte integer-semantiek verliezen. PHP floats zijn binaire floating-pointwaarden, dus veel decimale breuken, zoals `0.1`, zijn benaderend. Voor exacte decimaal- of geldberekeningen, vermijd floats en gebruik een exacte aanpak zoals gehele getallen als 'kleinere eenheden' waar van toepassing, of hulpmiddelen voor vaste/willekeurige precisie-decimalen zoals BCMath. GMP is nuttig voor integer-arithmetiek met willekeurige precisie.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Probeer deze vraag te beantwoorden met een AI-begeleider

11Leg PHP-referenties (&), copy-on-write, en hoe referenties onverwachte neveneffecten kunnen creëren uit.

PHP-variabelen worden intern gerepresenteerd door zvals. Voor waarden zoals arrays en strings gebruikt PHP copy-on-write: het toewijzen van `$b = $a` deelt gewoonlijk dezelfde onderliggende waarde totdat één variabele wordt gewijzigd, waarna PHP deze scheidt/kopieert. Een referentie gemaakt met `&` maakt variabelen aliassen naar dezelfde container, dus het wijzigen van de ene wijzigt de andere en kan het scheidingsgedrag omzeilen dat men verwacht bij normale toewijzing. Doorgeven via referentie stelt een functie in staat de variabele van de aanroeper te wijzigen. Referenties kunnen verrassende neveneffecten veroorzaken, vooral bij `foreach ($array as &$value)`: de lusvariabele blijft een referentie naar het laatste element na de lus, dus het later hergebruiken ervan kan dat element per ongeluk overschrijven, tenzij u `unset($value)` gebruikt.

$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);
Probeer deze vraag te beantwoorden met een AI-begeleider

12Hoe werken objectverwijzingen, toewijzing, `clone`, `__clone`, ondiepe kopieën en diepe kopieën in PHP?

In PHP slaat een objectvariabele een verwijzing naar een object op, niet een volledige kopie van het object. Wanneer `$b = $a` wordt toegewezen, wordt de verwijzing gekopieerd, zodat beide variabelen naar hetzelfde object verwijzen; wijzigingen aan het object via de ene variabele zijn zichtbaar via de andere, zonder gebruik te maken van `&`. `clone $a` creëert een nieuwe objectinstantie met een ondiepe kopie van de eigenschappen van het oorspronkelijke object. Als een eigenschap zelf een object is, wijst de eigenschap van het gekloonde object nog steeds naar hetzelfde geneste object, tenzij je het expliciet kloont. `__clone()` wordt aangeroepen op het nieuwe object na de ondiepe kopie en is de plek om ID's te resetten, resources los te koppelen of een diepe kopie van geneste objecten uit te voeren. Een diepe kopie betekent het recursief kopiëren van de geneste veranderlijke objecten die niet gedeeld moeten worden.

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;
Probeer deze vraag te beantwoorden met een AI-begeleider

Senior vragen

13Hoe zou u problemen met uitputting van databaseverbindingen oplossen en verbindingbeheer ontwerpen voor PHP-FPM en langlopende workers?

Om problemen met verbindinguitputting op te lossen, zou ik de limieten voor databaseverbindingen en de huidige sessies vergelijken met de totale PHP-concurrentie: elk actief PHP-FPM-childproces kan zijn eigen databaseverbinding vasthouden, en meerdere applicatiepools, hosts, cronjobs, wachtrij-workers, migraties en deployments vermenigvuldigen de vraag. Ik zou database-sessieoverzichten, verbindingsstatussen, inactieve-in-transactie-sessies, trage queries, vergrendelingswachten, foutlogboeken, poolmetrieken en recente verkeers- of worker-aantalwijzigingen inspecteren. Voor PHP-FPM: houd `max_children` en de concurrentie per host binnen het databasebudget, gebruik korte, aanvraag-specifieke transacties/verbindingen, commit of draai snel terug, en vermijd het vasthouden van verbindingen tijdens trage externe oproepen. Persistente PDO-verbindingen kunnen de verbindingsoverhead verminderen, maar kunnen veel inactieve verbindingen per FPM-proces vasthouden en de sessiestatus behouden, dus ze vereisen voorzichtigheid. Een pooler of proxy zoals PgBouncer kan meerdere PHP-clients multiplexen naar minder databaseserververbindingen wanneer de applicatie compatibel is. Langlopende workers hebben expliciete afhandeling van de verbindingslevenscyclus nodig: gezondheidscontroles, opnieuw verbinden na inactiviteitstime-outs/herstarts/failover/forks, sessiestatus resetten en alleen veilige of idempotente taken opnieuw proberen.

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.
Probeer deze vraag te beantwoorden met een AI-begeleider

14Hoe behandel je leesreplica's, scheiding van lezen en schrijven, replicatievertraging en failover van de primaire database in PHP?

Scheid primaire en replicaverbindingen volgens de betekenis van de bewerking, niet enkel of SQL met SELECT begint: vergrendelende leesacties, schrijfacties en transacties die primaire consistentie vereisen moeten de primaire database gebruiken. Alleen-lezen-transacties mogen een geschikte replica gebruiken als hun consistentie-eisen dat toestaan. Asynchrone replicatie kan achterlopen. Laravels sticky-instelling stuurt leesacties na een schrijfactie tijdens het huidige verzoek naar de schrijfverbinding; een vaste bindingsperiode tussen verzoeken beperkt alleen het probleem en garandeert niet onvoorwaardelijk het lezen van eigen schrijfacties. Lees daarvoor van de primaire database of wacht totdat een replica de vereiste schrijfpositie heeft toegepast, met timeout en terugval op de primaire database. Proxy's verschillen: RDS Proxy heeft aparte lees-/schrijfendpoints en verdeelt niet algemeen automatisch elke SQL-instructie. Verwerp verouderde verbindingen bij failover en verbind opnieuw met begrensde toenemende wachttijd. Een verloren commit-antwoord laat het resultaat onzeker: herhaal niet-idempotente schrijfacties niet blind; gebruik idempotentiesleutels of stel eerst het resultaat vast.

<?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',
    ],
];
Probeer deze vraag te beantwoorden met een AI-begeleider

15Hoe rol je databasemigraties en schemawijzigingen veilig uit in PHP-applicaties zonder of met minimale uitval?

Gebruik uitbreiding gevolgd door verwijdering van oude structuren, die oude en nieuwe applicatieversies verdragen tijdens een geleidelijke uitrol. Voeg eerst compatibele structuren toe en rol vervolgens code uit die beide representaties gebruikt. Houd eventueel dubbel schrijven consistent en ontwerp hervatbare, snelheidsbegrensde aanvulling van historische gegevens die nieuwere schrijfacties niet overschrijft. Valideer gekopieerde gegevens en schakel leesacties om voordat oude codepaden verdwijnen. Online DDL (datadefinitietaal) vermindert bepaalde blokkeringsrisico's maar is niet altijd vergrendelingsvrij: onderzoek database- en versiespecifieke locks, transactiebeperkingen, resourcegebruik en replicatie-effecten. PostgreSQL CREATE INDEX CONCURRENTLY heeft bijvoorbeeld beperkingen en kan na een fout een ongeldige index achterlaten. Verwijder oude kolommen pas wanneer oude applicatie-instanties verdwenen zijn en de vereiste terugrolperiode voorbij is. Na destructieve verwijdering is terugrollen naar code die verwijderde gegevens nodig heeft niet automatisch veilig.

<?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
Probeer deze vraag te beantwoorden met een AI-begeleider