227 Php Developer Interview Questions & Answers

116 top • 13 Amazon • 21 Google • 10 Netflix • 7 Meta • 18 NVIDIA • 21 Apple • 21 Microsoft

Php Developer icon

Questions with Detailed ExplanationsWith Detailed Explanations

(Last Updated: September 3, 2026)

31. What is PHP-FPM?Language SpecificEasy

Question Details

Define PHP-FPM as the FastCGI Process Manager used to run PHP behind a web server such as Nginx or Apache. Explain the request path, master and worker processes, pools, process-management modes, worker limits, timeouts, slow logs, graceful reloads, and how pool sizing affects memory use, concurrency, queueing, and availability.

Short Interview Answer (30-60 seconds)

PHP FPM is the FastCGI Process Manager commonly used to run PHP behind a web server such as Nginx or Apache. The web server sends PHP work to an FPM pool, and an available worker process runs the request and returns the result. The main production concern is worker sizing. Too few workers can make requests wait, while too many workers can consume too much memory and reduce availability.

Detailed Explanation

PHP FPM is a service that manages PHP workers for a web site. A web server receives a visitor request and sends PHP work to this service. The service keeps separate worker processes available. Each worker handles one request at a time. This allows several requests to run at the same time through different workers. The worker count needs careful sizing. Too few workers can make requests wait. Too many workers can use too much memory and make the server unstable. FPM also provides settings for slow requests, long requests, and safe process management.

Useful Questions to Ask the Interviewer
  1. Are you asking about a typical Nginx or Apache setup?
  2. Should I also explain how to size and monitor an FPM pool?
What is PHP-FPM? diagram
How to Explain It in an Interview

PHP FPM means FastCGI Process Manager. It is a PHP server interface designed to manage FastCGI processes. A common request path is browser to Nginx or Apache, then through FastCGI to an FPM pool. An available FPM worker executes the PHP script and returns the response through the web server.

FPM has a master process that manages worker processes. Workers are organized into pools. Each pool can have its own listening address or socket, operating system user, PHP settings, and process limits. Normal requests execute in separate worker processes, so ordinary mutable request state is not shared between workers.

