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 41-60 of 100

  1. Q41.How do you implement rate limiting in PHP when each request runs in a separate process?

    PHP workers share no memory between requests, so the counters must live in shared storage: a fast cache server, a database table or the web server or gateway in front of PHP. Every request checks and updates the counter atomically.

    Common algorithms:

    • Fixed window: count requests per key per minute; simple, but allows bursts at window boundaries.
    • Sliding window: combine the current and previous windows for smoother limits.
    • Token bucket: store tokens and the last refill time, allowing short bursts with a steady average.

    Implementation details:

    1. Atomic updates: use atomic increment operations, or a single database statement, so concurrent requests cannot both pass when only one should.
    2. Keys: per API key, per user or per IP address, with stricter limits on login and password reset.
    3. Expiry: counters expire automatically after their window.
    4. Responses: status 429 with a Retry-After header.
    5. Fail-open or fail-closed: decide what happens if the shared store is down; login endpoints usually fail closed.

    Prefer the edge for coarse limits, such as per-IP floods, and keep business limits, such as per-plan quotas, in the application.

  2. Q42.How do you handle CORS correctly in a PHP API?

    Cross-origin resource sharing lets browsers call your API from pages on other origins. The server decides which origins, methods and headers are allowed by sending response headers; browsers enforce the rules, while non-browser clients ignore them.

    How it works:

    1. For simple requests, the browser sends the request with an Origin header and checks the response's Access-Control-Allow-Origin.
    2. For other requests, such as those with JSON bodies or custom headers, the browser first sends an OPTIONS preflight request, and the server answers with allowed methods, headers and a cache duration.

    Correct configuration:

    • Allowlist origins: compare the incoming Origin with a configured list and echo it back only when it matches; never reflect arbitrary origins.
    • Credentials: if cookies are needed, send Access-Control-Allow-Credentials: true, which is not allowed with a wildcard origin.
    • Vary header: add Vary: Origin when the response depends on it, so caches do not serve one origin's headers to another.
    • Preflight caching: set Access-Control-Max-Age to reduce preflight requests.
    • Answer preflights early, in middleware, without running authentication or business logic.

    Remember: CORS is not an authorization mechanism; every request still needs authentication and authorization on the server.

  3. Q43.How do you stream large responses or downloads from PHP?

    Send data in chunks as it is produced instead of building the whole response in memory. This keeps memory flat for large exports and lets clients start receiving data immediately.

    Techniques:

    1. Disable or flush output buffering with ob_end_flush or flush, so chunks reach the web server instead of accumulating.
    2. Write rows as you read them: fetch database rows one at a time and write each as a line of CSV or newline-delimited JSON.
    3. Stream files with readfile or by copying a file stream to the output with stream_copy_to_stream, rather than loading files into strings.
    4. Set headers first: content type, Content-Disposition for downloads and no Content-Length when the size is unknown.
    5. Raise time limits only for these endpoints, and stop work when the client disconnects, checking connection_aborted.

    Server considerations:

    • Web servers and proxies may buffer responses; configure them to pass streamed responses through for these routes.
    • Long-running exports tie up PHP workers, so for very large files, generate them in a background job and send a download link.
    • For static files, let the web server serve them directly, using a signed or internal redirect, which is far cheaper than streaming through PHP.
  4. Q44.What causes "headers already sent" errors, and how does output buffering relate?

    HTTP headers must be sent before the body. As soon as PHP outputs any byte of the body, such as an echo, whitespace before the opening <?php tag or a warning message, it sends the headers, and later calls to header, setcookie or session_start fail with "headers already sent".

    Common causes:

    1. Whitespace or a byte order mark before <?php or after a closing ?> tag in an included file.
    2. Debug output or warnings printed before redirects.
    3. Starting the session after sending part of the page.

    Fixes:

    • Omit the closing ?> tag in files that contain only PHP code.
    • Save files as UTF-8 without a byte order mark.
    • Turn off display_errors in production and log warnings instead.
    • Structure code so responses are built first and sent once: the handler returns a response object, and the front controller sends headers and body at the end.

    Output buffering collects output in memory and sends it later, which hides the problem but does not fix it, and it wastes memory for large responses. Use it deliberately, for example to capture a template's output, rather than as a global workaround.

  5. Q45.How do HTTP caching headers and conditional requests work in a PHP API?

    Caching headers tell clients and proxies how long a response may be reused. Conditional requests let a client ask "has this changed?" with a validator, such as an entity tag, and receive a short 304 Not Modified response instead of the full body when nothing changed.

    Key headers:

    • Cache-Control: max-age=60 allows reuse for a minute; private limits caching to the user's browser; no-store forbids storing sensitive responses.
    • ETag: a validator, often a hash of the response body or a version number.
    • If-None-Match: the client sends the entity tag it has; if it matches, the server returns 304.
    • Last-Modified and If-Modified-Since: a time-based alternative.

    Practical advice:

    1. Compute entity tags cheaply, such as from a row version or update timestamp, without rendering the full response.
    2. Mark personalized responses private, and never let shared caches store authenticated data.
    3. Use entity tags with If-Match on updates, too, for optimistic concurrency.

    Example

    The first request returns the body and an entity tag; repeating it returns 304 until the data changes.

    <?php
    declare(strict_types=1);
    
    function respond(array $data, ?string $ifNoneMatch): array
    {
        $body = json_encode($data, JSON_THROW_ON_ERROR);
        $etag = '"' . substr(hash('sha256', $body), 0, 16) . '"';
        return $ifNoneMatch === $etag ? [304, $etag, ''] : [200, $etag, $body];
    }
    
    [$status, $etag, $body] = respond(['id' => 7, 'name' => 'Lamp'], null);
    echo "$status $body\n";                                              // 200 {"id":7,"name":"Lamp"}
    echo respond(['id' => 7, 'name' => 'Lamp'], $etag)[0], "\n";         // 304
    echo respond(['id' => 7, 'name' => 'Desk lamp'], $etag)[0], "\n";    // 200
  6. Q46.How do you version and document an HTTP API built in PHP?

    Publish a machine-readable description of every endpoint, its parameters and responses, keep it in version control next to the code and version the API when you must make breaking changes.

    Documentation practices:

    1. Describe the contract in a standard machine-readable format, generated from code annotations or written by hand and checked in continuous integration.
    2. Include examples of requests, responses and errors.
    3. Generate client code or contract tests from the description, so documentation and behavior cannot drift apart.
    URL versionHeader versionQuery parameter version
    /v2/ordersAccept header naming the version/orders?version=2
    Visible, simple to route and cacheKeeps URLs stableSimple, but easy to forget
    Most common choice for public APIsGood for APIs consumed by sophisticated clientsRare in practice

    Avoiding breaking changes: add fields rather than renaming them, make new inputs optional, keep old versions running during migration and announce deprecations with dates and usage metrics.

  7. Q47.How do you configure a PHP application from the environment?

    Read configuration from environment variables, or from files provided by the deployment platform, at startup, convert values to the right types, validate them and pass a configuration object to the rest of the code. The same build then runs in every environment with different settings.

    Practices:

    1. One configuration class that reads, converts and validates all settings, failing fast with a clear message when something is missing or invalid.
    2. Typed values: convert strings to integers, Booleans and enums explicitly; getenv and $_ENV always return strings.
    3. Secrets from secret stores or mounted files, never committed to the repository; local development can use an untracked environment file.
    4. Cache configuration in production, for example in a generated PHP file that the bytecode cache keeps in memory, so it is not parsed on every request.
    5. No configuration reads scattered through the code; pass the configuration object to services that need it.

    Watch for: process managers that clear environment variables for workers by default, which requires explicitly passing or allowing them.

    Example

    Settings are read, converted and validated in one place, and an invalid value fails immediately.

    <?php
    declare(strict_types=1);
    
    final readonly class Config
    {
        public function __construct(public string $dbDsn, public int $pageSize, public bool $debug) {}
    
        public static function fromEnv(array $env): self
        {
            $pageSize = filter_var($env['APP_PAGE_SIZE'] ?? '20', FILTER_VALIDATE_INT, ['options' => ['min_range' => 1, 'max_range' => 200]]);
            if ($pageSize === false) {
                throw new InvalidArgumentException('APP_PAGE_SIZE must be between 1 and 200');
            }
            return new self(
                $env['APP_DB_DSN'] ?? throw new InvalidArgumentException('APP_DB_DSN is required'),
                $pageSize,
                filter_var($env['APP_DEBUG'] ?? 'false', FILTER_VALIDATE_BOOL),
            );
        }
    }
    
    $config = Config::fromEnv(['APP_DB_DSN' => 'driver:host=db;dbname=shop', 'APP_PAGE_SIZE' => '50', 'APP_DEBUG' => 'yes']);
    echo $config->pageSize, " ", var_export($config->debug, true), "\n"; // 50 true
    
    try {
        Config::fromEnv(['APP_DB_DSN' => 'driver:host=db;dbname=shop', 'APP_PAGE_SIZE' => '5000']);
    } catch (InvalidArgumentException $e) {
        echo $e->getMessage(), "\n"; // APP_PAGE_SIZE must be between 1 and 200
    }
  8. Q48.How should a PHP application log?

    Write structured log records, usually JSON lines, to standard output or standard error in containers, or to files collected by an agent. Each record should include a level, a message, a timestamp, the request ID and relevant fields, so logs can be searched and correlated.

    Good practices:

    1. A logging interface injected into services, rather than calling error_log or echo throughout the code, so output format and destination can change in one place.
    2. Levels used consistently: errors for failures needing attention, warnings for handled anomalies and information for key business events.
    3. Context, not concatenation: pass fields such as order_id separately from the message.
    4. A request ID generated at the entry point, or taken from the proxy, added to every record and returned in error responses.
    5. Never log secrets or sensitive personal data: passwords, tokens, full card numbers or session IDs.
    6. Log exceptions once, with the stack trace, at the boundary where they are handled.

    PHP-specific settings: turn on log_errors, turn off display_errors in production and make sure warnings and notices are logged rather than printed into responses.

  9. Q49.How do you structure a PHP backend project as it grows?

    Separate the entry point, the HTTP layer, the business logic and the infrastructure, and organize the business logic by feature. Keep the public web directory minimal and everything else outside it.

    flowchart TD
      A["public/index.php: front controller"] --> B["Http: routes, controllers, middleware"]
      B --> C["Orders: domain and use cases"]
      B --> D["Billing: domain and use cases"]
      C --> E["Infrastructure: database, queues, external clients"]
      D --> E
      F["config: wiring and settings"] --> A

    Principles:

    1. Public directory contains only the front controller and static assets.
    2. Feature modules, such as orders and billing, each with entities, services and interfaces for what they need.
    3. Thin controllers: parse the request, call a use case and build the response.
    4. Infrastructure behind interfaces defined in the modules, so tests can use fakes and storage can change.
    5. Configuration and wiring in one place, usually a container setup file.
    6. Tests mirror the source structure.

    Avoid: business logic in templates or controllers, global helper functions with hidden dependencies and generic folders such as utils that grow without structure.

  10. Q50.How do you use dependency injection in PHP without a framework?

    Pass each class its dependencies through the constructor, typed as interfaces where useful, and create the object graph in one composition place, such as a small container or a factory file. Classes never construct their own collaborators or reach into global state.

    Benefits:

    • Dependencies are explicit in constructor signatures.
    • Tests pass fakes instead of real databases or HTTP clients.
    • Configuration and wiring live in one place.

    Practices:

    1. Constructor promotion keeps injection code short.
    2. Interfaces for boundaries: repositories, clocks, mailers and payment gateways.
    3. Inject configuration values as typed parameters or configuration objects.
    4. Lazy creation for expensive services, through factories or closures that create the object on first use.
    5. Avoid the service locator pattern, where classes fetch dependencies from a global container, since it hides dependencies.

    When a container helps: with many services, a container that resolves constructor parameters automatically through reflection saves boilerplate; cache its resolution in production for speed.

    Interview tip: describing how you would test a class that sends emails, by injecting a fake mailer interface, is a concise way to show you understand why injection matters.

  11. Q51.How do you use PDO correctly in a PHP application?

    PDO is PHP's common database interface. Configure it once to throw exceptions and return associative arrays, use prepared statements for every query with values and share one connection per request through dependency injection.

    Recommended settings:

    1. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, so errors are never silently ignored.
    2. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, so rows are arrays keyed by column name only.
    3. Native prepared statements where the driver supports them, by turning off emulation, so types travel correctly.
    4. Character set in the connection string, so text is stored and read as UTF-8.

    Usage habits:

    • Prepare once, execute many times for repeated inserts.
    • Bind integers with PDO::PARAM_INT where the type matters, such as LIMIT.
    • Iterate statements directly with foreach, or fetch in chunks, instead of fetchAll on huge results.
    • Handle constraint violations by catching PDOException and checking the SQLSTATE code, such as 23000 for integrity constraint violations.
    • Read the connection string, user and password from configuration, never from source code.

    Example

    One configured connection, a reused prepared insert, a typed bound limit and a duplicate rejected by a unique constraint.

    <?php
    declare(strict_types=1);
    
    $db = new PDO(getenv('DB_DSN'), options: [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    ]);
    $db->exec('CREATE TABLE products (id INTEGER PRIMARY KEY, sku TEXT UNIQUE NOT NULL, price_minor INTEGER NOT NULL)');
    
    $insert = $db->prepare('INSERT INTO products (sku, price_minor) VALUES (:sku, :price)');
    foreach ([['LAMP-1', 45000], ['DESK-2', 720000], ['PEN-3', 2050]] as [$sku, $price]) {
        $insert->execute(['sku' => $sku, 'price' => $price]);
    }
    
    $cheap = $db->prepare('SELECT sku, price_minor FROM products WHERE price_minor < :max ORDER BY price_minor');
    $cheap->bindValue('max', 100000, PDO::PARAM_INT);
    $cheap->execute();
    foreach ($cheap as $row) {
        echo "{$row['sku']} {$row['price_minor']}\n";
    }
    
    try {
        $insert->execute(['sku' => 'LAMP-1', 'price' => 1]);
    } catch (PDOException $e) {
        echo "duplicate rejected: {$e->getCode()}\n";
    }
    // Output:
    // PEN-3 2050
    // LAMP-1 45000
    // duplicate rejected: 23000
  12. Q52.How do database transactions work with PDO?

    Start a transaction with beginTransaction, run the related statements, call commit when all succeed and rollBack if anything fails. Inside a transaction, other sessions see either all of its changes or none of them.

    A safe pattern:

    1. Begin the transaction.
    2. Run statements inside try.
    3. Commit at the end of try.
    4. In catch (Throwable $e), roll back and rethrow, so callers still see the failure.

    Good practices:

    • Wrap the pattern in a helper, such as transactional(callable $work), so every transaction is handled the same way.
    • Let the database enforce rules with constraints, such as CHECK (balance >= 0), so invalid states fail inside the transaction.
    • Keep transactions short: no HTTP calls, emails or slow computations inside them.
    • Retry the whole transaction on deadlocks or serialization failures, and make it safe to repeat.
    • Lock rows deliberately with SELECT ... FOR UPDATE where the database supports it, when you must read and then update the same row.

    Nested transactions are not supported directly by PDO; use savepoints or a helper that tracks nesting depth.

    Example

    A transfer that would make a balance negative fails the constraint, and the rollback undoes its first update too.

    <?php
    declare(strict_types=1);
    
    $db = new PDO(getenv('DB_DSN'), options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $db->exec('CREATE TABLE accounts (id TEXT PRIMARY KEY, balance INTEGER NOT NULL CHECK (balance >= 0))');
    $db->exec("INSERT INTO accounts VALUES ('a', 1000), ('b', 200)");
    
    function transfer(PDO $db, string $from, string $to, int $amount): void
    {
        $update = $db->prepare('UPDATE accounts SET balance = balance + :delta WHERE id = :id');
        $db->beginTransaction();
        try {
            $update->execute(['delta' => $amount, 'id' => $to]);
            $update->execute(['delta' => -$amount, 'id' => $from]);
            $db->commit();
        } catch (Throwable $e) {
            $db->rollBack();
            throw $e;
        }
    }
    
    transfer($db, 'a', 'b', 300);
    try {
        transfer($db, 'b', 'a', 900);
    } catch (PDOException) {
        echo "second transfer rolled back\n";
    }
    echo json_encode($db->query('SELECT id, balance FROM accounts ORDER BY id')->fetchAll(PDO::FETCH_KEY_PAIR)), "\n";
    // Output:
    // second transfer rolled back
    // {"a":700,"b":500}
  13. Q53.How do you avoid N+1 queries in PHP code?

    An N+1 problem happens when code loads a list with one query, then runs another query for each item, such as loading each order's customer separately. With 50 items, that is 51 round trips to the database, and the cost grows with the list.

    How to spot it:

    1. Log or count queries per request in development, and look for repeated queries that differ only by an ID.
    2. Watch for database calls inside loops, including hidden ones in lazy-loading model properties.

    Fixes:

    • Join the related table in the original query when you need a few columns.
    • Batch load: collect the IDs, load all related rows with one IN query and index them by ID in an array.
    • Eager loading in mappers that support it, which does the batching for you.
    • Aggregate in SQL with GROUP BY instead of counting in PHP loops.

    Watch the opposite problem: joins that multiply rows, such as orders joined to both items and payments, can produce huge result sets; two batched queries are sometimes better than one wide join.

    Example

    Counting queries shows the difference between loading per item, batching and joining.

    <?php
    declare(strict_types=1);
    
    final class CountingDb
    {
        public int $queries = 0;
    
        public function __construct(private PDO $pdo) {}
    
        public function all(string $sql, array $params = []): array
        {
            $this->queries++;
            $stmt = $this->pdo->prepare($sql);
            $stmt->execute($params);
            return $stmt->fetchAll(PDO::FETCH_ASSOC);
        }
    }
    
    $pdo = new PDO(getenv('DB_DSN'), options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $pdo->exec('CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT)');
    $pdo->exec('CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER)');
    $pdo->exec("INSERT INTO customers VALUES (1, 'c1'), (2, 'c2'), (3, 'c3')");
    $pdo->exec('INSERT INTO orders (customer_id) VALUES (1), (2), (1), (3), (2), (1)');
    $db = new CountingDb($pdo);
    
    foreach ($db->all('SELECT id, customer_id FROM orders') as $order) {
        $db->all('SELECT name FROM customers WHERE id = ?', [$order['customer_id']]);
    }
    echo "per item: {$db->queries} queries\n";
    
    $db->queries = 0;
    $orders = $db->all('SELECT id, customer_id FROM orders');
    $ids = array_values(array_unique(array_column($orders, 'customer_id')));
    $placeholders = implode(',', array_fill(0, count($ids), '?'));
    $db->all("SELECT id, name FROM customers WHERE id IN ($placeholders)", $ids);
    echo "batched: {$db->queries} queries\n";
    
    $db->queries = 0;
    $db->all('SELECT o.id, c.name FROM orders o JOIN customers c ON c.id = o.customer_id');
    echo "joined: {$db->queries} query\n";
    // Output:
    // per item: 7 queries
    // batched: 2 queries
    // joined: 1 query
  14. Q54.How do you paginate large result sets efficiently?

    Offset pagination, with LIMIT and OFFSET, is simple but gets slower on deep pages because the database must skip every earlier row. Keyset pagination remembers the last row's sort key and asks for rows after it, which stays fast at any depth.

    Offset paginationKeyset pagination
    LIMIT 20 OFFSET 4000WHERE id < :last_id ORDER BY id DESC LIMIT 20
    Jump to any page numberMove to the next or previous page only
    Slower as the offset growsFast at any depth with an index
    Rows can be skipped or repeated when data changes between pagesStable when new rows are inserted
    Best for small tables and admin screensBest for feeds, APIs and large tables

    Implementing keyset pagination well:

    1. Sort by a unique, indexed key, or a pair such as (created_at, id) to break ties.
    2. Return an opaque cursor, such as the encoded last key, rather than raw values, so the format can change.
    3. Fetch one extra row to know whether another page exists.
    4. Validate the page size against a maximum.

    Total counts are expensive on large tables; show approximate counts or omit them.

    Example

    Pages of four rows, newest first, each continuing after the last ID of the previous page.

    <?php
    declare(strict_types=1);
    
    $db = new PDO(getenv('DB_DSN'), options: [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
    $db->exec('CREATE TABLE jobs (id INTEGER PRIMARY KEY, title TEXT)');
    $insert = $db->prepare('INSERT INTO jobs (title) VALUES (?)');
    for ($i = 1; $i <= 10; $i++) {
        $insert->execute(["job $i"]);
    }
    
    function page(PDO $db, ?int $beforeId, int $limit): array
    {
        $stmt = $db->prepare('SELECT id FROM jobs WHERE id < :before ORDER BY id DESC LIMIT :limit');
        $stmt->bindValue('before', $beforeId ?? PHP_INT_MAX, PDO::PARAM_INT);
        $stmt->bindValue('limit', $limit, PDO::PARAM_INT);
        $stmt->execute();
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
    }
    
    $cursor = null;
    do {
        $ids = page($db, $cursor, 4);
        echo json_encode($ids), "\n";
        $cursor = $ids === [] ? null : end($ids);
    } while ($ids !== []);
    // Output:
    // [10,9,8,7]
    // [6,5,4,3]
    // [2,1]
    // []
  15. Q55.How do you manage database schema migrations in a PHP project?

    Keep schema changes as versioned migration files in the repository, apply them automatically in order during deployment and record which ones have run in a table in the database. Every environment then has the same schema history.

    Practices:

    1. One change per migration, with a timestamp or sequence number in the file name.
    2. Never edit a migration that has run in a shared environment; add a new one instead.
    3. Run migrations in the deployment pipeline, before or as part of releasing the new code, with a lock so two deployments cannot run them at once.
    4. Test migrations against a copy of production-sized data, since some changes lock tables for a long time.

    Zero-downtime changes, for applications that keep serving during deployment:

    • Expand, then contract: add new columns or tables first, deploy code that writes to both old and new structures, backfill, switch reads and remove old structures in a later release.
    • Make new columns nullable or give them defaults, so old code can still insert rows.
    • Create indexes without blocking writes, using the database's online index options.
    • Backfill in batches to avoid long locks and replication lag.

    Rollbacks of schema changes are risky; prefer forward fixes, and keep destructive steps for a later, separate release.

  16. Q56.How do database connections and persistent connections work in PHP?

    In the request-per-process model, each request normally opens a database connection when first needed and closes it at the end of the request. Opening connections is not free, especially with TLS, so high-traffic applications reduce that cost in one of three ways.

    Options:

    1. Persistent connections: PDO's persistent option keeps the connection open in the worker process and reuses it for the next request. It saves connection time, but every worker holds a connection, and state left by one request, such as open transactions or session settings, can leak into the next.
    2. An external connection pooler between PHP and the database, which multiplexes many short client connections onto fewer database connections.
    3. Long-running application servers, which keep a connection pool in memory across requests.

    Sizing: the total number of database connections equals the number of PHP workers across all servers, plus background workers. A pool of 200 workers on five servers can mean 1,000 connections, which many databases cannot handle well.

    Good practices:

    • Size worker pools with the database's connection limit in mind.
    • Reset or close connections cleanly after errors.
    • Monitor connection counts and wait times on the database side.
  17. Q57.How do you use caching in a PHP application?

    PHP applications cache at several levels: compiled code, application data within one request, data shared across requests and whole HTTP responses. Each level removes a different kind of repeated work.

    Bytecode cacheShared data cacheHTTP caching
    Keeps compiled scripts in shared memoryStores query results and computed values in a shared storeLets browsers, proxies or content delivery networks reuse responses
    Built into PHP; enable and size itNeeds a cache server or a shared in-memory storeDriven by Cache-Control and entity tag headers
    Removes parsing and compiling on every requestRemoves repeated database or API workRemoves the request to PHP entirely

    Data caching practices:

    1. Cache-aside: read from the cache; on a miss, load from the source, store with an expiry and return.
    2. Keys include every input that affects the value, such as tenant, language and version.
    3. Expiry and invalidation: short lifetimes for changing data, and explicit deletion when the source changes.
    4. Stampede protection: when a popular entry expires, let one request rebuild it, using a short lock, while others serve the old value.
    5. Per-request memoization in a simple array for values used many times within one request.

    Measure hit rates, and remember that cached data can be stale; never cache permissions or prices longer than the business accepts.

  18. Q58.How do you run background jobs and queues with PHP?

    Move slow or unreliable work, such as sending emails, generating reports or calling partner APIs, out of the request into a queue. Long-running PHP worker processes take jobs from the queue, process them and acknowledge them, while the web request returns immediately.

    flowchart LR
      A["Web request"] --> B["Validate and save data"]
      B --> C["Push job to queue"]
      C --> D["Return response quickly"]
      E["Queue"] --> F["Worker processes"]
      C --> E
      F --> G{"Job succeeded?"}
      G -->|Yes| H["Acknowledge"]
      G -->|No| I["Retry with backoff, then dead-letter"]

    Worker design:

    1. Idempotent jobs: a job may run twice after a crash or timeout, so check state before acting.
    2. Small payloads: store IDs in the job, and load fresh data when processing.
    3. Restart workers regularly, after a number of jobs or on memory growth, since state and leaks accumulate in long-running PHP processes.
    4. Graceful shutdown: handle termination signals, finish the current job and stop taking new ones.
    5. Supervision: a process supervisor or orchestrator keeps the right number of workers running.
    6. Monitoring: queue length, oldest job age, failures and dead-letter size.

    Push jobs only after the database transaction commits, or use an outbox table, so workers never see jobs for data that was rolled back.

  19. Q59.How does a long-running PHP application server differ from the request-per-process model?

    In the classic model, each request starts the application from scratch and throws everything away at the end. Long-running application servers keep the application booted in memory and handle many requests in the same process, which removes bootstrap cost but changes the rules developers must follow.

    Request-per-processLong-running application server
    Boots the application on every requestBoots once, then serves many requests
    No state survives between requestsStatic properties, singletons and globals persist
    Memory leaks within a request vanish at its endLeaks accumulate until the worker restarts
    Simple and very robustMuch lower latency and higher throughput
    Best for most traditional applicationsBest for high-traffic APIs and real-time features

    Rules for long-running servers:

    1. No request data in static properties or singletons; reset services that hold per-request state between requests.
    2. Close or reset connections that may be left in a bad state.
    3. Restart workers periodically as a safety net.
    4. Avoid blocking calls where the server uses asynchronous input and output, since one blocking call stalls other requests.
    5. Test for state leaks by sending sequences of requests from different users.
  20. Q60.How do you write unit tests for PHP code?

    Test small units of behavior through their public methods with fast, isolated tests. PHP's common test frameworks share the same model: test classes with test methods, assertions, data providers for many input cases and setup methods that build the object under test.

    Structure each test as arrange, act and assert:

    1. Arrange: build the object with fake dependencies and inputs.
    2. Act: call one method.
    3. Assert: check the returned value, the state change or the message sent.

    Good practices:

    • Name tests by behavior, such as "refund is rejected after 30 days".
    • Data providers for many cases of one rule, including edge cases.
    • Inject the clock, random values and identifiers, so tests are deterministic.
    • No real network or database in unit tests; use fakes behind interfaces.
    • Test failure paths, such as exceptions and validation errors.
    • Keep tests fast, so developers run them constantly.

    Beyond unit tests: integration tests against a real database engine, HTTP-level tests of the front controller and a small number of end-to-end tests.

    Measure usefulness, not just coverage: mutation testing tools change the code slightly and check that tests fail, revealing assertions that are too weak.