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.
Identity, Image, and Privacy Notice
To respect individual privacy, some names, profile photographs, avatars, biographical details, and other identifying information displayed in this guide may be replaced with pseudonyms, licensed stock images, illustrative avatars, composite images, or representative descriptions. Unless a person is expressly identified as an actual contributor, a displayed name, image, or profile should not be understood as depicting or identifying a specific candidate, interviewer, employee, or other real individual. These representations are provided for editorial and illustrative purposes only and do not imply endorsement, employment, participation, or affiliation with this guide or any company mentioned in it. Any resemblance to an actual person is coincidental.
Company Notice
This guide is an independent educational resource and is not affiliated with, endorsed by, sponsored by, or approved by the company named in this guide. Company names are used only to identify interview experiences commonly reported by candidates. Interview practices can change without notice, and inclusion of company-specific content does not mean these questions are official, complete, or guaranteed to be asked. To the fullest extent permitted by law, the author, contributors, and publisher are not responsible for outcomes related to use of this material.
Questions or comments?
Contact us for general questions, or share feedback, technical corrections, and comments with the community.
31. Explain PHP session locking and its effect on concurrent requests.Language SpecificHard
i Question Details
Describe when the session lock is acquired and released, why requests sharing a session can serialize, session_write_close, read-only access, correctness risks, and measurement.
Short Interview Answer (30-60 seconds)
With PHP's default file based session handler, session_start obtains an exclusive lock for the session before returning its data. Requests using the same session identifier can therefore wait and run one after another. I keep the session open only while reading or changing the required values, then call session_write_close before slow work. For true read only access, I can use read_and_close. Custom handlers may use different locking rules, so I verify their behavior and measure the wait around session_start.
The main problem is that two actions from the same user may have to wait for each other when both use the same saved login information. One action temporarily reserves that information while it reads or changes it. A second action then waits until the first action finishes with it. This can make a page, an upload, or a background update feel slow even when the computer has free capacity. The practical fix is to finish any needed changes quickly and release the reservation before starting slow work.
Useful Questions to Ask the Interviewer
Are we using PHP's default file based session handler or a custom handler?
Do these requests only read the session, or do they also change it?
Which slow operations happen after the session is opened?
Are the tests sending the same session cookie in parallel requests?
How to Explain It in an Interview
With PHP's default file based session handler, session_start obtains an exclusive lock for the current session identifier before the session data is made available to the script. The lock prevents two requests from writing the same session file at the same time.
PHP normally writes the session and releases the lock when the active session is closed near the end of the request. session_write_close writes the current session data and releases the lock immediately. session_abort also releases the lock, but it discards changes made during the current session.
Because the lock belongs to one session identifier, two requests carrying the same session cookie can become serialized. A second request may wait inside session_start until the first request closes its session. Requests using different session identifiers do not contend for that same session lock.
The practical pattern is to call session_start, read or update the required session values, and then call session_write_close before database work, remote calls, report creation, file processing, or other slow operations. Changes made to $_SESSION after the close are only local changes in that request and are not automatically saved.
For read only access, session_start can receive the read_and_close option. PHP reads the session and closes it immediately. Later changes to $_SESSION are not saved. With the default file handler, read_and_close may also leave the session file modification time unchanged, which can matter when file cleanup depends on that time.
Closing early reduces lock waiting, but it can introduce read then write races. Two requests may read the same value, close the session, calculate separately, and later overwrite each other. Important counters, balances, inventory, or workflow state should use a database transaction or another atomic storage operation.
To measure the effect, record time immediately before and after session_start. Send parallel requests with the same session cookie, compare them with different session identifiers, and repeat the test after releasing the session early. Custom handlers can use different locking behavior, so their documentation and production measurements must be checked.
Example
The example provides three modes that use the same session identifier. The hold mode keeps the default file based session locked during a five second delay. A second request with the same session cookie waits inside session_start. The close mode updates the counter, stores the value, calls session_write_close, and then performs the delay without holding the session lock. The read mode uses read_and_close when no session change is required. The waitBeforeStart value shows how long session_start took to return.
Code
<?phpdeclare(strict_types=1);
header('Content-Type: application/json');
$mode = $_GET['mode'] ?? 'hold';
$beforeStart = microtime(true);
if ($mode === 'read') {
// Read the session and release its lock immediately.if (!session_start(['read_and_close' => true])) {
thrownewRuntimeException('Unable to start the session.');
}
$afterStart = microtime(true);
$count = $_SESSION['count'] ?? 0;
$response = [
'mode' => 'read',
'waitBeforeStart' => $afterStart - $beforeStart,
'count' => $count,
'message' => 'The session was read and closed immediately.'
];
} elseif ($mode === 'close') {
if (!session_start()) {
thrownewRuntimeException('Unable to start the session.');
}
$afterStart = microtime(true);
// Finish the required session change while the lock is held.$_SESSION['count'] = ($_SESSION['count'] ?? 0) + 1;
$count = $_SESSION['count'];
// Save the session and release the lock before slow work.if (!session_write_close()) {
thrownewRuntimeException('Unable to write and close the session.');
}
sleep(5);
$response = [
'mode' => 'close',
'waitBeforeStart' => $afterStart - $beforeStart,
'count' => $count,
'message' => 'Slow work ran after the session lock was released.'
];
} else {
if (!session_start()) {
thrownewRuntimeException('Unable to start the session.');
}
$afterStart = microtime(true);
$_SESSION['count'] = ($_SESSION['count'] ?? 0) + 1;
$count = $_SESSION['count'];
// The default file based session remains locked during this delay.sleep(5);
$response = [
'mode' => 'hold',
'waitBeforeStart' => $afterStart - $beforeStart,
'count' => $count,
'message' => 'Slow work ran while the session remained open.'
];
}
echojson_encode($response, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR);
Where it is used
Session locking matters in applications where one browser sends several requests at the same time. Common examples include dashboards with background requests, upload progress checks, report generation, long database operations, payment pages, and pages that call several endpoints. It is especially important with PHP FPM because separate worker processes can still wait for the same stored session. Releasing the session before slow work improves response concurrency, but important shared business state should not rely on an early closed session for atomic updates.
Why Interviewers Ask This
Interviewers ask this question to check whether the candidate understands how PHP session storage can coordinate requests handled by separate workers. They also want to see whether the candidate knows when a lock is obtained, when it is released, how session_write_close and read_and_close affect concurrency, and how early closing can create correctness risks.
Common interview mistakes
A common mistake is assuming that requests from the same user always run independently. Another is calling session_start at the beginning of every script and keeping the session open during slow work. Code that only reads values still holds the default file based lock when it uses a normal session_start call. Developers may call session_write_close and then change $_SESSION, incorrectly expecting those later changes to be saved. They may also use read_and_close and later attempt to persist changes. Other mistakes include ignoring session_start failures, measuring only total response time, testing with different session identifiers, and assuming every custom handler follows the default file handler's locking behavior.
Interview tip
Begin with the practical conclusion that requests sharing one session can serialize. Then state that the default file based handler locks during session_start, explain when the lock is released, show session_write_close as the normal early release method, mention read_and_close for read only access, and finish with custom handler differences and lost update risks.
Interviewer may ask next
What happens if code changes $_SESSION after calling session_write_close?
Those later changes are not automatically saved. session_write_close writes the current session data, ends the active session, and releases its lock at that point. The $_SESSION array can still contain local values in the running script, but changing the array does not reopen the session or persist the new values. This matters because the current request may display the changed value while the next request reads the older stored value. Required session changes should be completed before closing.
When should important shared state be moved out of the PHP session?
Important state should be moved when several requests must update it safely and concurrently. Examples include balances, inventory, counters, and workflow transitions. A database transaction or another atomic storage operation can enforce the required consistency without keeping the PHP session locked during unrelated slow work. The tradeoff is that session locking gives simple serialization for session data, while dedicated storage requires more design but provides clearer concurrency rules and better control over important updates.
32. How does PHP-FPM manage worker processes, and how would you choose pool settings?Language SpecificHard
i 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
How much memory is reserved for PHP FPM?
What are the normal and peak request rates?
What are the typical and high percentile request durations?
How much private memory does a busy worker use?
What queueing and response time are acceptable?
Which downstream services limit safe concurrency?
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. How does opcache change PHP execution and deployment behavior?Language SpecificHard
i 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
Which server mode runs the application, such as PHP FPM or Apache?
Is timestamp validation enabled in production?
Does deployment switch release directories or overwrite live files?
Is preloading enabled?
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))
34. How do weak references and WeakMap work in PHP?Language SpecificHard
i Question Details
Explain how they differ from strong references, garbage-collection behavior, object-keyed metadata use cases, limitations, and examples where they prevent unintended retention.
Short Interview Answer (30-60 seconds)
WeakReference and WeakMap let PHP access objects without keeping those objects alive. WeakReference observes one object, and its get method returns that object while it exists or null after it is destroyed. WeakMap stores values under object keys without increasing the key reference count, so PHP removes an entry when its key has no remaining strong reference. I use them for optional observation, derived caches, and object metadata that must not extend an object lifetime.
This question asks how PHP can watch an object or attach extra information to it without forcing that object to stay in memory. A normal variable keeps an object available while the variable still points to it. These features do not provide that ownership. The object may disappear when the rest of the program stops using it. This is useful for temporary caches and information connected to short lived objects. It can prevent a service from keeping objects only because it forgot to remove old entries.
Useful Questions to Ask the Interviewer
Do we need to observe one object or store data for many objects?
Should the stored data disappear when its object disappears?
Is this code used in a normal request or a long running process?
How to Explain It in an Interview
A normal PHP variable that contains an object creates a strong reference to that object. The object stays alive while a reachable strong reference remains.
WeakReference observes one object without increasing its reference count. Create it with WeakReference::create($object). Its get method returns the object while it is alive. It returns null after PHP destroys the object, so the caller must always handle both results. WeakReference cannot be serialized. ([php.net](https://www.php.net/manual/en/class.weakreference.php))
WeakMap is a collection whose keys must be objects. Values may be any PHP value. The map holds each value strongly, but it does not increase the reference count of the object used as its key. When no strong reference to a key remains, PHP destroys that key and automatically removes its entry from the map. ([php.net](https://www.php.net/weakmap))
PHP normally destroys an object when its reference count reaches zero. An unreachable object cycle may remain until the cycle collector processes it. Weak references do not keep an otherwise unreachable object alive.
WeakMap is useful for derived cache results, validation state, serializer state, and metadata linked to object instances. It avoids manual entry cleanup when object lifetime is the correct cleanup rule.
A major limitation is that a value can retain its own key. For example, if a WeakMap value contains a strong reference back to the key object, that reference keeps the key alive. The entry therefore remains until that strong path is removed.
Do not use weak storage when the collection must own the objects or preserve data after a key disappears. It also does not replace explicit cleanup for files, sockets, transactions, or other external resources.
Example
The example creates a Service object and a WeakReference that observes it. The first call to get returns the Service because the service variable is a strong reference. After that variable is removed, the Service is destroyed and get returns null. The example then creates a WeakMap and stores metadata under a Request object key. The map contains one entry while the request variable exists. Removing that final strong reference destroys the Request and automatically removes the related map entry.
Code
<?phpdeclare(strict_types=1);
finalclassService{
}
finalclassRequest{
}
// WeakReference observes an object without keeping it alive.$service = newService();
$serviceReference = WeakReference::create($service);
var_dump($serviceReference->get() instanceof Service);
// Remove the final strong reference to the Service object.unset($service);
var_dump($serviceReference->get());
// WeakMap stores a value under an object key without retaining the key.$metadata = newWeakMap();
$request = newRequest();
$metadata[$request] = ['validated' => true];
var_dump(count($metadata));
var_dump($metadata[$request]);
// Remove the final strong reference to the Request object.unset($request);
var_dump(count($metadata));
Where it is used
WeakReference is useful when an event system, diagnostic tool, or object coordinator needs to observe one object without owning its lifetime. WeakMap is useful for validation results, serializer state, computed values, proxy information, and temporary metadata associated with object instances. These features are especially valuable in long running workers because strong object storage can unintentionally retain every processed object. In a normal PHP request, they can still express correct ownership, although request termination already releases request memory.
Why Interviewers Ask This
Interviewers ask this question to check whether a candidate understands PHP object lifetime, reference counting, cycle collection, and unintended memory retention. It also tests whether the candidate can choose between WeakReference, WeakMap, and strong object storage in caches, metadata systems, and long running PHP processes.
Common interview mistakes
A common mistake is assuming that WeakReference::get always returns an object. It can return null as soon as the target has been destroyed. Another mistake is treating WeakMap as a normal PHP array. WeakMap accepts only object keys, and entries depend on key lifetime. Developers may also assume that its values are weak, but values are held strongly while their entries exist. A value that strongly refers back to its key can keep the key alive and prevent automatic removal. Calling gc_collect_cycles after every unset is also unnecessary for objects that are not part of unreachable cycles and can add avoidable runtime work.
Interview tip
Start with ownership. Explain that normal variables and strong collections keep objects alive, but WeakReference and WeakMap do not keep their target or key alive. Then distinguish one observed object from object keyed metadata. Finish with the null check, automatic entry removal, and the strong value back reference limitation.
Interviewer may ask next
What happens if a WeakMap value contains a strong reference to its own key object?
The key remains alive because the value provides a strong path back to it. WeakMap keeps the value strongly while the entry exists, so the key reference count does not reach zero. PHP therefore cannot destroy the key or automatically remove the entry. This matters because the back reference defeats the intended retention benefit and may cause memory growth in a long running process.
When should SplObjectStorage be used instead of WeakMap?
Use SplObjectStorage when the collection should strongly retain its object keys until entries are explicitly removed or the storage itself is destroyed. Use WeakMap when metadata should disappear automatically with each key object. SplObjectStorage gives deliberate ownership and predictable retention, while WeakMap reduces cleanup work and unintended retention. The tradeoff is that WeakMap data can disappear whenever no other strong reference to a key remains.
35. How would you design a reusable PHP API with correct covariance and contravariance?Language SpecificHard
i 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.
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
Which input capabilities does every implementation actually need?
Which result type can every implementation always guarantee?
Will external packages implement or extend this contract?
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
<?phpdeclare(strict_types=1);
interfaceRequest{
publicfunctionpayload(): array;
}
finalclassJsonRequestimplementsRequest{
publicfunction__construct(privatearray$data)
{
}
publicfunctionpayload(): array{
return$this->data;
}
}
finalclassFormRequestimplementsRequest{
publicfunction__construct(privatearray$data)
{
}
publicfunctionpayload(): array{
return$this->data;
}
}
interfaceResponse{
publicfunctionbody(): string;
}
finalclassJsonResponseimplementsResponse{
publicfunction__construct(privatearray$data)
{
}
publicfunctionbody(): string{
returnjson_encode($this->data, JSON_THROW_ON_ERROR);
}
}
classHandler{
publicfunctionhandle(JsonRequest $request): Response{
returnnewJsonResponse($request->payload());
}
}
finalclassApiHandlerextendsHandler{
// Request is less specific than JsonRequest.// JsonResponse is more specific than Response.publicfunctionhandle(Request $request): JsonResponse{
returnnewJsonResponse([
'success' => true,
'data' => $request->payload(),
]);
}
}
functionrunHandler(Handler $handler, JsonRequest $request): void{
echo$handler->handle($request)->body(), PHP_EOL;
}
runHandler(newApiHandler(), newJsonRequest(['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.
36. How do fibers work in PHP, and what problems do they solve?Language SpecificHard
i 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.
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
Should the example show direct Fiber control or a library abstraction?
Should cancellation and timeout handling be included?
Will this run in a command line worker or a web request?
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
<?phpdeclare(strict_types=1);
$fiber = newFiber(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(newRuntimeException('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.
37. How would you implement and validate a custom PHP stream wrapper?Language SpecificHard
i 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.
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
Which file operations must be supported?
Is the content read only or writable?
Must seeking work in both directions?
Which paths and context options are allowed?
Must data survive after the PHP process ends?
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.
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
i 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
Should outside code be able to change the property?
Is the value stored or calculated from other state?
May child classes replace the access behavior?
Will reflection or serialization tools handle the object?
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?NEWCodingEasy
i 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.
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
What is the largest expected input size n?
Should I compare running time, extra memory, or both?
Can I use the usual average-case cost for PHP associative-array key access?
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.// ------------------------------------------------------------functionbinarySearch(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 ($arras$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
Treating Big O as an exact number of milliseconds instead of a growth rate.
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.
Assuming every PHP array operation is O(1). in_array(), array_search(), and array_shift() are not constant-time operations.
Calling PHP associative-array access guaranteed O(1) in every case instead of saying O(1) on average.
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
i 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.
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
Should the function support valid UTF-8 text, or only ASCII text?
Should an empty string return an empty string?
Is reversing Unicode code points enough, or must combined grapheme clusters remain together?
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
<?phpdeclare(strict_types=1);
/**
* Reverse a valid UTF-8 string without using strrev().
*/functionreverseString(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.returnimplode('', $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.