FPM supports three process management modes. Static keeps exactly the configured number of workers. Dynamic adjusts the number of workers while staying within configured limits. Ondemand creates workers when requests arrive and removes workers after they stay idle for the configured time. The pm.max_children setting defines the maximum number of child processes that can serve requests at the same time. ([php.net](https://www.php.net/manual/en/install.fpm.configuration.php))

Worker sizing is a memory and concurrency tradeoff. More workers allow more requests to execute at once, but each worker uses memory. When all allowed workers are busy, new connections can wait in the listen queue. A very high worker limit can exhaust memory and hurt availability.

FPM also has production controls. request_terminate_timeout can terminate a worker serving a request that exceeds the configured time. request_slowlog_timeout can trigger a PHP stack trace in the configured slowlog for a slow request. pm.max_requests can recycle a child after it has handled a chosen number of requests, which can help contain memory growth in application code or third party libraries. FPM also supports graceful reload behavior so configuration can be reloaded while existing work is allowed to finish rather than using an immediate hard stop. ([php.net](https://www.php.net/manual/en/install.fpm.configuration.php))

Where it is used

PHP FPM is commonly used on production web servers where Nginx or Apache receives HTTP requests and passes PHP execution to FPM. It is useful when a team needs controlled request concurrency, separate application pools, worker limits, slow request diagnosis, request time limits, and controlled process lifecycle management. Separate pools are useful when applications need different operating system users, PHP settings, sockets, or resource limits.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how PHP commonly runs in production. They want to see knowledge of the request path, FPM processes, pools, worker limits, time limits, slow request diagnosis, reload behavior, memory use, concurrency, queueing, and availability.

Common interview mistakes

A common mistake is saying that Nginx or Apache directly executes the PHP script when the setup actually passes PHP work to FPM. Another mistake is thinking one FPM worker runs several PHP requests at the same instant. A worker normally executes one request at a time. It is also wrong to assume that increasing pm.max_children always improves performance. Too many workers can exhaust memory and reduce availability. Other mistakes include ignoring queued connections, slow request logs, request time limits, worker recycling, and the fact that normal mutable request state is not shared between separate FPM worker processes.

Interview tip

Start with the request path. Explain that the web server passes PHP work through FastCGI to an FPM pool and an available worker executes it. Then explain the master process, pools, static, dynamic, and ondemand modes, and pm.max_children. Finish with the main tradeoff: more workers can increase concurrency, but they also increase memory use, so production sizing must balance memory, queueing, and availability.

Interviewer may ask next
What happens when every allowed PHP FPM worker is busy and another request arrives?

The new request cannot start PHP execution immediately because no worker is free and pm.max_children prevents the pool from creating more than its configured maximum. The connection can wait in the listen queue while it waits for a worker, subject to the configured queue capacity and the surrounding server limits. This matters because queueing increases response time, and sustained overload can eventually cause failed requests. I would not automatically increase the worker limit. I would first check memory per worker, slow requests, CPU use, request duration, queue activity, and available memory before deciding whether more workers are safe.

How would you choose between static, dynamic, and ondemand PHP FPM process management?

I would choose based on traffic patterns, available memory, and how quickly workers need to be ready. Static keeps exactly pm.max_children workers, so its worker count is predictable but idle workers still use memory. Dynamic changes the number of workers according to configured start and spare worker settings while never exceeding pm.max_children. Ondemand creates workers when requests arrive and removes idle workers after pm.process_idle_timeout, which can save memory for low traffic pools but can add process creation cost when new work arrives after an idle period. In all three modes, pm.max_children sets the maximum number of child processes that can serve requests at the same time. ([php.net](https://www.php.net/manual/en/install.fpm.configuration.php))

32. How does PHP-FPM manage worker processes, and how would you choose pool settings?Language SpecificHard

Question Details

Compare static, dynamic, and ondemand process management; explain max_children, spare servers, memory limits, queueing, timeouts, recycling, and evidence-based sizing.

Short Interview Answer (30-60 seconds)

I would choose PHP FPM pool settings from measured worker memory, available server memory, request duration, expected concurrency, and dependency capacity. Static keeps exactly max_children workers. Dynamic adjusts the worker count while keeping spare workers ready. Ondemand starts workers only when requests arrive. max_children is the main safety limit because each worker handles one request at a time. I would validate the setting with queue depth, active worker count, request latency, memory use, slow logs, and load tests.

Detailed Explanation

PHP FPM keeps separate worker programs that run PHP requests for a web server. Each worker normally serves one request at a time. Pool settings decide how many workers exist, when they start, when idle workers stop, and how many requests can run together. More workers can serve more requests at once, but every worker uses memory and processor time. The correct settings therefore depend on measured memory use, traffic volume, request duration, response time goals, and the capacity of databases or other services used by the application.

Useful Questions to Ask the Interviewer
  1. How much memory is reserved for PHP FPM?
  2. What are the normal and peak request rates?
  3. What are the typical and high percentile request durations?
  4. How much private memory does a busy worker use?
  5. What queueing and response time are acceptable?
  6. Which downstream services limit safe concurrency?
How does PHP-FPM manage worker processes, and how would you choose pool settings? diagram
How to Explain It in an Interview

PHP FPM has a master process that creates, monitors, and stops worker processes. Workers are separate operating system processes, so normal request variables and mutable PHP state are not shared between them.

With static management, FPM creates exactly pm.max_children workers and keeps them running. It gives predictable ready capacity, but all workers consume resources even during quiet periods.

With dynamic management, FPM starts pm.start_servers workers. It then tries to keep idle workers between pm.min_spare_servers and pm.max_spare_servers. It can create or stop workers as traffic changes, but the total never exceeds pm.max_children. This mode suits steady traffic that rises and falls.

With ondemand management, workers are created when requests arrive. Idle workers are stopped after pm.process_idle_timeout. This saves memory for quiet applications, but the first requests after an idle period can wait for workers to start.

I would first reserve memory for the operating system, the web server, OPcache shared memory, monitoring, and other services. I would measure worker memory under realistic traffic. Private memory or proportional memory is more useful than simply adding every worker RSS value because RSS can count shared pages more than once. An initial memory bound is the memory available to workers divided by conservative per worker memory. I would then reduce that number when processor capacity, database connections, or external services support less concurrency.

When all workers are busy, requests wait in the FPM listen queue, subject to the configured socket backlog and surrounding web server limits. A growing queue, frequent max children warnings, or high latency shows that capacity or request performance needs investigation.

request_slowlog_timeout can write a worker backtrace to the configured slowlog for a slow request. request_terminate_timeout can terminate a worker handling a request that exceeds the configured time. pm.max_requests recycles a worker after a chosen number of requests and can contain gradual memory growth, but it does not fix the underlying cause. PHP memory_limit limits memory allocated through PHP accounting for one script. It is not a complete measurement of total worker process memory.

Where it is used

PHP FPM pools are used when Nginx, Apache, or another FastCGI client sends web requests to PHP workers. Dynamic pools are common for production applications with regular but changing traffic. Ondemand pools are useful for quiet administration sites, development tools, or many lightly used applications where idle memory matters. Static pools are useful when traffic is predictable, enough memory is reserved, and keeping all capacity ready is preferred. Separate pools can give applications different users, sockets, logs, PHP settings, and resource limits, but pools are not complete security isolation because resources such as one OPcache instance can be shared by the same FPM instance.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands how PHP requests run in production, how the FPM master process controls workers, and how worker limits affect memory, throughput, queueing, and response time. It also tests whether the candidate can choose pool settings from measured evidence instead of copying arbitrary values.

Common interview mistakes

Common mistakes include choosing pm.max_children from processor count alone, multiplying memory_limit by the worker count, or adding worker RSS values without accounting for shared memory. Another mistake is increasing workers when the real problem is a slow database, blocked network call, lock, or overloaded dependency. Very high spare worker settings waste memory, while very low values can cause repeated worker creation during traffic bursts. Ondemand can add startup delay after idle periods. request_terminate_timeout can interrupt valid long requests when chosen without evidence. pm.max_requests should not be treated as a repair for a memory leak. Teams also forget that separate pools need separate capacity planning and that the total workers across all pools must fit within the host limits.

Interview tip

Begin with the practical rule that pm.max_children is a memory and concurrency limit, not a value to guess. Compare the three process management modes in terms of ready capacity and idle memory. Then explain how measured worker memory, request duration, queue depth, processor use, and downstream capacity determine the final settings.

Interviewer may ask next
What happens when every worker has reached a long running request and pm.max_children is already reached?

New requests wait in the FPM listen queue until a worker becomes available. If the queue or socket backlog fills, later connections can fail or time out depending on the web server and operating system configuration. This matters because blindly increasing pm.max_children can exhaust memory or overload the database. The correct response is to inspect request duration, slow logs, queue depth, dependency latency, and available host capacity before adding workers.

When would you choose ondemand instead of dynamic process management?

I would choose ondemand when traffic is infrequent and reducing idle worker memory is more important than avoiding worker startup delay. Workers are created when requests arrive and idle workers are stopped after pm.process_idle_timeout. The main tradeoff is that requests arriving after an idle period can wait for worker creation. Dynamic is usually better for steady or latency sensitive traffic because it keeps a configured number of spare workers ready.

33. What is OPcache?Language SpecificEasy

Question Details

Define OPcache as a PHP extension that stores compiled PHP bytecode in shared memory so scripts do not need to be parsed and compiled on every request. Explain the normal request benefit, memory sizing, file-change validation, deployment invalidation or reset, command-line differences, and why OPcache does not cache application data or database results.

Short Interview Answer (30-60 seconds)

OPcache is a PHP extension that stores compiled PHP bytecode in shared memory. PHP can then reuse that prepared code instead of parsing and compiling the same script again for normal web requests. In production, I would enable it, give it enough memory for the application, and make sure deployments correctly activate changed files.

Detailed Explanation

OPcache helps a PHP application respond faster by remembering work that PHP has already done. When PHP runs a program, it first has to read and prepare the program before it can run it. OPcache keeps that prepared form in memory so later requests can reuse it. This saves repeated work when the same PHP files are used many times. It is mainly useful for busy web applications. It does not remember user information, application values, saved page results, or information returned from a database.

Useful Questions to Ask the Interviewer
  1. Are you asking about OPcache for normal web requests or command line scripts?
  2. Should I also explain how deployments make changed PHP files become active?
What is OPcache? diagram
How to Explain It in an Interview

OPcache is a PHP extension that stores compiled PHP bytecode in shared memory. Bytecode is the prepared form of a PHP script that the PHP engine can execute. PHP can reuse this cached bytecode instead of parsing and compiling the same script again for each normal web request. This reduces repeated work and usually improves application response time and server efficiency. ([php.net](https://www.php.net/manual/en/book.opcache.php))

The cache has a limited amount of shared memory. The opcache.memory_consumption setting controls its size. Production systems should give it enough memory for the application and monitor cache usage. If capacity is too small, the cache can fill and reduce the performance benefit. ([php.net](https://www.php.net/manual/en/opcache.configuration.php))

File change validation is also important. With opcache.validate_timestamps enabled, OPcache checks for changed files according to its validation settings. If timestamp validation is disabled, changed source files are not automatically noticed. A deployment must then use invalidation, reset the cache, or restart the appropriate PHP service so new code becomes active. ([php.net](https://www.php.net/manual/en/opcache.configuration.php))

For command line PHP, OPcache is controlled separately by opcache.enable_cli. Its default setting is off. Short command line programs often gain less because the process ends quickly. ([php.net](https://www.php.net/manual/en/ini.list.php))

OPcache only caches compiled PHP code. It is not an application data cache and does not cache database query results.

Where it is used

OPcache is commonly used on production PHP web servers where the same application files run across many requests. It is especially useful with PHP FPM because repeated requests can benefit from compiled scripts kept in shared memory. Teams also plan for OPcache during deployments so changed PHP files become active correctly. Command line workloads need separate consideration because OPcache has a separate command line setting.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands how PHP prepares scripts for execution and how OPcache improves production performance. They also want to see whether the candidate understands memory sizing, source file validation, deployment behavior, command line differences, and the important limit that OPcache does not store application data or database results.

Common interview mistakes

A common mistake is saying that OPcache caches application data or database results. It does not. It caches compiled PHP bytecode. Another mistake is assuming that edited source files always become active immediately. That depends on file change validation and deployment handling. Candidates may also forget that OPcache uses limited shared memory, so sizing and monitoring matter. Another mistake is assuming command line PHP uses OPcache in exactly the same way as normal web requests. Command line use has its own setting.

Interview tip

Start by saying that OPcache stores compiled PHP bytecode in shared memory so PHP can avoid repeated parsing and compilation. Then explain the production points that matter most: memory sizing, file change validation, deployment invalidation or reset, and command line behavior. Finish by clearly saying that OPcache does not cache application data or database results.

Interviewer may ask next
What happens when PHP source files change while OPcache is enabled?

The result depends on file change validation. When opcache.validate_timestamps is enabled, OPcache checks for changed files according to its validation settings and can compile the changed script again. When timestamp validation is disabled, cached code remains valid until it is explicitly invalidated, the cache is reset, or the appropriate PHP service is restarted. This matters during deployment because the release process must make sure new code becomes active.

Should OPcache always be enabled for command line PHP?

No. Command line OPcache is controlled separately by opcache.enable_cli, and its default setting is off. Long running or repeated command line workloads can benefit in some cases, while short commands may gain little because the process ends quickly. The main tradeoff is whether reusing compiled code provides enough benefit for that workload to justify enabling the cache.

34. How does opcache change PHP execution and deployment behavior?Language SpecificHard

Question Details

Explain compilation to opcodes, shared cache, timestamp validation, preload considerations, invalidation, memory sizing, deployment strategies, and how to verify stale-code or cache issues.

Short Interview Answer (30-60 seconds)

OPcache improves PHP performance by keeping compiled script instructions in shared memory, so PHP can reuse them instead of loading, parsing, and compiling the same source files for every request. The important deployment issue is freshness. When timestamp validation is disabled, changing a file on disk does not make PHP use the new code automatically. The release process must invalidate the affected cache or restart the PHP processes that own it. ([php.net](https://www.php.net/manual/en/book.opcache.php))

Detailed Explanation

OPcache lets a PHP server remember prepared versions of application files. Normally, PHP must read a file and prepare its instructions before running it. OPcache keeps those prepared instructions in shared memory so later requests can reuse them. This reduces repeated work and usually improves response time. It also changes deployment behavior because the server may continue using a remembered version after a source file changes. A safe release must therefore consider file checks, cache clearing, process restarts, available memory, and code loaded when the server starts.

Useful Questions to Ask the Interviewer
  1. Which server mode runs the application, such as PHP FPM or Apache?
  2. Is timestamp validation enabled in production?
  3. Does deployment switch release directories or overwrite live files?
  4. Is preloading enabled?
How does opcache change PHP execution and deployment behavior? diagram
How to Explain It in an Interview

PHP compiles a source file into opcodes, which are instructions for the PHP virtual machine. OPcache stores this compiled bytecode in shared memory. Requests using the same OPcache instance can reuse it, but ordinary request variables and mutable application data are not shared between PHP FPM workers. ([php.net](https://www.php.net/manual/en/book.opcache.php))

When opcache.validate_timestamps is enabled, OPcache checks for changed scripts according to opcache.revalidate_freq. A value of zero allows a check on every request. When validation is disabled, filesystem changes require opcache_invalidate, opcache_reset, or a restart of the server processes that own the cache. A reset executed through a different server mode may affect a different cache instance, so deployment should target the actual PHP FPM pool or web server. ([php.net](https://www.php.net/manual/en/opcache.configuration.php))

For production, I prefer versioned release directories, an atomic symbolic link switch, and a controlled PHP FPM reload or restart. This avoids requests observing files from two releases. Directly replacing active files one at a time can create inconsistent execution.

Memory must fit the number and size of cached scripts. I inspect free memory, wasted memory, cache_full, restart_pending, cached script count, hit rate, and restart counters through opcache_get_status. I also verify the configured memory limit and maximum cached file count. The status function reports the memory cache, not the optional file cache. ([php.net](https://www.php.net/manual/en/function.opcache-get-status.php))

Preloading loads selected functions, classes, interfaces, and traits when the persistent PHP process starts. It uses baseline memory, requires a process restart to clear changed definitions, and is not supported on Windows. I use it only after measurement shows a useful benefit. ([php.net](https://www.php.net/opcache.preloading.php))

Where it is used

OPcache is commonly used in production PHP applications served by PHP FPM or Apache, especially applications that load many PHP files for each request. It is also relevant to persistent application servers and queue workers when their server mode has OPcache enabled. Deployment systems use cache invalidation or process restarts when timestamp validation is disabled. Teams monitor memory use, cached file capacity, hit rate, wasted memory, and restart activity to confirm that the cache is large enough and remains healthy.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands the PHP compilation process and the production effects of keeping compiled code in shared memory. It also tests judgment about timestamp checks, invalidation, memory sizing, preloading, process restarts, safe releases, and stale code diagnosis. A strong answer connects runtime performance with a reliable deployment process instead of treating OPcache as only a configuration switch.

Common interview mistakes

A common mistake is believing that OPcache stores request data or shares mutable variables between PHP FPM workers. It stores compiled script instructions. Another mistake is disabling timestamp validation without adding invalidation or process restart steps to deployment. Other errors include calling opcache_reset from a command line process and assuming it cleared the web server cache, restarting only one pool while another pool still serves traffic, overwriting live files one at a time, sizing the shared memory too small, setting the maximum cached file count below the application needs, and assuming opcache_get_status or opcache_reset manages the optional file cache. Preloading too much code without measurement can also waste persistent memory and make releases harder. ([php.net](https://www.php.net/manual/en/function.opcache-get-status.php))

Interview tip

Begin with the practical tradeoff. OPcache removes repeated compilation work, but deployment must guarantee cache freshness. Then explain timestamp validation, targeted invalidation, process restarts, memory sizing, preloading, and the status values you would inspect when stale code is suspected.

Interviewer may ask next
What happens when opcache.validate_timestamps is disabled and a cached PHP file changes on disk?

PHP can continue running the old cached opcodes because OPcache is not checking the file timestamp for changes. The new source takes effect only after the affected script is invalidated, the memory cache is reset, or the PHP processes that own the cache are restarted. This matters because the files on disk may show the new release while requests still execute old code. The tradeoff is less repeated file checking in exchange for a stricter deployment process. ([php.net](https://www.php.net/manual/en/opcache.configuration.php))

How would you size and verify OPcache for a large production application?

I would count the application scripts, observe real memory use, and configure opcache.memory_consumption and opcache.max_accelerated_files with enough room for the deployed code. I would then inspect free memory, wasted memory, cache_full, cached script count, hit rate, and restart counters through opcache_get_status. This matters because insufficient capacity can leave scripts uncached or schedule cache restarts. The tradeoff is that larger settings reserve more shared memory, so sizing should use production measurements rather than guesswork. ([php.net](https://www.php.net/manual/en/function.opcache-get-status.php))

35. How do fibers work in PHP, and what problems do they solve?Language SpecificHard

Question Details

Explain suspension and resumption, values and exceptions crossing fiber boundaries, cooperative scheduling, relationship to event loops, blocking I/O limitations, and appropriate library-level use.

Short Interview Answer (30-60 seconds)

Fibers let PHP pause a function and later continue it from the same place while preserving its local variables and call stack. They make asynchronous library code easier to write in a clear sequential style, but they do not provide parallel execution or automatically make input and output asynchronous. A scheduler or event loop must resume each suspended fiber, and a normal blocking operation still blocks the PHP thread.

Detailed Explanation

See the Code while reading this explanation.

Fibers let one piece of PHP work pause and allow another piece of work to run. The paused work remembers where it stopped and keeps the information it was using. Later, it can continue from that exact point. This is useful when a program waits for timers, network data, or other results. However, fibers do not make work happen at the same time, and they do not make a slow waiting operation safe by themselves. Another part of the program must decide when each paused task can continue.

Useful Questions to Ask the Interviewer
  1. Should the example show direct Fiber control or a library abstraction?
  2. Should cancellation and timeout handling be included?
  3. Will this run in a command line worker or a web request?
How do fibers work in PHP, and what problems do they solve? diagram
How to Explain It in an Interview

A Fiber is a full stack interruptible function. It has its own call stack, so it can suspend even inside a deeply nested function. Creating a Fiber does not run it. Fiber::start begins it and passes arguments to its callable.

Inside the running fiber, Fiber::suspend pauses the complete fiber stack. The value given to suspend is returned by the caller's start, resume, or throw operation. When the caller uses Fiber::resume, the supplied value becomes the result of the suspended Fiber::suspend call.

Fiber::throw resumes a suspended fiber by throwing a Throwable from its current Fiber::suspend call. The fiber may catch it. An uncaught Throwable inside the fiber crosses back through start, resume, or throw. After normal completion, Fiber::getReturn reads the callable's return value. It throws FiberError if the fiber has not completed normally.

Scheduling is cooperative. A fiber keeps running until it suspends, returns, or throws. PHP does not automatically switch fibers. An event loop or scheduler must resume them when timers, sockets, or other operations become ready.

Fibers do not convert blocking input and output into asynchronous input and output. A blocking database call, file operation, sleep call, or network call can stop every fiber on the same PHP thread. Libraries therefore need suitable nonblocking operations or extensions.

Each live fiber keeps its own call stack and state, so it uses additional memory. Starting, suspending, and resuming also has runtime cost. These costs are normally useful when fibers simplify many waiting operations, but fibers should not be created for ordinary straight line code.

Example

The example starts one fiber and receives the value produced by its first suspension. It then resumes the fiber with a value, which becomes the result of Fiber::suspend inside the fiber. At the second suspension, the caller injects a RuntimeException with Fiber::throw. The fiber catches that exception and returns a final result. The caller checks that the fiber has terminated and then reads the result with Fiber::getReturn. The example demonstrates suspension, resumption, values moving across the boundary, exception transfer, state checking, and normal completion.

Code
<?php

declare(strict_types=1);

$fiber = new Fiber(function (string $taskName): string {
    echo "Fiber started for {$taskName}.\n";

    // Send a value to the caller and pause this fiber.
    $resumeValue = Fiber::suspend('waiting for input');
    echo "Fiber received: {$resumeValue}\n";

    try {
        // Pause again so the caller can send an exception.
        Fiber::suspend('waiting for the next action');
    } catch (RuntimeException $exception) {
        echo "Fiber caught: {$exception->getMessage()}\n";
    }

    return 'fiber completed';
});

// Start runs the fiber until its first suspension.
$firstSignal = $fiber->start('report generation');
echo "Caller received: {$firstSignal}\n";

// Resume sends a value into the suspended Fiber::suspend call.
if ($fiber->isSuspended()) {
    $secondSignal = $fiber->resume('approved input');
    echo "Caller received: {$secondSignal}\n";
}

// Throw sends an exception into the current suspension point.
if ($fiber->isSuspended()) {
    $fiber->throw(new RuntimeException('operation cancelled'));
}

// A return value can be read only after normal termination.
if ($fiber->isTerminated()) {
    echo "Final result: {$fiber->getReturn()}\n";
}
Where it is used

Fibers are most useful inside asynchronous PHP libraries, event loop based network clients, socket servers, timer systems, concurrent service clients, and long running command line workers. A library can suspend the current fiber while an operation waits and resume it when the event loop reports readiness. This allows application code to look sequential while the library handles scheduling. Direct fiber control is usually kept inside the library because production scheduling also requires timeout handling, cancellation, cleanup, state checks, exception propagation, and protection from blocking operations.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands interruptible PHP functions, call stack preservation, value and exception transfer, and valid fiber states. It also tests whether the candidate knows that fibers are not threads, that scheduling is cooperative, and that asynchronous input and output still requires an event loop and suitable nonblocking operations.

Common interview mistakes

A common mistake is describing fibers as parallel threads. Only one fiber runs at a time on the same PHP thread unless a separate concurrency mechanism is also used. Another mistake is assuming that PHP schedules fibers automatically. A scheduler or event loop is still required. Developers may also put blocking database, file, sleep, or network calls inside fibers and expect other fibers to continue. They cannot continue while the thread is blocked. Other mistakes include calling resume or throw when the fiber is not suspended, calling start more than once, calling Fiber::suspend outside a running fiber, reading getReturn before normal completion, and failing to propagate cancellation or clean up resources.

Interview tip

Begin by saying that a fiber preserves a function's call stack while it is paused. Then explain values and exceptions crossing the suspension point. Finish by stating that fibers use cooperative scheduling, need an event loop for asynchronous input and output, and cannot prevent a normal blocking operation from blocking the PHP thread.

Interviewer may ask next
What happens when resume, throw, or getReturn is called in an invalid fiber state?

PHP throws FiberError for the invalid operation. Resume and throw require a currently suspended fiber. Start cannot be called after the fiber has already started. GetReturn requires a fiber that completed normally and returned. This matters because a scheduler must check and control fiber state rather than assuming that every fiber can always be resumed.

What are the main performance and memory tradeoffs of using many fibers?

Each live fiber consumes additional memory because it preserves its own call stack, local variables, and execution state. Starting and switching fibers also adds runtime overhead. The benefit is clearer control flow for many waiting operations. The tradeoff is not worthwhile for simple straight line work, and creating too many fibers can increase memory use and scheduling cost. Production libraries should limit unnecessary fibers and avoid blocking operations.

36. How would you design a reusable PHP API with correct covariance and contravariance?Language SpecificHard

Question Details

Explain return-type covariance, parameter contravariance, inheritance compatibility, Liskov substitution, union or intersection types, and examples of valid and invalid overrides.

Short Interview Answer (30-60 seconds)

I would design the parent contract around the minimum input capability the operation needs and the stable result that every implementation can guarantee. A child method may accept a less specific parameter type and may return a more specific type. It must not require a more specific input or return a less specific result. This keeps every child usable wherever the parent contract is expected.

Detailed Explanation

See the Code while reading this explanation.

A reusable API lets several classes follow the same promise. Any replacement class must accept every value that the original promise allowed. It may accept additional values. It must also return a value that is at least as specific as the promised result. These rules stop a replacement from surprising existing callers. They matter when an application has several handlers, processors, storage providers, or other interchangeable parts. The design should remain safe even when another team adds a new implementation later.

Useful Questions to Ask the Interviewer
  1. Which input capabilities does every implementation actually need?
  2. Which result type can every implementation always guarantee?
  3. Will external packages implement or extend this contract?
How would you design a reusable PHP API with correct covariance and contravariance? diagram
How to Explain It in an Interview

PHP supports full return covariance and parameter contravariance from PHP 7.4. An overriding method must remain compatible with the parent method or interface method.

Return covariance means a child may return a more specific type. If the parent returns Response, the child may return JsonResponse when JsonResponse implements Response. Every value returned by the child still satisfies the parent promise.

Parameter contravariance means a child may accept a less specific type. If the parent accepts JsonRequest, the child may accept Request when JsonRequest implements Request. The child still accepts every value that callers were allowed to pass through the parent contract.

The reverse changes are invalid. A child cannot accept only JsonRequest when the parent accepts Request. A caller may pass another Request implementation. A child also cannot return Response when the parent promises JsonResponse. A caller may rely on the more specific result.

Union and intersection types follow the same rules. Removing a member from a return union makes the result more specific. Adding a member to a parameter union makes the input less specific. Adding a member to a return intersection makes the result more specific. Removing a member from a parameter intersection makes the input less specific. PHP supports union types from PHP 8.0, intersection types from PHP 8.1, and combinations written in disjunctive normal form from PHP 8.2. PHP+2PHP+2

This is part of the Liskov substitution principle. A child must remain usable anywhere its parent is expected. PHP checks incompatible declared overrides while loading or compiling the class and raises a fatal compatibility error. Compatibility also covers required parameters, optional parameters, visibility, and other signature rules. Renaming an inherited parameter is allowed by the compatibility check, but it can break callers that use named arguments, so a public API should preserve parameter names. PHP

Variance does not copy objects or create an additional collection. It mainly affects declaration compatibility and normal argument and return type checks. The example performs constant extra work for method dispatch and type validation. Its response allocation and JSON encoding come from the method body, not from covariance or contravariance.

Example

Handler defines the contract used by callers. Its handle method accepts JsonRequest and returns Response. ApiHandler accepts Request, which is less specific than JsonRequest, so its parameter is contravariant. It returns JsonResponse, which is more specific than Response, so its return type is covariant. The commented invalid examples show the reversed changes. NarrowInputHandler would reject Request implementations allowed by its parent. BroadOutputHandler would weaken the JsonResponse result promised by its parent.

Code
<?php

declare(strict_types=1);

interface Request
{
    public function payload(): array;
}

final class JsonRequest implements Request
{
    public function __construct(private array $data)
    {
    }

    public function payload(): array
    {
        return $this->data;
    }
}

final class FormRequest implements Request
{
    public function __construct(private array $data)
    {
    }

    public function payload(): array
    {
        return $this->data;
    }
}

interface Response
{
    public function body(): string;
}

final class JsonResponse implements Response
{
    public function __construct(private array $data)
    {
    }

    public function body(): string
    {
        return json_encode($this->data, JSON_THROW_ON_ERROR);
    }
}

class Handler
{
    public function handle(JsonRequest $request): Response
    {
        return new JsonResponse($request->payload());
    }
}

final class ApiHandler extends Handler
{
    // Request is less specific than JsonRequest.
    // JsonResponse is more specific than Response.
    public function handle(Request $request): JsonResponse
    {
        return new JsonResponse([
            'success' => true,
            'data' => $request->payload(),
        ]);
    }
}

function runHandler(Handler $handler, JsonRequest $request): void
{
    echo $handler->handle($request)->body(), PHP_EOL;
}

runHandler(new ApiHandler(), new JsonRequest(['id' => 95]));

// Invalid parameter direction:
// class ParentProcessor
// {
//     public function process(Request $request): Response {}
// }
// class NarrowInputHandler extends ParentProcessor
// {
//     public function process(JsonRequest $request): Response {}
// }

// Invalid return direction:
// class JsonProcessor
// {
//     public function process(Request $request): JsonResponse {}
// }
// class BroadOutputHandler extends JsonProcessor
// {
//     public function process(Request $request): Response {}
// }
Where it is used

This design is useful for request handlers, serializers, message processors, repository contracts, payment providers, middleware, and extension points. A framework or application can depend on a general parent contract while an implementation accepts a broader supported input or returns a more specific result. For a public package, the contract should expose only capabilities that every implementation can honor. Changing parameter names, narrowing accepted values through manual checks, or changing exception behavior can still break callers even when the declared PHP types are compatible.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate can design inheritance based PHP APIs without breaking callers. It evaluates knowledge of method compatibility, return covariance, parameter contravariance, union and intersection types, named arguments, and the Liskov substitution principle. It also tests whether the candidate can separate a type safe declaration from a method that only appears safe but changes its promised behavior.

Common interview mistakes

A common mistake is reversing the variance directions. Narrowing a child parameter is unsafe because it may reject a value accepted by the parent. Broadening a child return type is unsafe because it weakens the result promised to callers. Another mistake is assuming that a compatible declaration automatically satisfies the complete contract. A child can still violate substitution by rejecting values with manual checks, changing important side effects, returning misleading data, or introducing unexpected exceptions. Developers also forget that changing parameter names can break named argument calls. Union and intersection compatibility must be judged by the complete set of accepted or returned values.

Interview tip

Start with substitution. Say that the child must accept at least what the parent accepts and return no less than what the parent promises. Then show one valid broader parameter and one valid narrower return. Finally, reverse each change to explain why PHP rejects the invalid overrides.

Interviewer may ask next
How do union and intersection types change override compatibility?

They use the same variance directions. A child return type may remove a union member or add an intersection member because that makes the result more specific. A child parameter may add a union member or remove an intersection member because that makes the accepted input less specific. The exact class relationships still matter, and PHP also rejects some redundant or invalid composite declarations. This matters because compatibility is based on the set of possible values, not only on how the declaration looks.

Does using covariance or contravariance add meaningful performance or memory cost?

No meaningful separate algorithmic or memory cost is normally introduced by variance itself. PHP must validate compatible declarations and perform the normal type checks used for arguments and return values. Variance does not by itself copy an object, allocate a collection, or change object handle behavior. Any important time or memory cost usually comes from the method body, such as creating a response object or encoding JSON. The tradeoff is API design complexity, because a very broad contract can provide weaker guarantees while a very narrow contract can reduce reuse.

37. How would you implement and validate a custom PHP stream wrapper?Language SpecificHard

Question Details

Explain wrapper registration, required methods, URL parsing, stream context, seeking and stat behavior, error handling, security boundaries, and tests for filesystem-style consumers.

Short Interview Answer (30-60 seconds)

I would register a unique scheme with stream_wrapper_register, define only the wrapper methods required by the operations I promise, and follow PHP stream contracts exactly. I would strictly validate the URL, modes, and context options, track the current byte position, implement correct read, write, seek, and stat behavior, and return false for normal failures while reporting warnings only when PHP requests them. I would validate the wrapper through fopen, fread, fwrite, fseek, fstat, stat, file_get_contents, and failure cases rather than calling wrapper methods directly.

Detailed Explanation

See the Code while reading this explanation.

A custom stream wrapper lets normal PHP file functions work with data that is not stored in a normal file. The data might come from memory, a private service, encrypted storage, or another controlled source. The main goal is to make that data behave like a file. Reading, writing, moving the current position, checking size, and reporting failures must produce results that callers expect. Before implementing it, I would ask:

Useful Questions to Ask the Interviewer
  1. Which file operations must be supported?
  2. Is the content read only or writable?
  3. Must seeking work in both directions?
  4. Which paths and context options are allowed?
  5. Must data survive after the PHP process ends?
How would you implement and validate a custom PHP stream wrapper? diagram
How to Explain It in an Interview

I would first choose a unique scheme such as memfs and register its class with stream_wrapper_register. PHP then creates the wrapper and calls methods such as stream_open, stream_read, stream_write, stream_seek, stream_stat, and url_stat when normal filesystem functions are used.

The exact methods depend on the promised behavior. Reading normally needs stream_open, stream_read, stream_eof, and stream_tell. Writing adds stream_write. Random access adds stream_seek. fstat calls stream_stat, while path based checks such as stat, file_exists, and filesize call url_stat.

PHP places the supplied stream context in the public context property. I would read it with stream_context_get_options and accept only documented options. I would parse the URL, allow only the expected scheme and host, decode and validate the path, reject dot segments, null bytes, credentials, ports, queries, fragments, and unsupported modes, and never map an untrusted wrapper path to an unrestricted local filesystem path.

The wrapper must track a byte position. stream_read returns no more than the requested byte count and advances the position by the bytes returned. stream_write returns the number of bytes accepted. In append mode, each write goes to the current end even if reading or seeking changed the visible position. stream_seek calculates a new nonnegative position for SEEK_SET, SEEK_CUR, or SEEK_END and returns true only when it succeeds.

stream_stat and url_stat return a stat compatible array containing numeric and named keys. The mode must identify the entry as a regular file, and size must match the stored byte length. url_stat must suppress warnings when STREAM_URL_STAT_QUIET is set. stream_open should report warnings only when STREAM_REPORT_ERRORS is set.

For this string backed example, reading allocates a result string proportional to the bytes returned. A write can copy much of the stored string, so its time and temporary memory cost can grow with file size. Large production objects should use chunked storage or a real backing service instead of repeatedly rebuilding one large string.

I would test successful reads and writes, every supported mode, append behavior, all seek origins, seeking before zero, seeking beyond the end, sparse writes, end detection, missing paths, quiet stat calls, context restrictions, stat cache behavior, repeated opens, and path escape attempts.

Example

This example registers memfs as a small process local memory filesystem. It accepts paths in the form memfs://main/path. It validates the scheme, host, path, URL parts, access mode, and a read_only context option. It supports reading, writing, append behavior, telling, end detection, seeking, resource stat calls, and path stat calls. Data is stored in a static PHP array and therefore exists only in the current PHP process. The validation section uses normal PHP filesystem functions so it tests the real wrapper integration. It also clears the PHP stat cache before checking path metadata.

Code
<?php

declare(strict_types=1);

final class MemoryStreamWrapper
{
    /** PHP assigns the supplied stream context to this property. */
    public $context;

    /** @var array<string, array{data: string, mtime: int}> */
    private static array $files = [];

    private string $key = '';
    private int $position = 0;
    private bool $readable = false;
    private bool $writable = false;
    private bool $appendWrites = false;

    public function stream_open(
        string $path,
        string $mode,
        int $options,
        ?string &$openedPath
    ): bool {
        $key = $this->parsePath($path);
        if ($key === null) {
            return $this->openFailure('Invalid memfs URL.', $options);
        }

        /* Accept the standard r, w, a, x, and c modes with optional +, b, or t. */
        if (preg_match('/^(?:[rwaxc](?:[bt]?\\+|\\+[bt]?|[bt]?)|[rwaxc])$/', $mode) !== 1) {
            return $this->openFailure('Unsupported stream mode.', $options);
        }

        $baseMode = str_replace(['b', 't'], '', $mode);
        $allowedModes = [
            'r', 'r+', 'w', 'w+', 'a',
            'a+', 'x', 'x+', 'c', 'c+',
        ];

        if (!in_array($baseMode, $allowedModes, true)) {
            return $this->openFailure('Unsupported stream mode.', $options);
        }

        $contextOptions = is_resource($this->context)
            ? stream_context_get_options($this->context)
            : [];

        $readOnly = (bool) ($contextOptions['memfs']['read_only'] ?? false);

        $this->readable = $baseMode === 'r' || str_contains($baseMode, '+');
        $this->writable = $baseMode !== 'r';
        $this->appendWrites = str_starts_with($baseMode, 'a');

        if ($readOnly && $this->writable) {
            return $this->openFailure('The stream context is read only.', $options);
        }

        $exists = array_key_exists($key, self::$files);
        $firstModeCharacter = $baseMode[0];

        if ($firstModeCharacter === 'r' && !$exists) {
            return $this->openFailure('The requested path does not exist.', $options);
        }

        if ($firstModeCharacter === 'x' && $exists) {
            return $this->openFailure('The requested path already exists.', $options);
        }

        if ($firstModeCharacter === 'w' || $firstModeCharacter === 'x') {
            self::$files[$key] = [
                'data' => '',
                'mtime' => time(),
            ];
        } elseif (
            ($firstModeCharacter === 'a' || $firstModeCharacter === 'c')
            && !$exists
        ) {
            self::$files[$key] = [
                'data' => '',
                'mtime' => time(),
            ];
        }

        $this->key = $key;
        $this->position = $this->appendWrites
            ? strlen(self::$files[$key]['data'])
            : 0;

        /* openedPath is needed when PHP requested include path resolution. */
        if (($options & STREAM_USE_PATH) !== 0) {
            $openedPath = $path;
        }

        return true;
    }

    public function stream_read(int $count): string|false
    {
        if (!$this->readable || $count < 0) {
            return false;
        }

        $chunk = substr(
            self::$files[$this->key]['data'],
            $this->position,
            $count
        );

        $this->position += strlen($chunk);
        return $chunk;
    }

    public function stream_write(string $data): int|false
    {
        if (!$this->writable) {
            return false;
        }

        $current = self::$files[$this->key]['data'];

        /* Append modes always write at the current end of the file. */
        $writeAt = $this->appendWrites
            ? strlen($current)
            : $this->position;

        /* A write after seeking beyond the end creates a null byte gap. */
        if ($writeAt > strlen($current)) {
            $current .= str_repeat("\0", $writeAt - strlen($current));
        }

        $prefix = substr($current, 0, $writeAt);
        $suffixStart = $writeAt + strlen($data);
        $suffix = $suffixStart < strlen($current)
            ? substr($current, $suffixStart)
            : '';

        self::$files[$this->key] = [
            'data' => $prefix . $data . $suffix,
            'mtime' => time(),
        ];

        $this->position = $writeAt + strlen($data);
        return strlen($data);
    }

    public function stream_tell(): int
    {
        return $this->position;
    }

    public function stream_eof(): bool
    {
        return $this->position >= strlen(self::$files[$this->key]['data']);
    }

    public function stream_seek(int $offset, int $whence = SEEK_SET): bool
    {
        $size = strlen(self::$files[$this->key]['data']);

        $newPosition = match ($whence) {
            SEEK_SET => $offset,
            SEEK_CUR => $this->position + $offset,
            SEEK_END => $size + $offset,
            default => -1,
        };

        if ($newPosition < 0) {
            return false;
        }

        $this->position = $newPosition;
        return true;
    }

    /** @return array<int|string, int>|false */
    public function stream_stat(): array|false
    {
        if (!array_key_exists($this->key, self::$files)) {
            return false;
        }

        return $this->makeStat(self::$files[$this->key]);
    }

    /** @return array<int|string, int>|false */
    public function url_stat(string $path, int $flags): array|false
    {
        $key = $this->parsePath($path);

        if ($key === null || !array_key_exists($key, self::$files)) {
            if (($flags & STREAM_URL_STAT_QUIET) === 0) {
                trigger_error(
                    'The requested memfs path does not exist.',
                    E_USER_WARNING
                );
            }

            return false;
        }

        return $this->makeStat(self::$files[$key]);
    }

    public function stream_close(): void
    {
    }

    private function parsePath(string $url): ?string
    {
        if (str_contains($url, "\0")) {
            return null;
        }

        $parts = parse_url($url);

        if ($parts === false || ($parts['scheme'] ?? '') !== 'memfs') {
            return null;
        }

        if (
            isset($parts['user'])
            || isset($parts['pass'])
            || isset($parts['port'])
            || isset($parts['query'])
            || isset($parts['fragment'])
        ) {
            return null;
        }

        $host = $parts['host'] ?? '';
        $path = rawurldecode($parts['path'] ?? '');

        if (
            $host !== 'main'
            || $path === ''
            || !str_starts_with($path, '/')
            || str_contains($path, "\0")
        ) {
            return null;
        }

        foreach (explode('/', $path) as $segment) {
            if ($segment === '.' || $segment === '..') {
                return null;
            }
        }

        return $host . $path;
    }

    /**
     * @param array{data: string, mtime: int} $file
     * @return array<int|string, int>
     */
    private function makeStat(array $file): array
    {
        $size = strlen($file['data']);
        $mode = 0100000 | 0660;
        $mtime = $file['mtime'];

        $numericValues = [
            0,
            0,
            $mode,
            1,
            0,
            0,
            0,
            $size,
            $mtime,
            $mtime,
            $mtime,
            0,
            0,
        ];

        return $numericValues + [
            'dev' => 0,
            'ino' => 0,
            'mode' => $mode,
            'nlink' => 1,
            'uid' => 0,
            'gid' => 0,
            'rdev' => 0,
            'size' => $size,
            'atime' => $mtime,
            'mtime' => $mtime,
            'ctime' => $mtime,
            'blksize' => 0,
            'blocks' => 0,
        ];
    }

    private function openFailure(string $message, int $options): false
    {
        /* stream_open should warn only when PHP requests error reporting. */
        if (($options & STREAM_REPORT_ERRORS) !== 0) {
            trigger_error($message, E_USER_WARNING);
        }

        return false;
    }
}

if (in_array('memfs', stream_get_wrappers(), true)) {
    throw new RuntimeException('The memfs scheme is already registered.');
}

if (!stream_wrapper_register('memfs', MemoryStreamWrapper::class)) {
    throw new RuntimeException('Could not register memfs.');
}

try {
    $context = stream_context_create([
        'memfs' => [
            'read_only' => false,
        ],
    ]);

    $handle = fopen(
        'memfs://main/demo.txt',
        'w+',
        false,
        $context
    );

    if ($handle === false) {
        throw new RuntimeException('Could not open the test stream.');
    }

    fwrite($handle, 'PHP stream wrapper');
    fseek($handle, 4, SEEK_SET);

    echo fread($handle, 6) . PHP_EOL;
    echo fstat($handle)['size'] . PHP_EOL;

    fclose($handle);

    clearstatcache(true, 'memfs://main/demo.txt');

    echo file_get_contents('memfs://main/demo.txt') . PHP_EOL;
    echo stat('memfs://main/demo.txt')['size'] . PHP_EOL;
} finally {
    stream_wrapper_unregister('memfs');
}
Where it is used

Custom stream wrappers are useful when existing PHP code or a third party library already expects a stream resource or file style URL. Examples include controlled in memory files, encrypted content, generated documents, archive entries, remote object storage, and test fixtures. They should be avoided when a normal service object would make latency, retries, authentication, and failures clearer, or when the implementation cannot honestly support the filesystem operations expected by consumers.

Why Interviewers Ask This

Interviewers ask this to test whether the candidate understands how PHP connects filesystem functions to user defined wrapper methods. It also evaluates method contracts, URL and context validation, stream position handling, seek and stat behavior, error reporting, security boundaries, testing strategy, and awareness of production performance costs.

Common interview mistakes

Common mistakes include using a scheme that can collide with another wrapper, implementing methods without defining which operations are supported, accepting malformed modes, trusting URL parts, ignoring encoded dot segments, and exposing local files through untrusted paths. Other mistakes are advancing the byte position incorrectly, allowing negative seeks, assuming append writes use the current seek position, returning an incomplete stat array, using the wrong mode bits, warning during quiet url_stat calls, warning from stream_open without STREAM_REPORT_ERRORS, ignoring PHP stat caching, hiding large string copy costs, and testing methods directly instead of using real filesystem consumers.

Interview tip

Explain the wrapper as a contract between PHP filesystem functions and your class. Start with the operations you support. Then cover registration, mode and URL validation, context options, byte position rules, append and seek behavior, stat data, error flags, security boundaries, performance costs, and integration tests. State clearly that unsupported operations should return failure rather than pretend to work.

Interviewer may ask next
What should happen when a caller seeks beyond the current end and then writes?

The exact behavior should be documented by the wrapper. In this implementation, the seek succeeds because the new position is nonnegative. A later write fills the gap with null bytes, writes the supplied data, updates the modification time and size, and moves the position after the written bytes. This matters because later reads, fstat, and stat must all describe the same byte layout. The tradeoff is that a large gap allocates memory, so a production wrapper may reject very large positions or store sparse ranges without creating every gap byte.

When should a dedicated storage client be used instead of a custom stream wrapper?

A dedicated client should be used when explicit network latency, retries, authentication, transactions, or partial failures matter more than compatibility with filesystem consumers. A wrapper is useful when existing code already requires fopen style streams, but it can hide expensive work behind simple file functions and requires accurate mode, seek, stat, cache, and error behavior. The main tradeoff is compatibility versus clarity. I would use the wrapper only when its integration benefit is greater than its hidden operational complexity.

38. How do property hooks and asymmetric visibility affect PHP object design?Language SpecificHard

Question Details

Explain controlled property access, read versus write visibility, invariants, inheritance implications, reflection or serialization considerations, and when methods remain clearer.

Short Interview Answer (30-60 seconds)

Property hooks control what PHP does when a property is read or written. Asymmetric visibility separately controls which scopes may read and write that property. Together, they can keep validation and calculated values close to the object state while allowing public reads and restricted writes. I use them for small property focused rules. I prefer named methods when a change represents an important business action, affects several values, performs external work, or has complex failure behavior.

Detailed Explanation

The practical decision is whether a value should behave like simple object data or like an important action. PHP can let outside code read a value while limiting who may replace it. PHP can also check, clean, transform, or calculate a value whenever it is written or read. This helps an object prevent invalid state without requiring repetitive access methods. However, property syntax looks simple to the caller. Complex work hidden inside it can make code surprising, slow, or difficult to test. Important business actions should therefore remain clearly named methods.

Useful Questions to Ask the Interviewer
  1. Should outside code be able to change the property?
  2. Is the value stored or calculated from other state?
  3. May child classes replace the access behavior?
  4. Will reflection or serialization tools handle the object?
How do property hooks and asymmetric visibility affect PHP object design? diagram
How to Explain It in an Interview

Property hooks were added in PHP 8.4. A get hook controls a read, and a set hook controls an assignment. A backed property stores its own value because one of its hooks directly accesses that same property. A virtual property does not store a separate value. It usually calculates a result from other state or redirects access elsewhere. Virtual properties therefore require no property storage slot. ([php.net](https://www.php.net/language.oop5.property-hooks.php))

Asymmetric visibility was also added for object properties in PHP 8.4. A declaration such as public protected(set) string $status allows public reads, but only the class and its child classes may write. Separate set visibility is allowed only on typed properties, and it cannot be more permissive than read visibility. A private(set) property is implicitly final and cannot be redeclared by a child class. PHP 8.5 extended asymmetric visibility to static properties. ([php.net](https://www.php.net/releases/8.5/en.php))

These features help protect invariants. For example, a set hook can reject a negative amount before storing it. A get hook can expose a calculated total. The hook code still runs on every relevant access, so its performance and failure behavior depend on the code inside it. Small validation or calculation is normally suitable. Database calls, network calls, event publishing, or changes to several properties are clearer as methods. Virtual properties save the storage for that property, while backed properties use normal property storage.

Inheritance needs care. A child may redefine individual hooks or widen allowed visibility unless the property or relevant hook is final. Adding hooks in a child also removes an inherited default value unless the child declares that default again. ([php.net](https://www.php.net/language.oop5.property-hooks.php))

ReflectionProperty getValue and setValue use hooks. The raw reflection operations bypass hooks for backed properties and fail for virtual properties. Normal serialize and unserialize use raw values, while json_encode and get_object_vars use get hooks. Explicit __serialize and __unserialize methods are safest when the stored form must preserve clear invariants. ([php.net](https://www.php.net/manual/en/reflectionproperty.setrawvalue.php))

Where it is used

These features are useful in value objects, domain models, data transfer objects, configuration objects, framework entities, and public library APIs. Typical uses include validating a price, normalizing an email address, exposing an identifier that only the class may assign, calculating a display name, publishing a public read only status, and allowing controlled writes from child classes. They are less suitable when an operation changes several parts of an aggregate, performs input or output, requires authorization, or represents a major business command.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands modern PHP property access, object invariants, inheritance rules, reflection behavior, serialization behavior, and API design. They also want to know whether the candidate can use concise property syntax without hiding complex business operations behind an apparently simple property read or write.

Common interview mistakes

Common mistakes include assuming every property with hooks has stored data, placing database or network work inside a get hook, and expecting hooks to work with readonly properties even though PHP does not allow that combination. Other mistakes include using separate set visibility on an untyped property, making set visibility wider than read visibility, forgetting that private(set) makes a property final, and assuming child classes can freely replace final hooks. Developers may also expect every reflection or serialization operation to run hooks. Raw reflection access and normal serialize or unserialize bypass them. Indirect array element modification and references also need special care because they can bypass an ordinary set operation.

Interview tip

Separate the answer into two ideas. First, explain that hooks control behavior during access. Second, explain that asymmetric visibility controls which scope may write. Then connect both ideas to invariants, inheritance, reflection, serialization, performance, memory, and the decision to use a named method for complex work.

Interviewer may ask next
What happens when reflection or serialization accesses a hooked property?

Normal ReflectionProperty getValue and setValue use the get and set hooks. ReflectionProperty getRawValue and setRawValue bypass those hooks for backed properties, and they throw an Error for virtual properties because no raw value exists. The property type is still enforced when setRawValue is used. Normal serialize and unserialize use raw backing values, while json_encode and get_object_vars use get hooks. This matters because a hydration or persistence tool can bypass validation or store a value different from the public representation. Explicit __serialize and __unserialize methods make that boundary clearer. ([php.net](https://www.php.net/manual/en/reflectionproperty.setrawvalue.php))

When is a named method clearer than a property hook?

A named method is clearer when the change represents a business command rather than simple property access. Examples include approving an order, checking authorization, changing several related properties, writing to a database, calling a remote service, or publishing an event. A method gives the operation an explicit name and makes its side effects and failure cases visible. A hook is better for small local validation, normalization, or calculation. The tradeoff is more method boilerplate in exchange for a clearer and less surprising API.

39. What is Big O notation, and why does it matter when comparing PHP solutions?CodingEasy

Question Details

Define Big O notation as a way to describe how running time or extra-space use grows with input size. Explain O(1), O(log n), O(n), O(n log n), and O(n²) using small PHP operations, distinguish growth rate from exact wall-clock time, and show how input constraints and PHP array operations influence solution choice.

Short Interview Answer (30-60 seconds)

Big O notation tells me how the running time or extra memory of a solution grows as the input size n grows. I compare growth rates such as O(1), O(log n), O(n), O(n log n), and O(n²), rather than exact milliseconds. In PHP, operation cost also matters. Associative-array key access is O(1) on average, while array_search() is O(n). I use the expected input size and these operation costs to choose a solution that scales well.

Detailed Explanation

See the Code while reading this explanation.

This question asks how we judge whether a PHP solution will still work well when the amount of data becomes larger. Big O describes how the amount of work or extra memory grows with input size n. It does not predict an exact number of milliseconds. We compare growth patterns instead. The diagram shows five common classes: O(1), O(log n), O(n), O(n log n), and O(n²). It also shows that PHP array operations have different costs, so those costs must be included when comparing solutions.

Useful Questions to Ask the Interviewer
  1. What is the largest expected input size n?
  2. Should I compare running time, extra memory, or both?
  3. Can I use the usual average-case cost for PHP associative-array key access?
What is Big O notation, and why does it matter when comparing PHP solutions? diagram
How to Explain It in an Interview
1. Explain what Big O measures

Big O describes how running time or extra-space use grows when n grows. It describes a growth rate, not an exact wall-clock time. Two solutions with the same Big O can still take different amounts of real time because their constant work and execution environment can differ.

2. Compare the five growth classes

O(1) is constant growth. The diagram shows accessing a key in a PHP associative array, such as $arr['id_123']. The amount of work does not grow with n for the usual average lookup case.

O(log n) is logarithmic growth. The diagram uses binary search on a sorted array. The search keeps left and right boundaries, calculates a midpoint, and removes about half of the remaining search area after each comparison.

O(n) is linear growth. The diagram shows a foreach loop that visits every item once and adds each value to $sum.

O(n log n) grows faster than linear but much slower than quadratic growth. The diagram shows sorting the array [5, 2, 9, 1, 5] with sort().

O(n²) is quadratic growth. The diagram shows two nested loops. Each outer-loop iteration runs an inner loop n times, so the work grows roughly with n multiplied by n.

3. Use n = 1,000 to see the difference

The diagram gives simple teaching estimates for n = 1,000. O(1) is about one operation. O(log n) is about 10 steps because log2(1000) is about 10. O(n) is about 1,000 operations. O(n log n) is about 10,000 operations. O(n²) is about 1,000,000 operations. These values show relative growth. They are not exact execution times.

4. Include the cost of PHP array operations

The operation inside a loop matters. The diagram shows associative-array key access as O(1) on average. in_array() and array_search() are O(n) because they may scan through the array. array_push() is amortized O(1). array_shift() is O(n). sort() is shown as O(n log n). If an O(n) operation is placed inside an O(n) loop, the full solution can become O(n²).

5. Let input constraints guide the choice

For a small n, a simple solution with a higher growth rate may still finish quickly. For a large n, the difference becomes important. The diagram therefore recommends preferring O(n) or better over O(n²) when n can be large, when the problem allows that choice.

6. Compare time and extra space separately

Big O can describe running time and auxiliary space. Auxiliary space means extra memory used by the algorithm. A solution may use extra memory to reduce repeated work. The right choice depends on both the input constraints and the available memory.

7. State the main takeaway

I first identify n. Then I look at the loops and the PHP operations used inside them. I combine those costs to find the overall growth rate. This lets me choose a solution that should remain practical as the input grows.

Key Insight / Why This Solution Works

The key idea is to compare growth as n increases. O(1) stays roughly constant. O(log n) grows slowly because each binary-search step removes about half of the remaining range. O(n) processes about one unit of work for each input item. O(n log n) is typical of efficient comparison sorting. O(n²) commonly appears when two loops process many pairs. The central rule is that the overall complexity must include the cost of operations inside the loops. For example, PHP associative-array key access is O(1) on average, but array_search() is O(n). Input constraints then tell us which growth rate is acceptable.

Code
<?php

// Big O notation examples in PHP.
// These examples follow the operations shown in the diagram.

// ------------------------------------------------------------
// O(1): average associative-array key access.
// ------------------------------------------------------------
$arr = [
    'id_123' => 'Alice',
    'id_456' => 'Bob',
];

$value = $arr['id_123'];
echo "O(1) key access: {$value}\n";

// ------------------------------------------------------------
// O(log n): binary search on a sorted array.
// The search keeps the same $i, $j, and $m structure shown
// in the diagram and removes about half of the range each step.
// ------------------------------------------------------------
function binarySearch(array $arr, int $target): int
{
    $i = 0;
    $j = count($arr) - 1;

    while ($i <= $j) {
        $m = intdiv($i + $j, 2);

        if ($arr[$m] === $target) {
            return $m;
        }

        if ($arr[$m] < $target) {
            $i = $m + 1;
        } else {
            $j = $m - 1;
        }
    }

    return -1;
}

// n = 1,000 gives about 10 binary-search steps in the
// teaching estimate shown in the diagram.
$sorted = range(1, 1000);
$foundIndex = binarySearch($sorted, 1000);
echo "O(log n) binary-search index: {$foundIndex}\n";

// ------------------------------------------------------------
// O(n): loop through all items once.
// ------------------------------------------------------------
$arr = range(1, 1000);
$sum = 0;

foreach ($arr as $x) {
    $sum += $x;
}

echo "O(n) visited items: " . count($arr) . "\n";

// ------------------------------------------------------------
// O(n log n): efficient comparison sorting.
// This is the exact example array shown in the diagram.
// ------------------------------------------------------------
$arr = [5, 2, 9, 1, 5];
sort($arr);

echo "O(n log n) sorted example: " . implode(', ', $arr) . "\n";

// ------------------------------------------------------------
// O(n^2): nested loops comparing or processing pairs.
// With n = 1,000, this performs 1,000,000 inner operations.
// ------------------------------------------------------------
$n = 1000;
$pairOperations = 0;

for ($i = 0; $i < $n; $i++) {
    for ($j = 0; $j < $n; $j++) {
        $pairOperations++;
    }
}

echo "O(n^2) pair operations: {$pairOperations}\n";

// ------------------------------------------------------------
// Teaching estimates from the diagram for n = 1,000.
// They compare growth rates, not exact wall-clock time.
// ------------------------------------------------------------
$n = 1000;

echo "\nApproximate growth comparison for n = {$n}:\n";
echo "O(1): about 1 operation\n";
echo "O(log n): about " . round(log($n, 2)) . " operations\n";
echo "O(n): about {$n} operations\n";
echo "O(n log n): about " . round($n * log($n, 2)) . " operations\n";
echo "O(n^2): about " . ($n * $n) . " operations\n";

// Common PHP array-operation costs shown in the diagram:
// $arr[$key]                 -> O(1) average
// in_array($value, $arr, true) -> O(n)
// array_search($value, $arr, true) -> O(n)
// array_push($arr, $value)   -> amortized O(1)
// array_shift($arr)          -> O(n)
// sort($arr)                 -> O(n log n) in this comparison
Time & Space Complexity

This question compares several complexities instead of having one final complexity. O(1) means the work stays about the same as n grows. O(log n) grows very slowly. O(n) grows directly with n. O(n log n) grows a little faster than linear. O(n²) grows with the square of n and becomes expensive quickly. For n = 1,000, the diagram estimates about 1, 10, 1,000, 10,000, and 1,000,000 operations respectively. PHP associative-array key access is O(1) on average. in_array() and array_search() are O(n), array_push() is amortized O(1), array_shift() is O(n), and sort() is shown as O(n log n). Time growth and auxiliary-space growth should be discussed separately.

Where it is used

Big O is useful whenever PHP code may handle growing amounts of data. It helps when choosing searching, sorting, loops, associative-array lookups, queues, caches, and collection-processing code. It is especially useful when two solutions both work correctly but one performs much more work as n becomes large.

Why Interviewers Ask This

Interviewers ask this to see whether you can reason about scalability instead of judging code only by a small test. They want to know whether you understand common growth rates, can separate Big O from exact execution time, recognize the cost of PHP array operations, and use input constraints when choosing a solution. They also check whether you can discuss both running time and extra memory and whether you describe associative-array lookup as average O(1) rather than a guaranteed constant-time operation.

Common interview mistakes
  1. Treating Big O as an exact number of milliseconds instead of a growth rate.
  2. Counting only visible loops and ignoring the cost of PHP operations inside them. For example, array_search() inside an n-item loop can lead to O(n²) work.
  3. Assuming every PHP array operation is O(1). in_array(), array_search(), and array_shift() are not constant-time operations.
  4. Calling PHP associative-array access guaranteed O(1) in every case instead of saying O(1) on average.
  5. Choosing a solution without first considering how large n can become.
Interview tip

When comparing PHP solutions, first define n. Then state the cost of each important loop and PHP array operation. Finally, combine those costs and compare the result with the expected input size.

Interviewer may ask next
Why can using array_search() inside a loop change an O(n) solution into O(n²)?

array_search() is O(n) because it may scan through the array. If an outer loop also runs n times and calls array_search() each time, the total work is n multiplied by n, so it becomes O(n²). If the problem allows it, an associative lookup table can sometimes replace repeated searches. Building that table uses O(n) extra memory, while each key lookup is O(1) on average, so the overall time can become O(n) expected time.

How does the maximum input size affect which Big O complexity is acceptable?

For a small input, even O(n²) may be fast enough. For a large input, quadratic growth becomes expensive very quickly. With n = 1,000, the diagram compares about 1,000 operations for O(n) with about 1,000,000 for O(n²). When n can be large, I prefer a lower growth rate such as O(n) or O(n log n) when the problem allows it. The tradeoff may be more code or more auxiliary memory.

40. Write a PHP function to reverse a string without using strrev().CodingEasy

Question Details

Given a UTF-8-safe requirement only if explicitly supported by your approach, return the characters in reverse order. Explain empty input, complexity, and any distinction between bytes and Unicode characters.

Short Interview Answer (30-60 seconds)

I would first split the valid UTF-8 string into Unicode characters with preg_split('//u', ...). Then I would traverse that character array from the last index to index 0 and append each character to a result array. Finally, I would join the result array with implode(). This works because the result array always contains the processed suffix in reverse order. The solution takes O(n) time and O(n) auxiliary space, where n is the number of characters.

Detailed Explanation

See the Code while reading this explanation.

The function receives a string and returns the same characters in reverse order without using strrev(). An empty string returns an empty string. Normal PHP string indexing works with bytes, so reading a UTF-8 string backward one byte at a time can damage a multi-byte character such as é. The selected solution first separates valid UTF-8 text into Unicode characters. It then visits those characters from right to left and joins them into the reversed result. For example, café becomes éfac.

Useful Questions to Ask the Interviewer
  1. Should the function support valid UTF-8 text, or only ASCII text?
  2. Should an empty string return an empty string?
  3. Is reversing Unicode code points enough, or must combined grapheme clusters remain together?
Write a PHP function to reverse a string without using strrev(). diagram
How to Explain It in an Interview
1. Understand the input and required output

The input is one string. The output is another string containing the same characters in reverse order. For the example, the input is café and the required output is éfac. The function must not use strrev(). If the input is empty, the function returns an empty string.

2. Choose the algorithm and data structure

PHP string indexing and strlen work with bytes. A multi-byte UTF-8 character can therefore be split incorrectly if the code reads the raw string backward. The solution uses preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY) to create an array of Unicode characters. It then traverses that array backward and stores the characters in a second array named reversedChars.

The central invariant is that reversedChars always contains the processed suffix of the input in correct reverse order.

3. Initialize the state

If text is empty, the function returns an empty string immediately. Otherwise, café is split into [c, a, f, é]. The character count is 4. reversedChars begins as an empty array. Traversal starts at index 3, which contains é, and moves left toward index 0.

4. Walk through the example

Step 1: i is 3. chars[3] is é. The state before the action is []. The code appends é. The state becomes [é].

Step 2: i is 2. chars[2] is f. The state before the action is [é]. The code appends f. The state becomes [é, f].

Step 3: i is 1. chars[1] is a. The state before the action is [é, f]. The code appends a. The state becomes [é, f, a].

Step 4: i is 0. chars[0] is c. The state before the action is [é, f, a]. The code appends c. The state becomes [é, f, a, c].

The next value of i is -1, so the condition i >= 0 becomes false and the loop stops. The code joins [é, f, a, c] and returns éfac.

5. Explain why the result is correct

The loop starts at the last character and moves left one position at a time. Each visited character is appended to reversedChars. After every iteration, reversedChars contains the part already visited in reverse order. When the loop finishes, every character has been visited exactly once and the array contains the complete reversed sequence. Joining the array therefore produces the correct result.

6. Explain the PHP implementation

The function first handles empty input. It then uses preg_split with the u modifier to split valid UTF-8 text into Unicode characters. PREG_SPLIT_NO_EMPTY prevents empty array entries. count($chars) gives the number of characters. The for loop starts at the final valid index, count($chars) - 1, and decreases i after every iteration. Each current character is appended to reversedChars. Finally, implode('', $reversedChars) joins the characters without adding a separator.

7. Explain complexity and edge cases

Splitting the string, traversing the character array, and joining the result each require work proportional to the input size. The total time complexity is O(n). The character array and reversed array grow with the input, so the auxiliary space complexity is O(n). Empty input returns immediately. One character returns unchanged. Repeated characters reverse normally. This approach reverses Unicode code points, but a visible grapheme made from several code points may require specialized grapheme-aware functions.

Key Insight / Why This Solution Works

The key idea is to avoid reversing raw PHP string bytes. The code first converts valid UTF-8 text into an array whose entries represent Unicode characters. It then visits that array from the final index down to index 0 and appends each entry to reversedChars. The invariant is that reversedChars always contains the processed suffix in correct reverse order. Once every character has been appended, joining the array produces the reversed string. This method does not use strrev() and does not split a multi-byte character such as é into separate bytes.

Code
<?php

declare(strict_types=1);

/**
 * Reverse a valid UTF-8 string without using strrev().
 */
function reverseString(string $text): string
{
    // Step 1: Return immediately when the input is empty.
    if ($text === '') {
        return '';
    }

    // Step 2: Split the valid UTF-8 string into Unicode characters.
    // PREG_SPLIT_NO_EMPTY removes empty entries from the result.
    $chars = preg_split('//u', $text, -1, PREG_SPLIT_NO_EMPTY);

    // Step 3: Create the array that will store characters in reverse order.
    $reversedChars = [];

    // Step 4: Start at the final character and move toward index 0.
    for ($i = count($chars) - 1; $i >= 0; $i--) {
        // Step 5: Append the current character to the result array.
        $reversedChars[] = $chars[$i];
    }

    // Step 6: Join the reversed characters without a separator.
    return implode('', $reversedChars);
}

// Example from the diagram.
$input = 'café';
$result = reverseString($input);

echo "Input: {$input}\n";
echo "Reversed: {$result}\n";
// Expected reversed value: éfac
Time & Space Complexity

Let n be the number of Unicode characters produced from the valid UTF-8 input. Splitting the string, walking backward through the array, and joining the result each take work proportional to n. The total time complexity is O(n). The code stores the split character array and another array containing the reversed characters. These arrays grow with the input, so the auxiliary space complexity is O(n). Auxiliary space means extra memory used while the function runs.

Where it is used

This pattern is useful when software must process valid UTF-8 text by Unicode characters instead of raw bytes. Similar backward traversal appears in text transformations, token processing, character-sequence utilities, and interview problems that test index handling. For user-facing text containing combined emoji or accented grapheme clusters, a grapheme-aware library may be required.

Why Interviewers Ask This

The interviewer is checking whether the candidate can write a correct backward traversal and maintain a simple invariant. The problem also tests PHP-specific string knowledge. A strong answer explains that ordinary PHP string indexing works with bytes and clearly states what level of Unicode support the solution provides. The interviewer can also evaluate empty-input handling, off-by-one errors, valid PHP syntax, array construction, and accurate O(n) time and O(n) auxiliary-space analysis.

Common interview mistakes

A common mistake is using strlen() and $text[$i] while claiming the solution is UTF-8 safe. Those operations work with bytes and can break a multi-byte character such as é. Another mistake is starting the loop at count($chars), which is one position after the final valid index. The correct starting index is count($chars) - 1. Candidates may also use a condition that skips index 0, forget the empty-input case, or claim O(1) auxiliary space even though the arrays grow with the input. Another mistake is claiming full grapheme-cluster support when preg_split('//u', ...) reverses Unicode code points rather than every possible user-perceived character.

Interview tip

Explain the Unicode decision before writing the loop. State that ordinary PHP string indexes are bytes, so the code first splits valid UTF-8 text into characters. Then trace café as [c, a, f, é] using the exact index order 3, 2, 1, 0.

Interviewer may ask next
How would you reverse user-perceived characters such as emoji sequences or letters built from multiple Unicode code points?

preg_split('//u', ...) separates Unicode code points, but one visible grapheme can contain several code points. For full grapheme-cluster handling, I would use PHP grapheme functions from the intl extension to read complete grapheme clusters, store them in an array, traverse that array backward, and join it. The invariant remains the same because the result array contains processed grapheme clusters in reverse order. The time complexity remains O(n), and the auxiliary space remains O(n). The tradeoff is the need for the intl extension and more specialized code.

Can this solution reduce auxiliary space to O(1)?

Not while keeping the same immutable string-return contract and the diagram's UTF-8 array approach. The function must create a new reversed result, and PHP strings are not mutable arrays of Unicode characters. Directly prepending or repeatedly concatenating characters could remove one array, but it may repeatedly copy a growing string and produce O(n²) time. The shown approach keeps O(n) time by storing characters in arrays and performing one final implode, with the tradeoff of O(n) auxiliary space.

More questions load as you scroll

Disclaimer: This interview guide is for educational and informational purposes only. It is designed to help readers prepare, but it does not guarantee any interview result, hiring decision, offer, or outcome. Interview questions, hiring criteria, and preferred answers can vary by employer, interviewer, industry, location, and time. The examples and explanations reflect the authors' research and judgment, are provided without warranties of any kind, and should not be treated as the only correct approach. Diagrams are simplified illustrations intended to highlight the main components and their interactions; actual systems and implementations may be more complex. Alternative approaches may be equally valid or better suited to a particular question, context, or interviewer. To the fullest extent permitted by applicable law, the author, contributors, and publisher are not liable for decisions made, actions taken, or losses incurred based on this guide.

Content Accuracy and Verification: To the fullest extent permitted by applicable law, we do not represent or warrant that interview guides, questions, answers, examples, or diagrams are accurate, complete, current, error-free, or suitable for any particular purpose. You are responsible for independently reviewing and verifying the information before relying on it.