Skip to content

Cookies on Jobverse

We use cookies that are needed to sign you in and keep the site working. With your permission we’d also use an analytics cookie to count job views, and Google advertising cookies to show ads. The site works the same whatever you choose. Read the Cookie Policy.

Jobverse
Sign in

PHP Backend Developer Top 100 Interview Questions

100 PHP backend developer interview questions with structured answers, comparison tables, flow diagrams, worked calculations and runnable code. Covers modern typed PHP, the request lifecycle, HTTP and APIs, security, PDO and databases, caching, queues, testing, performance, deployment and system design, from the basics to production incidents.

100 questions

Questions 61-80 of 100

  1. Q61.How do you replace dependencies with fakes in PHP tests?

    Depend on interfaces for anything external, such as mailers, payment gateways, clocks and repositories, and write simple fake implementations for tests that record what happened and return chosen results.

    Kinds of fakes:

    • In-memory implementations, such as a repository backed by an array.
    • Stubs that return fixed data or throw a chosen exception.
    • Spies that record calls for later assertions.

    Guidelines:

    1. Fake only what you do not own or what is slow; use real domain objects.
    2. Check outcomes rather than every call, so tests survive refactoring.
    3. Share fakes across tests, keeping them small and correct.
    4. Mocking libraries help for large interfaces, but hand-written fakes are often clearer.
    5. Contract tests run the same checks against the fake and the real implementation, so they stay consistent.

    Example

    A fake mailer records messages and can simulate a failure, so both paths are checked without sending email.

    <?php
    declare(strict_types=1);
    
    interface Mailer
    {
        public function send(string $to, string $subject): void;
    }
    
    final class FakeMailer implements Mailer
    {
        public array $sent = [];
        public bool $failNext = false;
    
        public function send(string $to, string $subject): void
        {
            if ($this->failNext) {
                $this->failNext = false;
                throw new RuntimeException('mail server unavailable');
            }
            $this->sent[] = "$to: $subject";
        }
    }
    
    final class Signup
    {
        public function __construct(private Mailer $mailer) {}
    
        public function register(string $email): string
        {
            try {
                $this->mailer->send($email, 'Welcome');
                return 'registered';
            } catch (RuntimeException) {
                return 'registered, welcome email queued for retry';
            }
        }
    }
    
    $mailer = new FakeMailer();
    $signup = new Signup($mailer);
    echo $signup->register('a@example.com'), "\n"; // registered
    $mailer->failNext = true;
    echo $signup->register('b@example.com'), "\n"; // registered, welcome email queued for retry
    echo json_encode($mailer->sent), "\n";         // ["a@example.com: Welcome"]
  2. Q62.How do you test HTTP endpoints in a PHP application?

    Build requests in memory and pass them through the application's front controller or kernel, then inspect the response object. This exercises routing, middleware, validation and serialization without starting a web server.

    How to set it up:

    1. Design the application around request and response objects, so the entry point is a function that takes a request and returns a response.
    2. Create test requests with method, path, headers and body.
    3. Replace external services with fakes through the container configuration for the test environment.
    4. Assert on status, headers and the decoded JSON body.

    What to cover:

    • Success responses for the main use cases.
    • Validation errors with their field messages.
    • Authentication and authorization failures, including access to another user's resources.
    • Error handling, such as a 404 for missing records and a generic 500 without internal details.

    Database: run these tests against a real database engine, reset between tests with transactions that are rolled back, or with a freshly migrated schema.

    A few end-to-end tests through a real web server catch configuration problems, such as rewrite rules and headers, that in-memory tests cannot.

  3. Q63.How do strict types and static analysis improve PHP code quality?

    Strict types make PHP reject wrong scalar types at run time, while static analysis tools read the code without running it and report type errors, impossible conditions, undefined methods and nullable values used unsafely. Together they catch many bugs before tests even run.

    What static analysis finds:

    1. Calling methods on values that may be null.
    2. Passing the wrong types to functions, including array shapes described in documentation types.
    3. Dead code and conditions that are always true or false.
    4. Missing return statements and unhandled enum cases.
    5. Unsafe dynamic features, such as variable method names.

    Adopting it in an existing codebase:

    • Start at a low strictness level and raise it gradually.
    • Record existing errors in a baseline file, so new code must be clean while old issues are fixed over time.
    • Run it in continuous integration and fail the build on new errors.

    Writing analyzable code:

    • Type everything natively, and use documentation types for details the language cannot describe, such as list<Order> or array shapes.
    • Prefer small value objects to deeply nested arrays.
    • Avoid magic methods and dynamic property access.
  4. Q64.How do you profile a slow PHP request?

    Measure where the time goes before changing code. Most slow PHP requests spend their time waiting on databases, caches or external services, not in PHP itself, so start with the request's timeline.

    1. Check the request's breakdown: total time, time in database queries, time in outgoing calls and time in PHP, from tracing or application monitoring.
    2. Count queries: many small queries point to N+1 problems; one slow query points to missing indexes or heavy joins.
    3. Profile PHP execution with a profiler extension in a test environment, or a sampling profiler in production, to find expensive functions.
    4. Look at memory: peak memory and large allocations, such as loading entire tables or files.
    5. Check configuration: bytecode cache enabled and large enough, debug extensions disabled and cached configuration and routes in production.

    Typical fixes, by impact:

    • Add or adjust database indexes and remove N+1 queries.
    • Cache expensive results.
    • Move slow external calls to background jobs.
    • Stream instead of loading large data.
    • Optimize hot PHP code only when profiling shows it matters.

    Compare before and after with the same request and data, and keep a latency budget per endpoint.

  5. Q65.How do you send emails and notifications reliably from a PHP application?

    Never send emails or other notifications synchronously inside the web request. Record the notification, push a job to a queue and let a background worker deliver it with retries, so slow or failing mail services do not slow down or break user requests.

    A reliable flow:

    1. Save the business change and the notification intent together, in the same transaction, for example in an outbox table.
    2. A worker delivers the message through the provider's API or a mail server, with timeouts.
    3. Retries with backoff for temporary failures, and a dead-letter store after the last attempt.
    4. Idempotency: a message ID prevents sending the same notification twice when a worker retries after a crash.

    Content and compliance:

    • Templates with escaped variables, and plain-text alternatives for email.
    • Unsubscribe links and preferences for marketing messages, respected by the sending code.
    • Sender authentication records configured for your domain, so messages are not marked as spam.
    • No sensitive data in notifications, such as full account numbers.

    Monitoring: track queued, sent, failed and bounced messages, and alert when the queue backs up or the failure rate rises.

  6. Q66.How do you implement search in a PHP application?

    Choose the search approach by data size and the quality users expect. Simple filters work with indexed SQL queries, text search needs full-text indexes and large catalogs with ranking and typo tolerance need a dedicated search engine kept in sync with the database.

    Options:

    1. Exact filters on indexed columns, such as category, price range and status, built from an allowlist of fields and bound parameters.
    2. Prefix searches such as name LIKE :prefix with 'lamp%' can use an index; a leading wildcard, '%lamp%', scans the whole table.
    3. Full-text indexes in the database provide word matching and basic ranking for moderate data sizes.
    4. A dedicated search engine for relevance ranking, typo tolerance, facets and very large data, updated from database changes through a queue.

    Implementation practices:

    • Escape % and _ in user input used in LIKE patterns.
    • Paginate results and cap page sizes.
    • Normalize text, such as case and accents, consistently for indexing and querying.
    • Keep the search index eventually consistent, with a background reindex job and monitoring of how far it lags behind the database.
    • Log queries without personal data to learn what users look for and which searches return nothing.
  7. Q67.How do you process large files in PHP without running out of memory?

    Read the file incrementally, a line or a chunk at a time, process each piece and discard it. Functions such as file and file_get_contents load the whole file into memory, which fails for large files and wastes memory for medium ones.

    Techniques:

    1. SplFileObject or fopen with fgets to read line by line.
    2. Generators that yield parsed records, so processing code works on one record at a time.
    3. fgetcsv for CSV files, which handles quoted fields correctly.
    4. Batch writes: collect a few hundred records, insert them in one transaction, then continue.
    5. Stream filters for compressed files, decompressing as you read.

    For long-running batch scripts:

    • Release references to processed data, so memory can be reused.
    • Watch memory with memory_get_usage during development.
    • Make the job resumable, by recording the last processed line or ID.
    • Raise the time limit for command-line scripts, not for web requests.

    Uploading large files through a web request ties up a worker; process them in a background job instead.

    Example

    A generator over SplFileObject reads one line at a time, so memory stays flat however large the file grows.

    <?php
    declare(strict_types=1);
    
    function readLines(string $path): Generator
    {
        $file = new SplFileObject($path);
        $file->setFlags(SplFileObject::DROP_NEW_LINE | SplFileObject::SKIP_EMPTY | SplFileObject::READ_AHEAD);
        foreach ($file as $line) {
            yield $line;
        }
    }
    
    function summarize(string $path): string
    {
        $count = $errors = 0;
        foreach (readLines($path) as $line) {
            [, , $status] = explode(' ', $line);
            $count++;
            if ((int) $status >= 500) {
                $errors++;
            }
        }
        return "$count requests, $errors with server errors";
    }
    
    $path = tempnam(sys_get_temp_dir(), 'log');
    file_put_contents($path, "GET /jobs 200 120\nGET /jobs 500 900\nPOST /apply 201 300\n\nGET /jobs 200 80\n");
    echo summarize($path), "\n"; // 4 requests, 1 with server errors
    unlink($path);
  8. Q68.How do you handle concurrency problems such as double form submissions in PHP?

    PHP workers handle requests in parallel, so two identical requests, from a double click, a retry or a malicious client, can run at the same time. Protect operations that must happen once with database constraints and idempotency keys, not with checks in PHP code alone.

    Why a simple check fails: "if no order exists, create one" runs in two requests at once, both see no order and both insert. Only the database can make such operations atomic.

    Techniques:

    1. Unique constraints: a unique index on an idempotency key, or on natural keys such as user plus coupon, makes the second insert fail or do nothing.
    2. Idempotency keys: the form or client sends a unique key with the request; the server stores it with the result and returns the stored result on repeats.
    3. Atomic updates: UPDATE stock SET quantity = quantity - 1 WHERE id = ? AND quantity > 0, then check the affected row count.
    4. Row locks with SELECT ... FOR UPDATE inside a transaction, for read-then-write logic.
    5. User interface help, such as disabling the button after submission, which reduces but never prevents duplicates.

    Example

    The second submission with the same key is ignored by the database, so only one order exists.

    <?php
    declare(strict_types=1);
    
    $db = new PDO(getenv('DB_DSN'), options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $db->exec('CREATE TABLE orders (id INTEGER PRIMARY KEY, idempotency_key TEXT NOT NULL UNIQUE, total INTEGER NOT NULL)');
    
    function placeOrder(PDO $db, string $key, int $total): string
    {
        $stmt = $db->prepare(
            'INSERT INTO orders (idempotency_key, total) VALUES (:key, :total) ON CONFLICT (idempotency_key) DO NOTHING'
        );
        $stmt->execute(['key' => $key, 'total' => $total]);
        return $stmt->rowCount() === 0 ? 'duplicate ignored' : 'order ' . $db->lastInsertId() . ' created';
    }
    
    echo placeOrder($db, 'form-7f3a', 1250), "\n";                        // order 1 created
    echo placeOrder($db, 'form-7f3a', 1250), "\n";                        // duplicate ignored
    echo $db->query('SELECT COUNT(*) FROM orders')->fetchColumn(), "\n"; // 1
  9. Q69.Which SPL data structures are useful in PHP backend code?

    The Standard PHP Library provides data structures with clearer semantics, and sometimes better performance, than general-purpose arrays for specific jobs.

    Useful structures:

    • SplQueue: first-in, first-out processing, such as work items in a batch script.
    • SplStack: last-in, first-out, such as undo history or depth-first traversal.
    • SplPriorityQueue: returns the highest-priority item first, useful for scheduling.
    • SplMinHeap and SplMaxHeap: keep the smallest or largest item ready, for example for top-k problems.
    • SplObjectStorage: a map or set keyed by object identity, useful for tracking objects already visited.
    • SplFixedArray: a fixed-size, integer-indexed array that uses less memory than a regular array for large numeric lists.

    Also in the SPL: iterators, such as LimitIterator and CallbackFilterIterator, and SplFileObject for reading files.

    When to use them: when a structure's behavior makes intent clearer, or when profiling shows arrays are wasteful. For most everyday data, arrays and value objects remain the simplest choice.

    Example

    Each structure does one job: queue order, priority order, identity checks, fixed size and smallest-first extraction.

    <?php
    declare(strict_types=1);
    
    $queue = new SplQueue();
    $queue->enqueue('email');
    $queue->enqueue('sms');
    echo $queue->dequeue(), " ", count($queue), "\n";
    
    $tasks = new SplPriorityQueue();
    $tasks->insert('low', 1);
    $tasks->insert('urgent', 10);
    $tasks->insert('normal', 5);
    echo $tasks->extract(), "\n";
    
    $seen = new SplObjectStorage();
    $order = new stdClass();
    $seen->offsetSet($order);
    var_dump($seen->offsetExists($order), $seen->offsetExists(new stdClass()));
    
    $heap = new SplMinHeap();
    foreach ([5, 1, 3] as $n) {
        $heap->insert($n);
    }
    echo $heap->extract(), ",", $heap->extract(), "\n";
    // Output:
    // email 1
    // urgent
    // bool(true)
    // bool(false)
    // 1,3
  10. Q70.How do you size the PHP process manager for production?

    The number of PHP workers sets how many requests a server handles at once. Too few workers make requests queue; too many exhaust memory and overload the database. Size the pool from memory per worker and from the database's connection limit.

    Key settings:

    • Process management mode: static, a fixed number of workers, is predictable for dedicated servers; dynamic and on-demand modes save memory on shared or quiet servers.
    • Maximum children: the hard cap on workers.
    • Maximum requests per worker: restarts each worker after a number of requests, limiting the effect of leaks in extensions or code.
    • Request timeouts: stop requests that run far too long, and log slow ones.

    How to calculate:

    1. Measure the average and peak memory of a worker under realistic traffic.
    2. Divide the memory available for PHP by the peak memory per worker.
    3. Check the database: workers across all servers must not exceed what the database can handle.
    4. Load test, watching for queueing in the process manager's status page.

    Example

    A server has 8 GB of memory, with 2 GB reserved for the operating system and other processes; each PHP worker peaks at 80 MB.

    1. Memory for PHP: about 6,000 MB.
    2. Maximum workers: 6,000 / 80 = 75.
    3. Four such servers give 4 × 75 = 300 workers, and the database allows 400 connections, leaving room for background workers and administration.
    4. If requests average 120 ms, each server can serve about 75 / 0.12 ≈ 625 requests per second before queueing begins.
  11. Q71.How do you deploy a PHP application with zero downtime?

    Build each release into its own directory, prepare it completely and then switch traffic to it in one atomic step, such as changing a symbolic link or replacing containers behind a load balancer. Requests in progress finish on the old release while new requests go to the new one.

    flowchart LR
      A["Build artifact: code and dependencies"] --> B["Upload to new release directory"]
      B --> C["Warm caches, run safe migrations"]
      C --> D["Switch the current link atomically"]
      D --> E["Reload PHP workers gracefully"]
      E --> F["Health checks pass"]
      F --> G["Keep previous release for rollback"]

    Details that matter:

    1. Build once: install dependencies, generate the optimized autoloader and cached configuration in the pipeline, not on production servers.
    2. Bytecode cache: reload workers gracefully, or reset the cache, after switching, so old compiled code is not served from shared memory.
    3. Compatible migrations: schema changes must work with both old and new code during the switch.
    4. Shared state: uploads, sessions and logs live outside release directories.
    5. Rollback: switch the link back, which is fast because the previous release is still there.

    With containers, the same principles apply: build an image, roll it out gradually with readiness checks and keep the previous image ready.

  12. Q72.How do you package a PHP application in a container?

    Build an image that contains the PHP runtime, the extensions you need and your application with production dependencies, then run it as a non-root user with configuration from the environment.

    Image design:

    1. Multi-stage build: install dependencies with development tools in one stage, then copy only the application and production dependencies into the final image.
    2. Install only needed extensions, keeping the image small and its attack surface low.
    3. Bake in production settings: bytecode cache enabled with timestamp checks off, errors logged rather than displayed and sensible memory limits.
    4. Generate optimized autoloading and cached configuration at build time.
    5. Non-root user and a read-only file system where possible, with writable directories only for temporary files.

    Running the web tier:

    • Run the PHP process manager and the web server in separate containers or processes, or use an image that combines them, with clear health checks.
    • Send logs to standard output and standard error.
    • Size worker counts to the container's memory limit.

    Background workers use the same image with a different command, so code and dependencies always match.

  13. Q73.How do you upgrade a PHP application to a new PHP version safely?

    Read the migration notes, find incompatible code with automated tools, run the full test suite on the new version and roll out gradually. Each PHP version deprecates features before removing them, so treating deprecation warnings as work items makes upgrades smooth.

    1. Check support dates: each PHP version gets active support and then security fixes for a limited time; plan upgrades before support ends.
    2. Read the migration guide for each version you pass through, especially removed features and changed behavior.
    3. Run tests with deprecations reported, and fix them in the current version first.
    4. Use static analysis and automated refactoring tools to find and rewrite incompatible code at scale.
    5. Check dependencies and extensions: every package and extension must support the new version.
    6. Test in a production-like environment, including background workers and command-line scripts.
    7. Roll out gradually: a canary server or a share of containers, watching errors and performance.

    Benefits: each new version brings performance improvements, language features and security fixes, so regular upgrades pay off.

  14. Q74.How do you secure a PHP application end to end?

    Combine safe defaults in configuration, careful handling of input and output, strong authentication and sessions, protected secrets and a maintained dependency set.

    Configuration:

    • Errors logged, never displayed, in production.
    • Only the public directory exposed by the web server; configuration, source and uploads outside it.
    • Dangerous features disabled where possible, such as remote file inclusion.

    Input and output:

    1. Validate every input and convert it to typed values.
    2. Use prepared statements for all queries.
    3. Escape output for its context, with templates that escape by default.
    4. Protect forms against cross-site request forgery.
    5. Never pass user input to include, eval, shell commands or unserialize.

    Authentication and sessions:

    • Hash passwords with password_hash, regenerate session IDs on login and set secure cookie flags.
    • Rate-limit login and other sensitive endpoints.

    Secrets and dependencies:

    • Secrets from the environment or a secret store, never committed.
    • Audit dependencies for vulnerabilities in continuous integration, and update PHP and packages regularly.

    Monitoring: log security events, such as failed logins and authorization failures, and alert on unusual patterns.

  15. Q75.How do you handle money and precise decimal values in PHP?

    Never use floats for money: binary floating point cannot represent most decimal fractions exactly, so 0.1 + 0.2 is not 0.3, and rounding errors accumulate. Store amounts as integers in the smallest currency unit, or use arbitrary-precision decimal arithmetic.

    Two safe approaches:

    1. Integers in minor units, such as paise or cents: 19.99 becomes 1999. Addition and multiplication by whole quantities are exact; divisions, such as tax and discounts, need an explicit rounding rule.
    2. Decimal strings with the arbitrary-precision math extension, such as bcadd and bcmul, with a fixed scale, when calculations involve many fractional steps.

    Good practices:

    • Keep the currency with the amount in a value object, and refuse to add different currencies.
    • Decide rounding rules explicitly, such as half up at the end of a calculation, and apply them in one place.
    • Store integers or exact decimal columns in the database, never floating-point columns.
    • Format only for display, with the right locale, separators and currency symbol.
    • Allocate remainders when splitting amounts, so the parts always add up to the total.

    Example

    Floats miss exact equality, while integer minor units and decimal strings give exact results.

    <?php
    declare(strict_types=1);
    
    echo (0.1 + 0.2 === 0.3) ? "equal\n" : "not equal\n"; // not equal
    
    $priceMinor = 1999;
    $quantity = 3;
    $taxBasisPoints = 1800;
    $subtotal = $priceMinor * $quantity;
    $tax = intdiv($subtotal * $taxBasisPoints + 5000, 10000);
    echo $subtotal, " + ", $tax, " = ", $subtotal + $tax, "\n"; // 5997 + 1079 = 7076
    
    echo bcadd('0.1', '0.2', 2), "\n";  // 0.30
    echo bcmul('19.99', '3', 2), "\n";  // 59.97

    Adding 5,000 before dividing by 10,000 rounds half up, so the tax of 1,079.46 becomes 1,079.

  16. Q76.A PHP site becomes slow under load and requests time out, but the servers are not short of processor time. What is happening?

    Most likely every PHP worker is busy waiting, usually on a slow database query or an external API, so new requests queue in the process manager until they time out. Each worker serves one request at a time, so slow waits directly reduce how many requests a server can handle.

    How to confirm:

    1. The process manager's status page shows all workers active and a growing listen queue.
    2. Slow request logs show where workers are stuck, often in database or HTTP client calls.
    3. Request traces show long waits on one dependency.

    Fixes:

    • Set timeouts on every external call, so a slow dependency cannot hold workers for long.
    • Move slow work to queues, such as emails, exports and partner API calls.
    • Fix slow queries with indexes or caching.
    • Cache responses that many users request.
    • Add workers or servers only after the waits are reduced, and only within memory and database connection limits.

    Watch for retries: client retries during a slowdown add more requests to an already full queue.

    Example

    A server runs 75 workers, and requests normally take 120 ms.

    1. Normal capacity: 75 / 0.12 = 625 requests per second.
    2. A partner API slows down, and requests that call it now take 2 seconds.
    3. If half the traffic calls that API, the average request time rises to about 0.5 × 0.12 + 0.5 × 2 = 1.06 seconds, and capacity falls to about 75 / 1.06 ≈ 71 requests per second.
    4. A 300 ms timeout with a cached fallback caps the slow requests, restoring capacity to roughly 75 / 0.21 ≈ 357 requests per second even while the partner is degraded.
  17. Q77.A PHP batch script fails with "Allowed memory size exhausted". How do you fix it?

    The script holds too much data in memory at once, usually because it loads an entire result set or file before processing it. Raising memory_limit only postpones the failure until the data grows again; the real fix is to process data incrementally.

    Common causes:

    1. fetchAll on a large query, building an array of every row.
    2. Buffered query results, where the database driver copies the whole result into PHP memory, even when iterating row by row.
    3. Reading whole files with file or file_get_contents.
    4. Accumulating results in arrays, such as collecting all output before writing it.
    5. Object mappers that keep every loaded entity tracked in an identity map.

    Fixes:

    • Iterate statements row by row, and use unbuffered queries where the driver supports them.
    • Process in chunks by primary key: WHERE id > :last ORDER BY id LIMIT 1000.
    • Stream output to files or the response as you go.
    • Clear or detach tracked entities after each chunk when using a mapper.
    • Use generators to keep processing code simple while data flows through.

    Example

    A nightly export loads 2 million rows with fetchAll, each row costing about 400 bytes as a PHP array, with a 512 MB memory limit.

    1. Memory needed: 2,000,000 × 400 bytes ≈ 800 MB, well above the limit.
    2. Reading in chunks of 1,000 rows by primary key needs about 1,000 × 400 bytes = 400 KB per chunk.
    3. Writing each chunk to the output file immediately keeps peak memory near a few megabytes.
    4. The script now handles any table size, and runtime even improves because the database returns small, indexed ranges.
  18. Q78.Users behind a load balancer are logged out randomly. What is the likely cause in a PHP application?

    Sessions are probably stored on each server's local disk, the default file-based handler. When the load balancer sends a user's next request to a different server, that server has no session file for the user's session ID, so the user appears logged out.

    Other causes to check:

    • Different session settings across servers, such as the cookie name, path or domain.
    • Aggressive garbage collection of session files on one server.
    • Cookie flags: a Secure cookie on a site partly served over plain HTTP, or a SameSite setting that blocks the cookie in a cross-site flow.
    • Clock differences between servers that affect expiry.

    Fixes:

    1. Store sessions centrally, in a shared database or cache that all servers use.
    2. Use stateless tokens for APIs, so no server-side session is needed.
    3. Sticky sessions at the load balancer are a stopgap, but they break when a server is removed and distribute load unevenly.
    4. Make settings identical across servers through shared configuration.

    After moving sessions, test logins while repeatedly routing requests to different servers.

  19. Q79.After a deployment, some users see errors or old behavior for several minutes. What is going wrong?

    The servers are probably running a mix of old and new code. Common causes are a bytecode cache that still holds old compiled files, a deployment that copies files in place while requests are running or servers that were updated at different times.

    Typical causes:

    1. Stale bytecode cache: with timestamp checks disabled for performance, PHP keeps serving the old compiled version until the cache is reset or workers reload.
    2. Copying files over a live release: a request can load some new files and some old ones, causing missing classes or method errors.
    3. Cached configuration or routes generated for the old release.
    4. Rolling deployments without compatibility: old and new code running at once against a database schema only one of them supports.

    Fixes:

    • Deploy to a new release directory and switch a link atomically.
    • Reload PHP workers gracefully, or reset the bytecode cache, right after switching.
    • Rebuild caches for configuration and routes as part of the build, not after switching.
    • Make releases compatible with the previous version's database schema and queued jobs.
    • Check versions: expose the release identifier in a health endpoint and confirm every server reports the new one.
  20. Q80.A payment provider sends the same webhook twice, and orders are fulfilled twice. How do you fix it?

    Webhooks are delivered at least once, so duplicates are normal: providers retry when your endpoint is slow or returns an error, and sometimes even when it succeeded. The handler must be idempotent, recording each event ID and ignoring events it has already processed.

    A safe webhook handler:

    1. Verify the signature with the provider's shared secret and hash_equals, and reject unsigned or stale events.
    2. Record the event ID in a table with a unique constraint, in the same transaction as the business change.
    3. Skip duplicates when the insert finds an existing ID.
    4. Respond quickly: return success after recording the event, and do slow work in a background job.
    5. Handle out-of-order events, for example by checking the order's current state before applying a change.

    Why the database matters: checking "already processed?" in PHP code is not enough, because two deliveries can arrive at the same moment on different workers; only a unique constraint makes the check atomic.

    Example

    The second delivery of the same event is detected by its ID, so only one shipment exists.

    <?php
    declare(strict_types=1);
    
    $db = new PDO(getenv('DB_DSN'), options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $db->exec('CREATE TABLE processed_events (event_id TEXT PRIMARY KEY)');
    $db->exec('CREATE TABLE shipments (order_id TEXT PRIMARY KEY)');
    
    function handleWebhook(PDO $db, array $event): string
    {
        $db->beginTransaction();
        try {
            $claim = $db->prepare('INSERT INTO processed_events (event_id) VALUES (?) ON CONFLICT (event_id) DO NOTHING');
            $claim->execute([$event['id']]);
            if ($claim->rowCount() === 0) {
                $db->commit();
                return "event {$event['id']} already processed";
            }
            $db->prepare('INSERT INTO shipments (order_id) VALUES (?)')->execute([$event['order_id']]);
            $db->commit();
            return "shipment created for {$event['order_id']}";
        } catch (Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }
    
    $event = ['id' => 'evt_91', 'type' => 'payment.succeeded', 'order_id' => 'ord-7'];
    echo handleWebhook($db, $event), "\n";                                  // shipment created for ord-7
    echo handleWebhook($db, $event), "\n";                                  // event evt_91 already processed
    echo $db->query('SELECT COUNT(*) FROM shipments')->fetchColumn(), "\n"; // 1