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: August 28, 2026)

51. What is a microservice?NEWSystem DesignEasy

Question Details

Define a microservice as a small independently deployable service centered on a focused business capability. Compare microservices with a modular PHP monolith, and explain service boundaries, APIs or events, data ownership, deployment, scaling, observability, network failures, consistency, and operational cost. State clearly that microservices are a tradeoff rather than a default.

Short Interview Answer (30-60 seconds)

At a high level, a microservice is a small service focused on one business capability. The main challenge is letting independent services work together without creating too much operational complexity. I would explain it in three parts: service boundaries and communication, independent data and deployment, then scaling and failures. In this design, services use APIs or events and own their databases. The benefit is flexibility and independent scaling. The trade-off is more network failures, monitoring work, and operational cost.

Detailed Explanation

A microservice is a small part of a larger system that handles one clear business job. The goal is to let different parts change, deploy, and grow independently. The difficult part is that these parts still need to work together correctly. Communication can now cross network boundaries, so delays and failures matter more. The diagram explains this with independent services, separate data ownership, shared operational tools, and a simple order example. It also compares this approach with keeping modules inside one PHP application.

Useful Questions to Ask the Interviewer
  1. Do different teams need to deploy their parts independently?
  2. Do some business areas need much more scaling than others?
  3. Can some updates arrive a little later when events are used?
  4. Does the organization have enough operations experience to run many services?
What is a microservice? diagram
How to Explain It in an Interview
1. Start with service boundaries

I would start by saying that each microservice owns one focused business capability. In the diagram, User Service manages users and profiles. Order Service manages orders and payments. Product Service manages products and inventory. Notification Service sends email and SMS.

This separation is called a service boundary. It means each service has a clear job. A Modular PHP Monolith can also have clear modules, but those modules remain inside one application and codebase. This gives tighter coupling than separate services.

2. Explain communication and the request path

The Client first sends requests through the API Gateway. The gateway handles routing, authentication, and rate limiting. It sends each request to the appropriate service.

Services can communicate through an API or an event. An API is a direct request between services. An event is a message saying that something happened. Events can make services more loosely coupled because another service can react separately.

3. Explain data ownership, deployment, and scaling

Each service owns its own database in this design. User Service has User DB. Order Service has Order DB. Product Service has Product DB. Notification Service has Notification DB.

This keeps data ownership clear and allows each service to deploy independently. It also lets us scale only the service that needs more capacity. A Modular PHP Monolith usually uses one shared database and deploys the whole application together.

4. Use the order example

For a simple example, the user places an order. Order Service creates the order. It then publishes an OrderCreated event. Notification Service reacts to that event and sends an email. The user then gets the confirmation.

The notification path is asynchronous, which means it does not have to happen in the same direct call as creating the order. The diagram does not show a specific queue or delivery guarantee, so I would not assume one.

5. Explain operations, failures, and the trade-off

Shared Services support the system with Service Discovery, Central Logging, Monitoring & Alerts, and Configuration. These tools help operators understand and manage many separate services.

Network calls can fail or time out. The diagram shows using timeouts, retries, and a circuit breaker for these failures. Observability means using logs, metrics, and traces to understand what happens across services.

Event-based workflows may use eventual consistency. This means different services can see the same business change at slightly different times. The main trade-off is simple. Microservices give independent deployment, scaling, and team flexibility. They also add more moving parts, network failures, monitoring work, DevOps needs, and operational cost. Microservices are a trade-off, not a default.

Engineering Considerations / Design Trade-offs

The benefit is that each service can change, deploy, and scale on its own. Teams can work more independently, and a busy service can get more capacity without scaling the whole application. The downside is that the system has more moving parts. Network calls can time out or fail. Logs, metrics, and traces must work across several services. Events may also mean one service sees a change a little later than another. Running many services needs more tools and more DevOps skill. Microservices are useful when these benefits are worth the extra cost. They should not be the default for every PHP application.

Why Interviewers Ask This

Interviewers want to see whether you understand why microservices exist, not only their definition. They want to know if you can choose sensible service boundaries, explain APIs and events, keep data ownership clear, and reason about deployment and scaling. They also want to hear the downsides. A strong answer shows judgment about network failures, observability, consistency, and operational cost.

Interviewer may ask next
What would change if the Order Service became much busier than the other services?

I would keep the same service boundaries and scale the Order Service independently. That is one of the main benefits shown in the diagram. We can give Order Service more capacity without scaling User Service, Product Service, or Notification Service just because order traffic increased.

The API Gateway would still route requests to the correct service. Order Service would still own Order DB and would still publish events such as OrderCreated. The other services would keep their existing responsibilities.

I would also watch Central Logging and Monitoring & Alerts more closely. Higher traffic can expose timeouts, failed network calls, or other bottlenecks. The diagram shows using timeouts, retries, and a circuit breaker for network failures.

The downside is that scaling Order Service may move the bottleneck somewhere else. Order DB or another service it calls may become the next limit. Independent scaling gives flexibility, but we still need to watch the whole system.

What happens if Notification Service is temporarily unavailable after an order is created?

I would keep the order responsibility separate from the notification responsibility. In the diagram, Order Service creates the order and publishes an OrderCreated event. Notification Service reacts to that event and sends the email.

If Notification Service is unavailable, the order still belongs to Order Service. The notification problem should not change that service boundary. Monitoring & Alerts should show the Notification Service problem, while Central Logging helps operators investigate it.

The event path is asynchronous, so the notification does not need to be part of the same direct service call that creates the order. However, the diagram does not show a durable queue, retry storage, or a delivery guarantee. I would not claim that the email is guaranteed to be delivered later without adding more design details.

The main downside is that the confirmation can be delayed or missed during a failure. This is part of the extra operational complexity that comes with microservices.

52. What is a REST API?NEWAPI DesignEasy

Question Details

Define a REST API in practical HTTP terms. Explain resources and URLs, HTTP methods, request and response representations such as JSON, status codes, stateless requests, validation, consistent errors, authentication, pagination, idempotency, and caching. Use one small PHP service example and distinguish REST from a framework or transport protocol.

Short Interview Answer (30-60 seconds)

At a high level, I see a REST API as a clear way for a client and server to work with resources over HTTP. Resources use URLs such as /api/users and /api/users/42. The client chooses methods like GET, POST, PUT, PATCH, or DELETE. The PHP service validates and processes the request, then returns JSON and an HTTP status code to the client. Each request is stateless and carries the information it needs. Authentication, pagination, caching, idempotency, and consistent errors improve the design. The trade-off is extra implementation work for a more predictable API.

Detailed Explanation

This question asks how a client and server can exchange information in a clear and predictable way. The example is a small service for users. A client can ask for users, create a user, change one, or remove one. Each operation has a clear address and action. The server checks the request, performs the work, and sends a result back to the client. We also need clear handling for bad input, access checks, large lists, repeated requests, and repeated reads. The diagram explains these ideas with one small PHP service.

Useful Questions to Ask the Interviewer
  • Should I focus mainly on REST concepts or also explain the PHP example?
  • Should I explain both collection URLs and single-resource URLs?
  • Do you want authentication, pagination, caching, and error handling included?
What is a REST API? diagram
How to Explain It in an Interview
1. Start with resources and URLs

I would start by saying that REST organizes an API around resources. A resource is something the client wants to work with. In this example, the main resource is a user. The collection URL is /api/users. A single user can use /api/users/42, where 42 identifies that user. The diagram also shows /api/orders/123 as another resource-style URL example. Using nouns in URLs keeps the API clear and predictable.

2. Use HTTP methods for actions

Next, I would explain that the HTTP method tells the server what action the client wants. GET /api/users lists users. GET /api/users/42 gets user 42. POST /api/users creates a user. PUT /api/users/42 replaces user 42. PATCH /api/users/42 updates only some fields. DELETE /api/users/42 removes user 42. This keeps the resource in the URL and the action in the HTTP method.

3. Explain the request and PHP service

The client sends an HTTP request to the PHP REST API. A request can contain a method, URL, headers, and an optional body. The concrete example uses GET /api/users?page=2&limit=5. It also sends Accept: application/json and Authorization: Bearer <token>. The PHP service reads the query values, validates and processes the request, fetches user data, and prepares the response. Each request contains the information needed to handle it. This is statelessness: the server does not depend on stored client session state between API requests.

4. Return JSON and meaningful status codes

The PHP service sends the HTTP response back to the client. The response includes a status code, headers, and usually a JSON body. The example returns 200 OK with data, page, limit, and total. The diagram also shows 201 Created when a resource is created, 400 Bad Request for invalid input, 401 Unauthorized when the caller is not authenticated, 403 Forbidden when the caller has no permission, 404 Not Found when the resource is missing, 409 Conflict for a conflict with current state, and 500 Internal Server Error for an unexpected server problem. Consistent JSON errors make failures easier for clients to handle.

5. Add validation, authentication, and pagination

The service should validate input before using it. Invalid input should return 400 with a clear message. Authentication uses a token in the Authorization header. Authentication checks who the caller is. Permission checks decide what that caller may do. For large collections, pagination avoids returning every item at once. The example uses page and limit query parameters, such as ?page=2&limit=5.

6. Explain caching and idempotency

Caching can make repeated reads faster. The diagram shows Cache-Control and ETag headers for GET responses. Idempotency means repeating an operation has the same intended effect after the first successful operation. GET, PUT, and DELETE are idempotent methods. POST is not generally idempotent because repeating a create request may create another resource.

7. Finish by defining what REST is not

I would finish by saying that REST is an architectural style for designing APIs. It is not a PHP framework such as Laravel or Symfony. It is also not a transport protocol. REST can work over HTTP or HTTPS and can return JSON. JSON is only a data format. The benefit of these conventions is a predictable API. The trade-off is that the team must consistently design URLs, methods, validation, errors, authentication, pagination, caching, and repeated-request behavior.

Practical Complexity & Trade-offs

The benefit of this design is predictability. Resource URLs and standard HTTP methods make the API easier for clients to understand. Clear status codes and consistent JSON errors make failures easier to handle. Validation protects the service from bad input. Authentication checks the caller before protected work is allowed. Pagination keeps large user lists manageable. Caching with Cache-Control or ETag can reduce repeated work for GET responses. Idempotent methods such as GET, PUT, and DELETE are safer when requests are repeated. The downside is extra design and testing work. The team must apply these rules consistently. Poor URLs, incorrect status codes, weak validation, or inconsistent errors can make an API difficult to use even when the HTTP communication itself works.

Why Interviewers Ask This

Interviewers ask this question to check whether you understand REST as practical API design, not just as a definition. They want to see whether you can model resources with clear URLs, choose correct HTTP methods, return useful status codes, and explain request and response data. They also look for judgment around statelessness, validation, authentication, pagination, caching, idempotency, and consistent errors. A strong answer shows that you can design an API that clients can understand and use correctly.

Interviewer may ask next
What would you change if the users collection became very large?

I would keep the same /api/users resource and use the pagination already shown in the design. The request would continue to use query parameters such as GET /api/users?page=2&limit=5. The PHP service would validate page and limit, fetch only the requested part of the collection, and return JSON containing data, page, limit, and total. This keeps the response smaller and avoids returning the complete user collection every time. Authentication would still use the Authorization: Bearer <token> header. Invalid input would still return 400 Bad Request, while a successful read would return 200 OK. Caching rules for GET responses can also remain in place when appropriate. The main downside is that the client must make several requests to read a large collection. The client must also handle page numbers, limits, and totals correctly. The resource URLs, HTTP methods, validation, status codes, and JSON response style remain unchanged.

How would you handle a client repeating the same request?

I would first look at the HTTP method because the design already explains idempotency. Repeating GET /api/users/42 only reads the resource again. Repeating PUT /api/users/42 should leave the resource in the same intended replacement state. Repeating DELETE /api/users/42 should not create another business side effect after the resource has already been removed. These methods are designed to be idempotent. POST /api/users is different because repeating it may create another resource, so POST is not generally idempotent. The PHP service should still validate each request and return an appropriate status code and JSON response. Authentication also remains in place where the bearer token is required. The benefit of idempotent operations is safer behavior when a request is repeated. The downside is that POST creation needs more care because this diagram does not show an idempotency key or another duplicate-prevention mechanism.

53. What is middleware in a PHP web application?NEWAPI DesignEasy

Question Details

Define HTTP middleware as a component that participates in processing an incoming request and producing the response, often before and after the main handler. Explain a middleware pipeline, delegation, ordering, short-circuit responses, and common uses such as error handling, authentication, authorization, CORS, rate limiting, logging, and request IDs. Relate the explanation to PSR-15 without requiring one framework.

Short Interview Answer (30-60 seconds)

At a high level, middleware is code that sits around the main PHP request handler. The HTTP request moves through an ordered pipeline, such as error handling, authentication, authorization, and other middleware. Each middleware can inspect or change the request, then pass control to the next handler. The response comes back through the pipeline in reverse order. Middleware can also stop early, such as returning 401 when authentication fails. The benefit is cleaner separation of common work. The trade-off is that middleware order matters and long pipelines can become harder to trace.

Detailed Explanation

Middleware helps a PHP web application handle common work around each request. A request enters the application and passes through several steps before reaching the main application code. These steps can handle errors, check identity and permissions, control cross-origin access, limit traffic, or record useful request details. Each step can pass the request forward or return a response early. When the main handler finishes, the response travels back through the earlier steps. The main challenge is keeping this ordered flow clear and predictable. The diagram shows that complete request and response path.

Useful Questions to Ask the Interviewer
  • Should I explain middleware in a framework-neutral way?
  • Should I relate the answer to PSR-15 interfaces?
  • Do you want examples of ordering and short-circuit responses?
What is middleware in a PHP web application? diagram
How to Explain It in an Interview
1. Start with the ordered middleware pipeline

I would first explain that middleware forms an ordered pipeline around the application. The client sends an HTTP request into Error Handling Middleware. The request then moves through Authentication Middleware and Authorization Middleware. After that, it reaches Other Middleware, which can include CORS, rate limiting, logging, and request ID handling. Finally, the request reaches the application handler. Each middleware gets a chance to do work before control moves forward.

2. Explain delegation to the next handler

Each middleware receives the request and a next handler. It can inspect or modify the request before delegating. In PSR-15 style, it calls the next handler with the request and receives a response back. This lets each middleware focus on one shared concern instead of putting every concern inside the application handler.

3. Explain why middleware order matters

The order changes application behavior. Error handling appears first, so it can surround later processing. Authentication comes before authorization because the application normally needs to know who the caller is before checking permissions. The remaining middleware runs after those checks in this diagram. The request continues until it reaches the final handler. Because changing the order can change results, middleware order should be deliberate.

4. Explain how the response returns

The request moves forward through the pipeline, but the response returns in reverse order. The final handler produces the application response. That response then travels back through middleware that delegated earlier. This allows middleware to do work after the next handler returns. For example, logging middleware can record response information. This before-and-after behavior is a key middleware idea.

5. Explain short-circuit responses

Middleware does not always need to call the next handler. It can stop the pipeline and return a response immediately. The diagram shows Authentication Middleware checking a token. If the token is missing or invalid, it returns 401 Unauthorized. The pipeline stops, so the application handler is not called. This behavior is called short-circuiting. It avoids unnecessary work when a request should not continue.

6. Relate the design to PSR-15

PSR-15 gives PHP applications a common middleware contract without requiring one framework. The diagram shows middleware implementing process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface. In simple terms, middleware receives the current request and the next handler, then returns an HTTP response. The final application handler is shown as a PSR-15 handler. This common shape makes compatible middleware easier to reuse.

7. Finish with common uses and the trade-off

Common uses in the diagram include error handling, authentication, authorization, CORS, rate limiting, request logging, request IDs or correlation IDs, and input validation. These concerns apply across many requests, so middleware keeps them separate from business logic. The benefit is cleaner and more reusable application code. The downside is that behavior depends on pipeline order. Too many middleware layers can also make request processing harder to trace.

Practical Complexity & Trade-offs

The main design choice is to put shared request work into an ordered middleware pipeline instead of repeating it inside every application handler. The benefit is cleaner code and reusable handling for errors, authentication, authorization, CORS, rate limiting, logging, request IDs, and validation. Middleware can also stop an invalid request early, which avoids unnecessary application work. The downside is that order matters. In this diagram, authentication comes before authorization because permissions are checked after identity is known. Another trade-off is visibility. A short pipeline is easy to follow, while many middleware layers can make debugging harder. PSR-15 helps by giving middleware a common interface, but developers still need a clear and well-documented order.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands the HTTP request lifecycle instead of only knowing a framework feature. They want correct request and response direction, delegation, ordering, and short-circuit behavior. They also look for the difference between authentication and authorization and knowledge of common cross-cutting concerns. Mentioning PSR-15 shows framework-neutral PHP knowledge. A strong answer also explains that middleware improves separation of concerns while making pipeline order important.

Interviewer may ask next
What happens if Authentication Middleware finds a missing or invalid token?

It should return a response immediately instead of calling the next handler. In this design, Authentication Middleware receives the request after Error Handling Middleware and before Authorization Middleware. If the token is missing or invalid, Authentication Middleware returns 401 Unauthorized. The request does not continue to Authorization Middleware, Other Middleware, or the final application handler. This is the short-circuit path shown in the diagram. It prevents protected application work from running for an unauthenticated request. Valid requests keep the original flow. They continue through authorization and the remaining middleware before reaching the final handler. The response then comes back through the middleware chain in reverse order. The main downside is that ordering becomes important. If authentication were placed too late, other components might do unnecessary work before the request is rejected. Keeping the pipeline order clear makes this behavior predictable.

Why does Authentication Middleware come before Authorization Middleware?

Authentication comes first because authorization needs a known identity before it can check permissions. In this diagram, Authentication Middleware receives the request before Authorization Middleware. Authentication checks whether the caller has valid identity information. If that check fails, it can short-circuit the pipeline with 401 Unauthorized. If authentication succeeds, the request continues to Authorization Middleware. Authorization then decides whether that authenticated caller is allowed to continue. The rest of the pipeline remains unchanged. Other Middleware still runs afterward, and the final handler receives only requests that earlier middleware allowed to continue. This keeps the two responsibilities separate. Authentication answers who the caller is. Authorization answers what that caller may do. The downside is that the components now have an important ordering dependency. If they are arranged incorrectly, authorization may not have the identity information it expects, so the middleware order should be controlled and tested.

54. What is dependency injection in a PHP application?NEWAPI DesignEasy

Question Details

Define dependency injection as giving an object the collaborators it needs instead of making it construct or locate them itself. Explain constructor injection for required dependencies, interfaces, factories, service containers, autowiring, configuration, object lifetime, and testability. Distinguish dependency injection from the container that may automate it and warn against using the container as a global service locator.

Short Interview Answer (30-60 seconds)

At a high level, dependency injection means giving a PHP object the dependencies it needs from outside. I would normally use constructor injection for required dependencies, such as UserRepository and Mailer. The application creates the implementations and passes them into UserService, either manually or with help from a service container. Interfaces keep UserService loosely coupled to concrete classes. A container can automate factories, configuration, lifetimes, and autowiring. The benefit is easier testing and replacement. The trade-off is extra wiring and container configuration.

Detailed Explanation

Dependency injection solves a simple problem. A class often needs other objects to do its work. Instead of letting that class create or search for those objects, we give them to the class from outside. This makes the class easier to understand, change, and test. In this design, UserService receives a UserRepository and a Mailer. The application can create them directly, or a service container can help build and connect them. The goal is to keep UserService focused on its own job.

Useful Questions to Ask the Interviewer
  • Should I explain manual dependency injection, a service container, or both?
  • Should I focus mainly on constructor injection for required dependencies?
  • Do you want me to discuss object lifetime and testing as well?
What is dependency injection in a PHP application? diagram
How to Explain It in an Interview
1. Start with the main idea

I would say dependency injection means receiving dependencies from outside the class. A dependency is another object that a class needs. In the diagram, UserService needs UserRepository and Mailer. UserService should not create or search for those objects itself. The application provides them. This reduces tight coupling because UserService does not control how its dependencies are created.

2. Use constructor injection for required dependencies

For required dependencies, I would use constructor injection. The UserService constructor accepts UserRepository and Mailer. This makes the required dependencies clear when the object is created. The service stores both objects and uses them later. In the example, UserService uses UserRepository to find a user. It then uses Mailer to send a message. The class can focus on this business work instead of creating those supporting objects.

3. Depend on interfaces

The diagram shows UserRepository and Mailer as interfaces. An interface describes what an object can do without choosing one concrete implementation. UserService can therefore work with DatabaseUserRepository or another repository implementation. The same idea applies to Mailer. The application chooses the concrete implementations outside UserService. This makes implementations easier to replace and keeps the service loosely coupled.

4. Create and connect objects outside UserService

The diagram shows object wiring happening outside the business class. The application can create DatabaseUserRepository and SmtpMailer, then pass both objects into UserService. A factory can create an object when construction needs configuration or several setup steps. This keeps creation logic separate from business logic. It also gives one clear place to decide which implementations the application should use.

5. Use a service container when useful

A service container is a tool that can build and connect objects. It can manage bindings between interfaces and implementations. It can also use factories and configuration. Autowiring means the container examines constructor types and automatically supplies matching dependencies. Dependency injection is the design principle. The container is only a tool that may automate the wiring. A small application can use dependency injection without using a container.

6. Manage configuration and object lifetime

Configuration can choose implementations for things such as a database, cache, mailer, or API client. The diagram also shows object lifetime. A transient object is created each time it is needed. A shared or singleton-style object may be reused when appropriate. A container can manage these choices. The lifetime must fit the dependency because shared objects can keep state longer than expected.

7. Explain testing and avoid the service locator pattern

Dependency injection improves testing because tests can provide simple replacement objects. A test can pass an in-memory repository and a fake or null mailer into UserService. It does not need a real database or real email system. I would also avoid passing the service container into UserService and asking it for dependencies. That turns the container into a global service locator. Dependencies become hidden and tests become harder to control. I would keep the container in the application wiring layer and pass required dependencies explicitly.

Practical Complexity & Trade-offs

The benefit of this design is clear separation. UserService uses its dependencies but does not decide how to create them. Interfaces make implementations easier to replace. Constructor injection also makes required dependencies visible. A service container can reduce manual wiring when the application grows. The downside is extra configuration and more concepts to understand. Autowiring is convenient, but complicated automatic rules can make debugging harder. Object lifetime also needs care because shared objects may keep state longer than expected. Manual wiring is often simpler for a small application. A container becomes useful when many objects must be created and connected. We accept some setup complexity because testability, maintainability, and replaceability improve.

Why Interviewers Ask This

Interviewers ask this question to see whether you understand loose coupling, clear class responsibilities, and testable PHP design. They want to know whether you can separate object creation from business logic. They also check your understanding of constructor injection, interfaces, factories, service containers, autowiring, configuration, and object lifetime. A strong answer should distinguish dependency injection from the container and explain why using the container as a global service locator creates hidden dependencies.

Interviewer may ask next
What would you change if the application became large and manually creating every dependency became difficult?

I would keep the same dependency injection design, but use a service container to automate more wiring. UserService would still receive UserRepository and Mailer through its constructor. I would not make UserService depend on the container. Instead, the application wiring layer would configure which implementations to use, such as DatabaseUserRepository and SmtpMailer. The container could use autowiring to inspect constructor types and supply matching dependencies. Factories would still help when an object needs configuration or several creation steps. I would also choose suitable object lifetimes, such as transient or shared, for each dependency. Correctness stays clear because required dependencies remain visible in the constructor. The main downside is extra container configuration. Automatic wiring can also become difficult to debug when too many hidden rules are added. I would therefore keep bindings simple and keep the container outside business classes.

How does dependency injection make UserService easier to test?

It makes testing easier because the test controls exactly which objects UserService receives. In production, the application may provide DatabaseUserRepository and SmtpMailer. In a test, I can instead provide an in-memory repository and a fake or null mailer. UserService does not need to change because it depends on the same repository and mailer contracts. The test can prepare known data, call UserService, and check the result without connecting to a real database or sending a real email. This makes tests faster and more predictable. It also keeps the test focused on UserService instead of external systems. The main downside is that interfaces and test replacements add some extra code. I would still keep dependencies explicit through the constructor because both production wiring and test setup remain easy to understand.

55. What database indexes would you add for a slow PHP query, and how would you verify the improvement?Sql / DatabaseMedium

Question Details

Given a query with filters, joins, and ordering, explain how to inspect its execution plan, choose single or composite indexes, verify selectivity, and measure before and after performance.

Short Interview Answer (30-60 seconds)

I would inspect the actual execution plan first, then index selective filter and join columns, using a composite index when its leading-column order matches the query. I would verify the result with repeated before-and-after tests of rows processed, sort work, execution time, and write cost.

Detailed Explanation

This question asks how I would make a slow request for stored information faster without guessing. I must first see the exact request, the values supplied with it, how much information is stored, and how the information is arranged. Then I choose a smaller, faster lookup path that matches the way the request searches, combines, and orders records. Finally, I repeat the same tests before and after the change to prove that it reduces work consistently without causing unacceptable extra work when records are added, changed, or removed.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Which database engine and version are being used?
  • What is the exact SQL statement and its representative bound values?
  • Which filters are mandatory, and which are optional?
  • What are the table sizes and relevant value distributions?
  • Which indexes and constraints already exist?
  • How many rows does the query normally return?
  • Is the workload read-heavy, write-heavy, or mixed?
  • Is the main target lower latency, lower database load, or both?
What database indexes would you add for a slow PHP query, and how would you verify the improvement? diagram
How to Explain It in an Interview

I would begin with the exact SQL statement generated or executed by the PHP application and representative parameter values. PHP should bind untrusted values through PDO or another safe database API, but parameter binding does not make the query fast and does not decide which index the database uses. Index selection and execution planning are database responsibilities.

First, I would establish a baseline in a safe environment using production-like data. I would record the database execution time over multiple runs, rows returned, rows read or examined, join operations, temporary structures, sorting work, and relevant input/output activity when the engine exposes it. I would separate database execution time from total PHP request time because network delay, connection acquisition, PHP processing, rendering, and other application work can hide or exaggerate the database cost.

Next, I would inspect the execution plan with the database engine's supported planning command, such as EXPLAIN, EXPLAIN ANALYZE, or an equivalent runtime-plan facility. A query plan shows how the engine scans tables, uses indexes, joins rows, and performs ordering. Runtime-plan commands may execute the query, so I would use them carefully for statements that modify data or are expensive in production.

I would look for large full-table or full-index scans, unexpectedly high row counts, repeated nested lookups, inefficient join order, temporary tables, explicit sorting, and large differences between estimated and actual row counts. A large estimate error can indicate stale statistics, skewed data, correlated columns, or parameter-sensitive behavior. Updating statistics may improve the plan without adding an index, although I would test that rather than assume it.

I would then review the predicates, meaning the filter and join conditions. Columns used in selective equality filters and joins are common index candidates. Selectivity describes how much a condition narrows the result. A condition matching a small fraction of a table is usually more selective than one matching most rows. I would inspect actual data distribution and engine statistics because a column with many distinct values can still contain individual values that occur very frequently.

A single-column index is appropriate when one column is commonly searched independently and existing indexes do not already support that access pattern. A composite index is appropriate when the same query pattern repeatedly filters or joins on multiple columns together. I would not create one index for every column in the WHERE, JOIN, or ORDER BY clauses because separate indexes may not combine efficiently, may duplicate existing indexes, and always add maintenance cost.

For a composite index, column order is critical. As a practical starting point, I would place columns used by stable equality conditions before a range condition, then consider columns needed for ordering or covering the query. However, this is not a universal formula. The best order depends on the database engine, optional predicates, value distribution, join strategy, sort direction, and the full workload.

Many B-tree composite indexes follow a leftmost-prefix principle. For an index on columns A, B, and C, the engine can commonly use the ordered prefix beginning with A, such as A alone or A with B. It normally cannot use the same index as efficiently for a search beginning only with B or C. Some engines can use skip-scan or index-combination strategies in limited cases, so I would verify behavior in the actual plan rather than state that non-leading columns are never usable.

For joins, I would verify that the lookup-side join column is indexed where appropriate and that joined columns have compatible data types, lengths, collations, and semantics. Primary-key or unique indexes may already cover the join. I would also confirm that expressions, implicit conversions, or functions applied to indexed columns are not preventing an efficient lookup. When an expression is required, an engine-supported functional or generated-column index may be an option.

For ORDER BY, I would check whether an index can provide rows in the required order after applying the leading filter conditions. This can avoid a separate sort, but only when the index column order, sort directions, query predicates, and engine rules align. If the query returns a large portion of the table, the optimizer may correctly prefer a scan and sort over many random index lookups.

I would also consider whether a covering index is justified. A covering index contains all columns needed for a particular query, allowing some engines to answer it with fewer table lookups. This can improve read performance, but including extra columns increases index size, memory pressure, storage use, cache churn, and write maintenance. I would use it only when measurements show a worthwhile benefit for an important query.

After selecting a candidate index, I would create it in a safe environment using the database engine's appropriate online or low-lock method when available. Large index builds can consume CPU, input/output capacity, temporary storage, replication bandwidth, and locks, so the deployment method matters. I would verify that statistics are current and then rerun the same plan analysis with the same representative parameters.

I would confirm that the intended index is selected or that the new plan is otherwise better. The important evidence is reduced work, such as fewer rows processed, fewer table lookups, less temporary activity, or removal of an expensive sort. Merely seeing the new index name in a plan is not enough; an index scan that reads most of the index may offer little improvement.

I would then repeat the timing tests under comparable conditions. I would include cold and warm cache scenarios when relevant, use multiple parameter values, and report a stable measure such as median and high-percentile latency rather than one fastest run. Parameter values matter because a plan that is efficient for a rare value may be inefficient for a common value. I would also test concurrency when the production problem appears only under load.

Finally, I would evaluate the tradeoffs. Every additional index consumes disk space and database cache memory. INSERT operations must add index entries, DELETE operations must remove them, and UPDATE operations must maintain each index whose indexed values change. Extra indexes can lengthen backups, restores, replication, schema changes, and maintenance operations. I would keep the smallest non-duplicated index set that produces a meaningful, repeatable improvement across the important workload, deploy it with monitoring and a rollback plan, and verify production results after release.

Technical Approach
  1. Capture the exact SQL statement and representative bound values from the PHP application.
  2. Confirm the database engine, version, schema, existing indexes, constraints, table sizes, and workload pattern.
  3. Establish a baseline using production-like data and multiple comparable executions.
  4. Measure database time separately from total PHP request time.
  5. Inspect the estimated plan and, when safe, the actual runtime plan.
  6. Identify expensive scans, joins, sorts, temporary work, lookup repetition, and estimate errors.
  7. Check whether stale statistics, implicit conversions, functions, or query structure are the real cause.
  8. Evaluate filter and join columns using actual selectivity and data distribution.
  9. Choose the smallest suitable single-column or composite index while avoiding redundant indexes.
  10. Order composite-index columns according to equality, range, ordering, coverage, optional filters, and engine-specific rules.
  11. Build the candidate index safely and refresh or verify statistics when required.
  12. Rerun the same plans and confirm that total database work decreases.
  13. Repeat timings with multiple representative values, cache states, and concurrency levels when relevant.
  14. Measure the effect on INSERT, UPDATE, DELETE, storage, memory use, maintenance, and deployment operations.
  15. Deploy with monitoring and rollback capability, then verify production performance.
Practical Insights

Without a useful index, the database may need to inspect a large part of a table, so the work can grow roughly with the number of stored rows. With a suitable B-tree index, locating the start of a matching range is commonly logarithmic, followed by work proportional to the matching index entries and any required table lookups. These are simplified expectations, not guaranteed timings. Actual cost depends on the engine, data distribution, cache state, storage, joins, sorting, and concurrency. Each index also consumes disk and cache memory and adds maintenance work to inserts, deletes, and updates that affect indexed columns. Wider composite or covering indexes increase those costs further.

Why Interviewers Ask This

This question tests whether the candidate can diagnose database performance systematically instead of adding indexes by guesswork. The interviewer is evaluating execution-plan analysis, index selectivity, composite-index ordering, join and sort behavior, measurement discipline, and awareness of storage and write costs. It also checks whether the candidate correctly separates PHP application behavior from database optimization: PHP submits the parameterized statement and measures the request, while the database optimizer chooses the access path and performs the index lookup.

Common interview mistakes

Common mistakes include recommending exact index columns without seeing the SQL or schema, adding indexes before reading the execution plan, and indexing every referenced column. Other mistakes are ignoring existing or overlapping indexes, choosing composite columns from the textual order of the WHERE clause, treating high distinct-value counts as proof that every value is selective, and overlooking optional filters or parameter-sensitive plans. Candidates may also ignore implicit type conversions, functions on indexed columns, stale statistics, large result sets, or sorting requirements. Measurement errors include timing only one run, comparing different parameter values, reporting total PHP latency as database latency, testing only a warm cache, or assuming that use of an index proves improvement. Production mistakes include building a large index without considering locks and resource use, forcing a plan without strong evidence, and ignoring write, storage, replication, backup, and maintenance costs.

Interview tip

Present the answer as a measured sequence: capture the exact query, establish a baseline, inspect the actual plan, choose the smallest matching index, compare plans and repeated timings, and evaluate write and operational costs. Do not guess specific columns when the interviewer has not supplied the query or schema.

Interviewer may ask next
How would you decide the column order in a composite index?

I would use the real query patterns and the database engine's rules. Stable equality conditions are often useful as leading columns, followed by a range column and then columns that may support ordering or coverage. I would also account for optional filters, join direction, value distribution, sort direction, and other queries that need the index. The written order of predicates in the SQL does not determine index order, so I would verify the choice with actual execution plans and repeated measurements.

What would you do if the database does not use the new index?

I would first check whether avoiding the index is actually the cheaper plan. The query may return too many rows, the index may have the wrong leading columns, statistics may be stale, values may be highly skewed, or functions and implicit conversions may block an efficient lookup. I would also check covering needs, join order, sorting, parameter-sensitive behavior, and overlapping indexes. I would test representative values and compare actual work before considering an engine-specific hint, because forcing an index can make other parameter values or future data distributions slower.

56. How do you perform a database transaction with PDO?Sql / DatabaseEasy

Question Details

Explain beginTransaction, commit, rollBack, exception handling, atomicity, and what should happen when one operation in a multi-step write fails.

Short Interview Answer (30-60 seconds)

Call beginTransaction(), run all related statements through the same PDO connection, and call commit() only when every step succeeds. On any failure, catch the error, call rollBack() if the transaction is active, and rethrow or handle the error so partial changes are not committed.

Detailed Explanation

See the Code while reading this explanation.

This question asks how to make several related changes behave as one complete action. For example, an application may create an order and add its items together. Every change should be saved only when all steps finish successfully. If any step fails, the earlier changes should be undone so the stored information is not left incomplete or incorrect. The answer should also explain how the program notices a failure, cancels unfinished work safely, and reports the problem instead of continuing as though the whole action succeeded.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Are all statements executed through the same PDO connection?
  • Do all affected tables and statements support transactions?
  • Is a particular isolation level or concurrency behavior required?
  • Should temporary failures such as deadlocks be retried?
How do you perform a database transaction with PDO? diagram
How to Explain It in an Interview

A database transaction groups related operations into one unit. In PDO, I call beginTransaction() before the first database change. I then execute every related statement through the same PDO connection. If every statement and required business check succeeds, I call commit(). Commit makes the transaction's changes permanent.

I configure PDO to report database errors as exceptions by setting PDO::ATTR_ERRMODE to PDO::ERRMODE_EXCEPTION. The transaction belongs inside a try block. If a PDO operation or an application-level validation check throws an exception, the catch block calls rollBack() when PDO::inTransaction() is true. Rollback cancels the uncommitted database changes. The application should then rethrow the original error or translate it at an appropriate boundary; it should not pretend the operation succeeded.

This provides atomicity. Atomicity means the related database changes are committed together or none of them are committed. For example, if creating an order succeeds but inserting one order item fails, rolling back also cancels the order insert.

The transaction should be kept short. Long transactions can hold locks or retain row versions and other database resources for longer. This can block competing work, increase contention, and make deadlocks more likely. Slow network calls, user interaction, file processing, and unrelated work should normally happen outside the transaction.

A transaction covers only operations performed by the participating database connection and supported transactional tables or statements. It does not automatically undo an email, file write, message publication, or external API request. Those side effects need a separate design, such as an outbox table, idempotent processing, or compensating work.

Prepared statements should bind untrusted values, but parameter binding and transactions solve different problems. Parameters protect values and improve statement handling; the transaction controls whether a group of database changes is committed. Dynamic identifiers such as table or column names cannot be made safe by binding them as parameters and should come from a trusted allowlist.

Database-specific behavior also matters. Some databases or statements can perform an implicit commit, especially certain schema-changing statements. PDO also does not provide portable nested transactions. If nested units are required, the application must use database-supported savepoints deliberately or structure the transaction ownership so only one layer begins and ends the transaction.

Technical Approach
  1. Obtain one PDO connection and enable exception mode.
  2. Validate data that does not require database locks before opening the transaction.
  3. Prepare the required statements.
  4. Call beginTransaction().
  5. Execute every related write through that same PDO connection.
  6. Check affected rows and any business conditions that must hold.
  7. Call commit() only after all required work succeeds.
  8. Catch Throwable so both database and application failures trigger cleanup.
  9. If inTransaction() is true, call rollBack().
  10. Rethrow, log, or translate the original failure at the appropriate application boundary.
  11. Retry only recognized temporary database failures, with a strict limit, when the whole operation is safe to repeat.
Practical Insights

The begin, commit, and rollback calls use very little PHP memory and add only a small amount of application work. The important cost is in the SQL statements, affected rows, indexes, constraints, logging, and locks or row versions maintained by the database. A longer transaction can delay other requests and increase contention or deadlock risk. Additional indexes can make writes slower because each affected index must be maintained. Retry logic and coordination with external side effects increase operational and maintenance complexity.

Code
<?php

declare(strict_types=1);

$pdo = new PDO(
    'mysql:host=127.0.0.1;dbname=shop;charset=utf8mb4',
    'app_user',
    'app_password',
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

$customerId = 42;
$items = [
    ['product_id' => 101, 'quantity' => 2, 'unit_price' => '19.99'],
    ['product_id' => 205, 'quantity' => 1, 'unit_price' => '8.50'],
];

$insertOrder = $pdo->prepare(
    'INSERT INTO orders (customer_id, status)
     VALUES (:customer_id, :status)'
);

$insertItem = $pdo->prepare(
    'INSERT INTO order_items
        (order_id, product_id, quantity, unit_price)
     VALUES
        (:order_id, :product_id, :quantity, :unit_price)'
);

try {
    $pdo->beginTransaction();

    $insertOrder->execute([
        'customer_id' => $customerId,
        'status' => 'pending',
    ]);

    $orderId = (int) $pdo->lastInsertId();

    foreach ($items as $item) {
        if ($item['quantity'] <= 0) {
            throw new InvalidArgumentException(
                'Quantity must be greater than zero.'
            );
        }

        $insertItem->execute([
            'order_id' => $orderId,
            'product_id' => $item['product_id'],
            'quantity' => $item['quantity'],
            'unit_price' => $item['unit_price'],
        ]);
    }

    $pdo->commit();
} catch (Throwable $error) {
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }

    throw $error;
}
Why Interviewers Ask This

Interviewers want to confirm that the candidate can keep related database writes consistent. A strong answer demonstrates correct PDO transaction boundaries, exception handling, rollback behavior, use of a single connection, and an understanding of atomicity when one step in a multi-step operation fails.

Common interview mistakes

Common mistakes include committing before all required operations succeed, forgetting to roll back after a failure, running part of the work through another connection, swallowing the exception and reporting success, opening a transaction before slow unrelated work, assuming rollback reverses external side effects, using non-transactional tables or statements without checking database behavior, attempting unsupported nested transactions, interpolating untrusted values into SQL, and retrying every error without limiting retries or making the operation safe to repeat.

Interview tip

Explain the path in order: begin, execute, validate, commit, catch, rollback, and rethrow. Define atomicity in one sentence, state that all statements must use the same connection, and mention one production tradeoff: keep the transaction short to reduce contention and deadlock risk.

Interviewer may ask next
Why should you check inTransaction() before calling rollBack()?

A failure can happen before the transaction starts, after it has already ended, or while commit is being attempted. Calling rollBack() without an active transaction can throw another exception and obscure the original problem. inTransaction() reduces that risk by confirming that PDO still reports an active transaction.

Should a PDO transaction automatically be retried after a deadlock?

Only recognized temporary failures, such as a deadlock or serialization failure, should be retried. Restart the entire transaction from the beginning, use a small retry limit with delay or backoff, and retry only when repeating the operation cannot create duplicate external side effects.

57. How do transaction isolation levels affect concurrent PHP requests?Sql / DatabaseHard

Question Details

Compare dirty reads, non-repeatable reads, phantom reads, write conflicts, and practical tradeoffs among common isolation levels for a web application.

Short Interview Answer (30-60 seconds)

Isolation levels control what concurrent transactions can observe and how conflicts are handled. Lower levels permit more changing results. Higher levels provide stronger guarantees but may increase blocking or aborted transactions. PHP code should also use atomic SQL, constraints, short transactions, and bounded full-transaction retries.

Detailed Explanation

This question asks what can happen when several website requests read or change the same information at nearly the same time. One request may see work that is later cancelled, get different answers when reading twice, miss newly added items, or compete with another request changing the same information. The candidate should explain how stricter rules reduce these surprises while sometimes causing more waiting or requiring a request to try again. The best choice depends on how serious an incorrect result would be and how much waiting the application can accept.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Which database engine and storage engine are being used?
  • Are the requests mainly reading, writing, or doing both?
  • Must repeated reads inside one transaction remain stable?
  • Which business rule must remain true under concurrency?
  • Can the application safely retry the complete transaction?
How do transaction isolation levels affect concurrent PHP requests? diagram
How to Explain It in an Interview

A transaction is a group of database operations that commits as one unit or rolls back as one unit. Concurrent PHP requests normally use separate database sessions and may run transactions at the same time. The isolation level determines which effects of other transactions are visible and how the database handles certain conflicts.

The SQL standard defines minimum guarantees for isolation levels, but exact behavior varies by database engine. Some engines mainly use locks, some use multi-version concurrency control, and many use both. Therefore, I would identify the database before relying on implementation-specific behavior.

Dirty reads

A dirty read occurs when one transaction reads a value written by another transaction that has not committed. If the writer later rolls back, the reader used a value that never became permanent.

Under the SQL standard, READ UNCOMMITTED may permit dirty reads. Some database engines still prevent true dirty reads at this level or treat it like READ COMMITTED. Dirty reads are rarely acceptable for balances, inventory, permissions, order states, or other business decisions.

Non-repeatable reads

A non-repeatable read occurs when one transaction reads the same row twice and receives different committed values because another transaction updated or deleted that row between the reads.

At READ COMMITTED, each statement commonly sees data committed before that statement begins. A PHP transaction can therefore read one value and later receive a different value for the same row.

This may be acceptable for short independent queries. It is unsafe when application logic assumes an earlier value remains unchanged before making a later decision or write.

Phantom reads

A phantom read occurs when one transaction repeats a condition-based query and receives a different set of matching rows because another transaction inserted, deleted, or changed rows between the two queries.

For example, one request counts pending orders twice while another request inserts a new pending order. At an isolation level that permits phantoms, the second query may contain an additional row.

The SQL standard requires phantom prevention at SERIALIZABLE. Practical behavior at REPEATABLE READ differs among engines. Snapshot-based ordinary reads may remain stable, while locking reads, writes, and predicate protection may follow different rules.

Write conflicts and lost updates

Read anomalies are not the only concern. Two PHP requests may update the same row, reserve the same item, or enforce a rule involving several rows.

A direct write conflict may cause one transaction to wait, deadlock, receive a serialization failure, or fail with another database-specific error. An unsafe read-modify-write sequence may also produce a lost update, where one request overwrites another request's result.

For example:

1. Request A reads stock as 10. 2. Request B reads stock as 10. 3. Both calculate a new value independently. 4. Both write their calculated value.

Depending on the SQL and database behavior, one write may overwrite the effect of the other.

A safer approach is an atomic conditional update that reduces stock only when enough stock remains, followed by checking the affected-row count. Other protections include row locking, optimistic version checks, and database constraints.

Write skew

Snapshot-based isolation can allow write skew even when each transaction sees a stable snapshot. Two transactions read the same multi-row condition, update different rows, and together violate a business rule.

For example, two requests both see that two staff members are on call. Each request independently removes a different staff member, leaving nobody on call. Because the transactions update different rows, a simple same-row write conflict may not occur.

A serializable transaction, targeted locking, an enforceable constraint, or a redesigned data model may be required for this rule.

Common isolation levels
READ UNCOMMITTED

This is the weakest standard level. Dirty reads, non-repeatable reads, and phantom reads may occur under the standard model.

It is rarely appropriate for PHP business workflows. The exact database implementation still matters because some engines provide stronger behavior than the standard minimum.

READ COMMITTED

Dirty reads are prevented. Separate statements in the same transaction may observe newly committed changes, so non-repeatable reads and phantom reads may occur.

This is a practical default for many short web transactions because it usually provides good concurrency. Application code must not assume that an earlier read remains current. Important writes should use atomic SQL, constraints, explicit locks, or optimistic concurrency checks.

REPEATABLE READ

The SQL standard prevents dirty reads and non-repeatable reads at this level, but it does not require the same phantom protection as SERIALIZABLE. Actual implementations differ substantially.

A multi-version database may provide one stable snapshot for ordinary reads. That does not automatically prevent every lost update, write skew, deadlock, or conflicting write. Locking reads may also behave differently from ordinary snapshot reads.

This level is useful when several related reads should use one consistent view, but the application must understand whether that view can become stale relative to concurrent commits.

SERIALIZABLE

SERIALIZABLE provides the strongest standard isolation. The committed result must be equivalent to transactions running in some valid serial order, even if the database executes them concurrently.

A database may enforce this by blocking operations, using row, range, or predicate locks, detecting dangerous dependency patterns, or aborting a transaction. Serializable isolation therefore does not mean every request succeeds immediately. PHP code must be prepared for deadlocks or serialization failures and may need to retry the complete transaction.

Choosing the practical level

For ordinary short create, read, update, and delete requests, READ COMMITTED is often a reasonable starting point when combined with atomic SQL and database constraints.

Use REPEATABLE READ when several related reads must use a stable view and the selected database's exact semantics fit the workflow.

Use SERIALIZABLE when correctness depends on a multi-row or predicate-based rule that cannot be protected reliably with a simpler atomic statement, constraint, optimistic version check, or targeted lock.

Do not automatically choose the strongest level for every request. Stronger isolation can increase waiting, aborted transactions, lock contention, retained row versions, cleanup work, and retry cost. In lock-based systems it can also increase deadlock opportunities. Under high contention, it may reduce throughput.

Do not choose weaker isolation only for speed. Incorrect inventory, duplicate redemption, or invalid account state may cost far more than a properly designed transaction.

Database protections that complement isolation

Isolation should be combined with features that express the business rule directly:

  • Use atomic conditional updates for counters, balances, and inventory changes.
  • Use unique constraints to prevent duplicate identifiers or one-time claims.
  • Use foreign-key and check constraints for relationships and row-level rules.
  • Use SELECT ... FOR UPDATE or the database's equivalent when specific rows must be locked before related writes.
  • Use a version column or expected previous value for optimistic concurrency.
  • Use indexes that support important search and locking predicates.

Prepared statements and parameter binding protect data values from SQL injection. They do not provide transaction isolation and do not prevent concurrency anomalies. Dynamic identifiers cannot be made safe merely by binding them as parameters; they must be selected from a trusted allowlist and quoted using database-specific rules when necessary.

PHP and PDO responsibilities

PDO does not define one universal isolation implementation. The selected PDO driver sends transaction and SQL commands to the database, and the database engine supplies the behavior.

A PHP request should:

  1. Use the same PDO connection for the entire transaction.
  2. Configure the required isolation level using syntax supported by that database and at the time required by that database.
  3. Begin the transaction immediately before the protected database work.
  4. Keep HTTP calls, file operations, user interaction, and slow computation outside the transaction.
  5. Commit only after every required statement succeeds.
  6. Roll back after an exception or validation failure.
  7. Recognize retryable failures through database-specific SQLSTATE values or driver error codes.
  8. Retry the entire transaction, not only the failed statement.
  9. Use a small retry limit and backoff to avoid retry storms.
  10. Prevent external side effects from being duplicated during retries.

PDO::beginTransaction() starts a transaction but does not itself select the desired isolation level. Isolation must be configured according to the database's supported commands and connection rules.

A retry must re-run all reads and writes because the database state may have changed. Retrying only the failed statement can make it inconsistent with decisions based on earlier reads.

Connection lifecycle

Isolation settings can be transaction-scoped or session-scoped depending on the database and command used. Persistent PDO connections, long-running PHP workers, connection pools, and database proxies may reuse a session. Session-level changes can therefore affect later work if they are not reset.

Traditional request-based PHP deployments often release ordinary non-persistent connections at the end of a request. Applications should still understand their actual connection lifecycle rather than assuming every request always receives a completely new database session.

Performance, storage, and memory tradeoffs

Isolation normally does not change the formal Big O complexity of the business algorithm. An indexed lookup remains an indexed lookup. However, it can significantly change operational cost.

Lock-based implementations may make transactions wait and can create deadlocks when transactions acquire incompatible locks in different orders. Multi-version implementations may retain older row versions while transactions or snapshots remain active. Those versions are normally stored and managed by the database, not in PHP application memory, but they can increase database storage, cleanup, vacuum, undo-log, or version-chain work depending on the engine.

Serializable implementations may block transactions or abort transactions that would complete at a weaker level. Retries consume additional database work, PHP execution time, and connection capacity.

Long transactions make these costs worse. Good indexes reduce unnecessary scanning and may narrow the rows or key ranges involved, but they do not guarantee that blocking, phantom protection, deadlocks, or serialization failures will disappear.

Technical Approach
  1. Identify the exact business invariant that concurrent requests must preserve.
  2. Determine whether the risk is a dirty read, non-repeatable read, phantom, lost update, write skew, duplicate creation, or direct write conflict.
  3. Confirm the database engine and its exact isolation semantics.
  4. Prefer an atomic SQL statement or database constraint when it can express the rule.
  5. Add optimistic concurrency or targeted row locking when appropriate.
  6. Choose the weakest isolation level that still preserves correctness.
  7. Keep the transaction short and use supporting indexes.
  8. Roll back on failure.
  9. Retry the complete transaction only for recognized transient database errors, using a strict attempt limit and backoff.
  10. Test the workflow with genuinely concurrent requests.
Practical Insights

Isolation usually does not change the formal time or memory complexity of the business algorithm. Its main costs are operational. Locks can make requests wait. Deadlocks and serialization failures can force complete retries. Multi-version databases may retain older row versions, increasing database storage and cleanup work rather than PHP heap memory. Stronger isolation can reduce throughput when many requests compete for the same data. Good indexes reduce unnecessary scanning and may reduce contention, but they do not eliminate every conflict. Retry handling, database-specific error detection, idempotency, and connection-state cleanup also increase implementation and maintenance cost.

Why Interviewers Ask This

Interviewers want to verify that the candidate understands how concurrent web requests interact through a database, can distinguish dirty reads, non-repeatable reads, phantom reads, lost updates, write conflicts, and write skew, and can choose practical protections without assuming that isolation alone solves every consistency problem.

Common interview mistakes

Common mistakes include assuming PHP requests execute one at a time; assuming READ COMMITTED prevents lost updates; describing all databases as having identical isolation behavior; confusing a stable snapshot with serializable execution; ignoring write skew; using a read followed by an unconditional write; relying only on application checks instead of database constraints; using SELECT ... FOR UPDATE outside a suitable transaction; holding transactions open during HTTP calls; changing isolation at an invalid point; omitting indexes for important predicates; treating every database exception as retryable; retrying only the failed statement; retrying without a limit; duplicating an external side effect during a retry; assuming prepared statements prevent concurrency problems; and assuming parameter binding makes dynamic table or column identifiers safe.

Interview tip

Start with the practical rule: choose the weakest isolation level that still protects the business invariant. Define each read anomaly, discuss write conflicts and write skew separately, acknowledge database-specific behavior, and finish with atomic SQL, constraints, short transactions, and bounded full-transaction retries.

Interviewer may ask next
How should a PHP application handle a deadlock or serialization failure?

It should roll back, inspect the database-specific SQLSTATE or driver error code, and retry only when the error is known to be transient. The application must start a new transaction and repeat every read and write because earlier decisions may no longer be valid. Retries need a small limit and backoff, and any external side effect must be idempotent, deferred, or coordinated so that it is not duplicated.

Does SERIALIZABLE remove the need for constraints, locks, or atomic updates?

No. Serializable isolation controls the outcome of concurrent transactions, but constraints remain the final protection for enforceable data rules. Atomic updates are often simpler and create less contention. Targeted locks, optimistic checks, or data-model changes may also express the rule more clearly. Serializable isolation is most useful when a multi-row or predicate-based invariant cannot be protected reliably by those simpler mechanisms.

58. How do you implement pagination safely and efficiently in a PHP application?Sql / DatabaseMedium

Question Details

Compare LIMIT/OFFSET pagination with keyset pagination, define stable ordering, bind pagination values safely, and discuss behavior when rows are inserted or deleted between requests.

Short Interview Answer (30-60 seconds)

I use LIMIT/OFFSET for small lists that require direct page-number navigation. For large or changing datasets, I prefer keyset pagination with a unique indexed order such as created_at and id. I validate limits, bind values with PDO, and create the next cursor from the final returned row.

Detailed Explanation

See the Code while reading this explanation.

Pagination returns a large list in smaller groups. The application must keep the order predictable, prevent unsafe input, and decide where each new group begins. A simple page number works well for short lists, but later pages can become slower and changing records can move between groups. A saved position works better for long or frequently changing lists, although it cannot easily jump to any numbered page. The correct design depends on how users navigate, how often the list changes, and whether every request must represent the same frozen view of the data.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Does the interface require direct jumps to page numbers, or only next and previous navigation?
  • How large can the result set become, and how deeply do users normally browse?
  • Can rows be inserted, deleted, or have their ordering values updated while a user is paging?
  • Which columns define the required business order?
  • Is an exact total row count required on every request?
How do you implement pagination safely and efficiently in a PHP application? diagram
How to Explain It in an Interview

I start by defining deterministic ordering. Deterministic ordering means every row has one predictable position. Ordering only by a non-unique value such as created_at is insufficient because multiple rows can share the same timestamp. I add a unique tie-breaker, normally the primary key:

ORDER BY created_at DESC, id DESC

For MySQL 8 or later, a matching index can be created as follows:

CREATE INDEX idx_articles_pagination ON articles (created_at DESC, id DESC);

Whether the optimizer uses that index also depends on filters, selected columns, table statistics, and the query plan, so I verify important queries with EXPLAIN rather than assuming the index will always be chosen.

For a small result set or an interface that requires numbered pages, LIMIT/OFFSET is reasonable:

SELECT id, title, created_at FROM articles ORDER BY created_at DESC, id DESC LIMIT :limit OFFSET :offset;

The PHP application validates the page number and page size before calculating the offset. It should reject or cap values that could overflow the application's integer range or cause unreasonable database work. LIMIT and OFFSET are data values in this fixed statement, so they can be bound as PDO::PARAM_INT when the database driver supports placeholders there. Dynamic table names, column names, and sort directions are identifiers or SQL syntax, not data values. Prepared statements do not make them safe; they must be selected from a fixed server-side allowlist.

LIMIT/OFFSET has two important limitations. First, the database still has to process or skip rows before the requested offset, so deep pages can become increasingly expensive. The exact cost depends on the execution plan and available indexes; it is not correct to promise a fixed complexity for every database and query. Second, offsets describe positions rather than row identities. If a new row is inserted before the next offset, a row already seen can shift into the next page and appear again. If an earlier row is deleted, an unseen row can shift into an already skipped position and be missed.

For large feeds, histories, logs, or frequently changing result sets, I prefer keyset pagination, also called seek or cursor pagination. The cursor contains the complete ordering values from the final row returned to the client. With descending created_at and id ordering, the next-page query is:

SELECT id, title, created_at FROM articles WHERE created_at < :created_at_before OR (created_at = :created_at_equal AND id < :id) ORDER BY created_at DESC, id DESC LIMIT :limit;

The two created_at placeholders are intentionally different. With native PDO prepared statements, a named placeholder should not be reused in the same statement. Both placeholders are bound to the same cursor value.

When the composite index and predicates fit the query, the database can begin near the cursor boundary instead of repeatedly skipping every earlier result. This usually makes the amount of work per page much more stable than a deep offset, but filters, joins, sorting, data distribution, and optimizer choices can still change the actual plan. I confirm production behavior with EXPLAIN and representative data.

Keyset pagination behaves more predictably when rows are inserted before the cursor. Those new rows do not move the saved boundary, so they do not normally cause the offset-style duplicate on subsequent pages. A new row that sorts after the boundary can appear on a later page. A deleted row simply cannot be returned. However, if an existing row's created_at or other ordering value is updated, it can move across the boundary and may be skipped or seen again. An immutable ordering value reduces this risk.

A cursor is client input, so I decode it, validate its structure and types, limit its length, and bind every value. Encoding a cursor with Base64 makes it opaque-looking but does not prevent modification. When cursor values must not be changed, I sign the serialized payload with an HMAC using a server-side secret and verify the signature before querying. Authorization must still be enforced independently; a signed cursor must not grant access to rows the current user cannot view.

Each HTTP page request normally uses its own short database transaction. A transaction or repeatable-read snapshot from one request does not automatically continue into the next request after the connection and transaction end. Keeping one transaction open while a user browses multiple pages is usually impractical and can retain database resources or old row versions. If the product requires every page to represent one frozen result set, I use an explicit snapshot design, such as immutable versioned data, a materialized result, or stored matching identifiers with an expiration policy.

Keyset pagination cannot efficiently jump directly to arbitrary page 500 because that page's starting cursor is not known. It is therefore best for next-page or previous-page navigation. LIMIT/OFFSET remains appropriate when direct page jumps are a real requirement and expected offsets remain controlled. The production choice should be based on navigation needs, data volatility, measured query plans, and acceptable consistency behavior.

Key Insight / Why This Solution Works
  1. Define the required business order and add a unique tie-breaker so the order is deterministic.
  2. Create and test a composite index that matches the filtering and ordering pattern.
  3. Set a server-side minimum and maximum page size.
  4. For LIMIT/OFFSET, validate the page number before calculating the offset and reject values that overflow or exceed an allowed depth.
  5. Use LIMIT/OFFSET for small or shallow lists that genuinely require direct page-number navigation.
  6. Use keyset pagination for large or frequently changing lists with sequential navigation.
  7. Decode the cursor, limit its encoded length, validate every field, and verify its signature when tamper resistance is required.
  8. Use a fixed SQL statement and bind all cursor, limit, and offset data values with appropriate PDO types.
  9. Fetch page size plus one row to determine whether another page exists.
  10. Remove the look-ahead row and create the next cursor from the last row actually returned.
  11. Use EXPLAIN with representative data to confirm index access, examined rows, and sorting behavior.
  12. Document how inserts, deletions, and updates to ordering columns affect navigation.
Code
<?php

declare(strict_types=1);

/**
 * @param array{created_at: string, id: int} $payload
 */
function encodeCursor(array $payload): string
{
    $json = json_encode($payload, JSON_THROW_ON_ERROR);

    return rtrim(strtr(base64_encode($json), '+/', '-_'), '=');
}

/**
 * @return array{created_at: string, id: int}
 */
function decodeCursor(string $cursor): array
{
    if (strlen($cursor) > 512 || !preg_match('/^[A-Za-z0-9_-]+$/', $cursor)) {
        throw new InvalidArgumentException('Invalid cursor format.');
    }

    $paddingLength = (4 - strlen($cursor) % 4) % 4;
    $base64 = strtr($cursor . str_repeat('=', $paddingLength), '-_', '+/');
    $decoded = base64_decode($base64, true);

    if ($decoded === false) {
        throw new InvalidArgumentException('Invalid cursor encoding.');
    }

    $data = json_decode($decoded, true, 16, JSON_THROW_ON_ERROR);

    if (!is_array($data) || array_keys($data) !== ['created_at', 'id']) {
        throw new InvalidArgumentException('Invalid cursor data.');
    }

    if (
        !is_string($data['created_at'])
        || preg_match('/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,6})?$/', $data['created_at']) !== 1
    ) {
        throw new InvalidArgumentException('Invalid cursor timestamp.');
    }

    $id = filter_var($data['id'], FILTER_VALIDATE_INT);
    if ($id === false || $id < 1) {
        throw new InvalidArgumentException('Invalid cursor id.');
    }

    return [
        'created_at' => $data['created_at'],
        'id' => $id,
    ];
}

/**
 * @return array{
 *     items: list<array{id: int, title: string, created_at: string}>,
 *     next_cursor: ?string
 * }
 */
function fetchArticlePage(PDO $pdo, int $requestedLimit, ?string $cursor): array
{
    $limit = max(1, min($requestedLimit, 100));
    $fetchLimit = $limit + 1;

    if ($cursor === null) {
        $sql = <<<'SQL'
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC, id DESC
LIMIT :limit
SQL;

        $statement = $pdo->prepare($sql);
        $statement->bindValue(':limit', $fetchLimit, PDO::PARAM_INT);
    } else {
        $boundary = decodeCursor($cursor);

        $sql = <<<'SQL'
SELECT id, title, created_at
FROM articles
WHERE created_at < :created_at_before
   OR (created_at = :created_at_equal AND id < :id)
ORDER BY created_at DESC, id DESC
LIMIT :limit
SQL;

        $statement = $pdo->prepare($sql);
        $statement->bindValue(':created_at_before', $boundary['created_at'], PDO::PARAM_STR);
        $statement->bindValue(':created_at_equal', $boundary['created_at'], PDO::PARAM_STR);
        $statement->bindValue(':id', $boundary['id'], PDO::PARAM_INT);
        $statement->bindValue(':limit', $fetchLimit, PDO::PARAM_INT);
    }

    $statement->execute();
    $rows = $statement->fetchAll(PDO::FETCH_ASSOC);

    $hasMore = count($rows) > $limit;
    if ($hasMore) {
        array_pop($rows);
    }

    $items = array_map(
        static fn(array $row): array => [
            'id' => (int) $row['id'],
            'title' => (string) $row['title'],
            'created_at' => (string) $row['created_at'],
        ],
        $rows
    );

    $nextCursor = null;
    if ($hasMore && $items !== []) {
        $lastItem = $items[array_key_last($items)];
        $nextCursor = encodeCursor([
            'created_at' => $lastItem['created_at'],
            'id' => $lastItem['id'],
        ]);
    }

    return [
        'items' => $items,
        'next_cursor' => $nextCursor,
    ];
}

$dsn = getenv('DATABASE_DSN') ?: 'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4';
$username = getenv('DATABASE_USER') ?: 'app';
$password = getenv('DATABASE_PASSWORD') ?: '';

$pdo = new PDO($dsn, $username, $password, [
    PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
    PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
    PDO::ATTR_EMULATE_PREPARES => false,
]);

$rawLimit = filter_input(INPUT_GET, 'limit', FILTER_VALIDATE_INT);
$requestedLimit = is_int($rawLimit) ? $rawLimit : 25;

$rawCursor = filter_input(INPUT_GET, 'cursor', FILTER_UNSAFE_RAW);
$cursor = is_string($rawCursor) && $rawCursor !== '' ? $rawCursor : null;

try {
    $result = fetchArticlePage($pdo, $requestedLimit, $cursor);

    header('Content-Type: application/json; charset=utf-8');
    echo json_encode($result, JSON_THROW_ON_ERROR | JSON_UNESCAPED_SLASHES);
} catch (InvalidArgumentException | JsonException $exception) {
    http_response_code(400);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['error' => 'Invalid pagination cursor.'], JSON_THROW_ON_ERROR);
} catch (PDOException) {
    http_response_code(500);
    header('Content-Type: application/json; charset=utf-8');
    echo json_encode(['error' => 'Database request failed.'], JSON_THROW_ON_ERROR);
}
Why Interviewers Ask This

Interviewers want to see whether the candidate can build pagination that remains secure, predictable, and efficient as data grows or changes. The question tests deterministic SQL ordering, composite indexes, prepared statements, PDO parameter types, cursor design, query-plan awareness, and consistency across separate requests. It also tests whether the candidate understands that LIMIT/OFFSET and keyset pagination solve different navigation requirements and have different behavior when rows are inserted, deleted, or reordered.

Common interview mistakes

Common mistakes include omitting ORDER BY; ordering by a non-unique column without a unique tie-breaker; using deep offsets without measuring the plan; forgetting a matching composite index; trusting the optimizer to use an index without checking EXPLAIN; accepting unlimited page sizes or offsets; allowing page arithmetic to overflow; interpolating pagination values into SQL; treating user-provided identifiers or sort directions as bindable data; reusing the same named PDO placeholder in a native prepared statement; binding LIMIT or OFFSET with an unsuitable type; placing only id in the cursor when the query primarily orders by created_at; using comparison directions that conflict with ORDER BY; creating the next cursor from the removed look-ahead row; assuming Base64 encoding prevents cursor changes; treating a signed cursor as authorization; claiming keyset pagination creates a frozen snapshot; and ignoring updates that move rows across the cursor boundary.

Interview tip

Begin with the choice: LIMIT/OFFSET for controlled numbered pages and keyset pagination for scalable sequential navigation. Then explain deterministic ordering, the matching index, safe PDO binding, and the cursor predicate. Finish with measured query plans, insert and delete behavior, ordering-column updates, snapshot limitations, and the inability of keyset pagination to jump directly to an arbitrary page.

Interviewer may ask next
How would you paginate in ascending order when created_at is not unique?

I would use ORDER BY created_at ASC, id ASC, with id as the unique tie-breaker. The next-page predicate would be created_at > :created_at_before OR (created_at = :created_at_equal AND id > :id). The cursor would contain both values, and I would test a matching composite index and the actual execution plan.

How would you stop clients from modifying a keyset cursor?

Base64 encoding is not protection. I would serialize only the permitted cursor fields, sign the serialized payload with an HMAC using a server-side secret, and verify the signature before decoding and querying. I would still validate every field, bind every value through PDO, enforce expiration when needed, and apply normal authorization independently.

59. What is the difference between PDO and MySQLi in PHP?Sql / DatabaseEasy

Question Details

Compare database support, procedural versus object-oriented APIs, named parameters, prepared statements, transactions, error handling, and portability.

Short Interview Answer (30-60 seconds)

PDO supports multiple database drivers, named or positional placeholders, and an object-oriented API. MySQLi targets MySQL and provides object-oriented and procedural APIs with positional placeholders. Both support prepared statements, transactions, and exceptions. Choose PDO for broader portability and MySQLi for MySQL-specific development.

Detailed Explanation

See the Code while reading this explanation.

This question asks you to compare two ways a PHP program can save and read information. The interviewer wants to know which choice works with more storage products, which writing styles each choice provides, and how they handle safe input, groups of changes, and failures. You should also explain whether changing the storage product later would be easier and when closer access to one product is useful. A good answer does not say that one choice is always better. It selects the choice that matches the application's actual needs.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Will the application use only MySQL, or might it support another database later?
  • Does the application require any MySQL-specific feature?
  • Does the team prefer named placeholders or a procedural API?
What is the difference between PDO and MySQLi in PHP? diagram
How to Explain It in an Interview

PDO means PHP Data Objects. It is a core PHP database-access extension that provides one object-oriented interface for multiple database drivers, including MySQL, PostgreSQL and SQLite. The correct driver must be installed for the selected database. PDO is a data-access abstraction, not a complete database abstraction layer: it gives PHP code a similar interface, but it does not rewrite database-specific SQL or make schemas and behavior identical.

MySQLi means MySQL Improved. It is a core PHP extension designed specifically for MySQL. It is also commonly used with MariaDB because MariaDB supports the MySQL client protocol, but compatibility with a particular MariaDB feature or version must still be verified. MySQLi offers both object-oriented and procedural APIs. PDO offers an object-oriented API only.

PDO supports named placeholders such as :email and positional placeholders such as ?. A single PDO statement must use one placeholder style, not mix both styles. MySQLi prepared statements use positional ? placeholders and normally require a type string when values are bound with bind_param().

In both extensions, placeholders represent complete data values only. They cannot represent a table name, column name, SQL keyword, sort direction, operator, or an entire list of values. Dynamic identifiers or keywords must be selected from a strict trusted allowlist. Untrusted input must never be inserted directly into those parts of the SQL statement.

Both PDO and MySQLi support prepared statements. Prepared statements separate the SQL structure from its values, which prevents bound values from being interpreted as SQL. They can also reduce repeated preparation work when the same statement is executed many times. They are not automatically faster for a single execution because preparing and executing may require additional work or network round trips.

PDO drivers may use native server-side prepared statements or emulated prepares. With the MySQL PDO driver, PDO::ATTR_EMULATE_PREPARES => false requests native prepares. Native and emulated modes can differ in parsing, supported statements, reported error timing and placeholder handling, so the application should test the selected mode instead of assuming they behave identically. MySQLi prepared statements use MySQL's prepared-statement protocol.

Both extensions support transactions through methods for beginning, committing and rolling back a transaction. Transactions work only when the selected database objects and statements are transactional. For example, a MySQL table using a non-transactional storage engine cannot gain rollback behavior merely because PDO or MySQLi is used. Neither extension changes the database's isolation level, locking rules, constraints or implicit-commit behavior. Transaction boundaries should be explicit and short, and every failure path should either roll back or safely end the connection.

PDO reports failures with PDOException when exception mode is active. Modern PHP uses exception mode by default for PDO, but setting PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION explicitly makes the intended behavior clear. MySQLi throws mysqli_sql_exception when strict error reporting is enabled. Modern PHP enables strict MySQLi reporting by default, but calling mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT) explicitly avoids relying on environment assumptions. Production code should log internal database details securely and return a generic message to the client.

PDO and MySQLi normally create non-persistent connections, which PHP closes when the connection object is destroyed or the request ends. PDO can request persistent connections with PDO::ATTR_PERSISTENT, but persistence is not automatically faster and may retain session state, temporary settings or an unfinished transaction if cleanup is poor. Connection pooling and persistence should therefore be chosen only after measurement and careful state management.

The practical decision is: choose PDO when the application needs a consistent API across supported database drivers or benefits from named placeholders. Choose MySQLi when the application is intentionally MySQL-specific, requires its procedural API, or benefits from direct MySQL-oriented functionality. Security and performance depend mainly on correct SQL, parameter binding, indexes, result size, transaction design and connection management rather than on the extension name alone.

Key Insight / Why This Solution Works
  1. Confirm which database servers the application must support.
  2. Check whether database portability is a real requirement rather than a theoretical possibility.
  3. Identify any required MySQL-specific functionality.
  4. Decide whether named placeholders or a procedural API matters to the codebase.
  5. Choose and explicitly configure exception handling and prepared-statement behavior.
  6. Use prepared statements for untrusted values and allowlists for dynamic identifiers or keywords.
  7. Define clear transaction boundaries and rollback paths.
  8. Benchmark real queries before making performance or persistent-connection decisions.
Code
<?php

declare(strict_types=1);

function findUserWithPdo(string $email): ?array
{
    $pdo = new PDO(
        'mysql:host=127.0.0.1;dbname=app;charset=utf8mb4',
        'app_user',
        'replace_with_secret',
        [
            PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
            PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
            PDO::ATTR_EMULATE_PREPARES => false,
        ]
    );

    $statement = $pdo->prepare(
        'SELECT id, email, display_name FROM users WHERE email = :email LIMIT 1'
    );
    $statement->execute(['email' => $email]);

    $user = $statement->fetch();
    $statement->closeCursor();
    $pdo = null;

    return $user === false ? null : $user;
}

function findUserWithMysqli(string $email): ?array
{
    mysqli_report(MYSQLI_REPORT_ERROR | MYSQLI_REPORT_STRICT);

    $mysqli = new mysqli(
        '127.0.0.1',
        'app_user',
        'replace_with_secret',
        'app'
    );
    $mysqli->set_charset('utf8mb4');

    $statement = $mysqli->prepare(
        'SELECT id, email, display_name FROM users WHERE email = ? LIMIT 1'
    );
    $statement->bind_param('s', $email);
    $statement->execute();
    $statement->bind_result($id, $storedEmail, $displayName);

    $user = null;

    if ($statement->fetch()) {
        $user = [
            'id' => $id,
            'email' => $storedEmail,
            'display_name' => $displayName,
        ];
    }

    $statement->close();
    $mysqli->close();

    return $user;
}

try {
    $email = 'candidate@example.com';

    var_dump(findUserWithPdo($email));
    var_dump(findUserWithMysqli($email));
} catch (PDOException | mysqli_sql_exception $exception) {
    error_log($exception->getMessage());
    exit('A database operation failed.');
}
Why Interviewers Ask This

Interviewers ask this question to verify that a PHP developer understands the language's two main database-access extensions and can choose between portability and MySQL-specific functionality. The answer also reveals whether the candidate understands API styles, placeholders, prepared statements, transactions, error handling, connection lifecycle, and the limits of database portability.

Common interview mistakes

Common mistakes include calling PDO an ORM, claiming PDO makes SQL fully portable, or saying MySQLi has no object-oriented API. Candidates may incorrectly claim that PDO supports only named placeholders or that MySQLi supports named placeholders. Another serious mistake is using placeholders for table names, column names, sort directions or a comma-separated IN list. Other errors include assuming prepared statements are always faster, assuming all MySQL tables support rollback, ignoring PDO emulated-prepare differences, relying on mysqli_stmt::get_result() without considering mysqlnd, exposing database exceptions to users, or enabling persistent connections without resetting connection state.

Interview tip

Start with the decision: PDO for multiple database drivers and named placeholders; MySQLi for a deliberately MySQL-specific application and procedural or direct MySQL-oriented access. Then compare prepared statements, transactions and exceptions. Mention that PDO does not make SQL portable and that neither extension makes dynamic identifiers safe.

Interviewer may ask next
Does using PDO make an application fully portable between MySQL and PostgreSQL?

No. PDO provides a similar PHP interface through different drivers, but it does not rewrite SQL or normalize database behavior. SQL syntax, data types, generated keys, schema definitions, functions, error codes, transaction behavior and locking rules may differ. Real portability requires intentionally portable SQL, database-specific adapters and testing against every supported database.

Can PDO or MySQLi placeholders be used for table names, sort directions or an IN list?

No. A placeholder represents one complete data value. It cannot represent an identifier, keyword, operator or several comma-separated values. Select table names, column names and sort directions from strict trusted allowlists. For an IN condition, generate one placeholder for each value and bind every value separately.

60. What are prepared statements, and why should PHP applications use them?Sql / DatabaseEasy

Question Details

Explain parameter binding, separation of SQL code from data, SQL-injection prevention, repeated execution, and important limitations such as dynamic identifiers.

Short Interview Answer (30-60 seconds)

Prepared statements define fixed SQL with placeholders and send values separately. PHP applications should use them because supplied values cannot change the intended SQL structure, preventing SQL injection through those values. They also support clean repeated execution, but dynamic identifiers require a trusted allowlist.

Detailed Explanation

See the Code while reading this explanation.

This question asks how a PHP program can safely send a command and changing information to a data store. The program first defines the command with empty positions where the changing information belongs. It then supplies each item separately. Because the command and the supplied information remain separate, text entered by a person cannot secretly change the command. The same command can also be used again with different information. However, names that change the command itself, such as a field used for sorting, must be selected from a fixed trusted list.

Useful clarifying questions:

Useful Questions to Ask the Interviewer
  • Should I demonstrate PDO, MySQLi, or both?
  • Should I include an example of executing one statement repeatedly?
  • Should I explain how to handle dynamic column names or sort directions?
What are prepared statements, and why should PHP applications use them? diagram
How to Explain It in an Interview

A prepared statement is an SQL statement whose structure is established separately from the data values used when it runs. The SQL contains parameter markers, also called placeholders, such as :email or ?. The PHP application prepares the SQL and then supplies a complete value for every placeholder when executing the statement.

The practical decision is to use prepared statements whenever values can change, especially when any value comes from an HTTP request, form, API request, command-line argument, file, message, or another external source. The application must not concatenate or interpolate an untrusted value into SQL.

Prepared statements prevent SQL injection through bound values because the SQL structure and parameter values are handled separately. A value containing quotes, comments, operators, or other SQL-looking characters remains a data value. It is not interpreted as an additional SQL condition or command. This removes the need to construct SQL by manually quoting or escaping each value.

With PDO, the normal process is:

  1. Create the PDO connection and enable exception-based error handling.
  2. Write fixed SQL containing named or positional placeholders.
  3. Call PDO::prepare() once to obtain a PDOStatement.
  4. Supply one value for each placeholder with bindValue(), bindParam(), or the parameter array accepted by execute().
  5. Execute the statement and process the result.
  6. Reuse the same statement with new values when the SQL structure remains unchanged.

Named and positional placeholders must not be mixed in the same statement. Each placeholder represents one complete data literal. A placeholder should not be enclosed in SQL quotes because the driver handles the value. One placeholder also cannot represent a list of values. For an IN clause, the application must create the required number of placeholders and bind every list element separately.

bindValue() binds the value available at the time of the call. bindParam() binds a PHP variable by reference, so its value is read when the statement executes. Passing an array to execute() is convenient, but PDO treats values in that array as strings unless values were explicitly bound with another PDO parameter type. Explicit types such as PDO::PARAM_INT, PDO::PARAM_BOOL, PDO::PARAM_NULL, and PDO::PARAM_STR can be useful when the database or query requires clear type handling.

A prepared statement can be executed repeatedly with different parameter values. Preparing once and reusing the resulting statement avoids repeatedly constructing SQL in PHP. Native database preparation may also reduce repeated parsing or allow the driver or server to reuse statement metadata or planning work. The actual benefit depends on the PDO driver, database server, connection lifetime, statement type, and number of executions. Prepared statements should therefore be presented primarily as a correctness and security practice, not as a guaranteed performance optimization.

Prepared statements have a critical limitation: placeholders can represent data values only. They cannot bind a table name, column name, SQL keyword, operator, clause, placeholder list, or sort direction. For example, ORDER BY :column does not make a user-supplied column name safe and normally orders by a bound value rather than selecting an identifier.

When part of the SQL structure must vary, the application should map a limited user-facing choice to a hard-coded trusted SQL fragment. For example, name can map to display_name, and created can map to created_at. Only the mapped value should be placed in the SQL. Unknown choices should be rejected or replaced with a safe default. Untrusted text must never be copied directly into the SQL structure.

PDO drivers may use native prepared statements or emulated prepared statements. PDO::ATTR_EMULATE_PREPARES => false asks supported drivers to use native preparation, but PDO documentation notes that a driver may fall back to emulation when it cannot prepare a particular query natively. Support and behavior are driver-specific, so the application should test against its actual production driver and database. Correct placeholders and trusted SQL structure are required in either mode.

Prepared statements do not replace input validation, authorization, database constraints, transactions, indexes, query-plan analysis, least-privilege database accounts, safe exception handling, or secure connection management. They solve the specific problem of safely supplying data values to SQL. They do not determine whether a value is valid for the business, whether a user is allowed to perform the operation, or whether the query is efficient.

Key Insight / Why This Solution Works
  1. Keep the SQL structure fixed.
  2. Add one named or positional placeholder for each changing scalar value.
  3. Never mix named and positional placeholders in one statement.
  4. Create PDO with exception mode enabled.
  5. Prepare the statement once.
  6. Bind values with suitable PDO parameter types when type handling matters, or pass a parameter array to execute() for simple string values.
  7. Execute and fully process the result.
  8. Reuse the statement when running the same SQL structure with new values.
  9. Build variable-length placeholder lists safely when an IN clause is required.
  10. Handle identifiers, operators, and sort directions through hard-coded allowlist mappings rather than parameters.
Code
<?php

declare(strict_types=1);

$dsn = getenv('DB_DSN');
$username = getenv('DB_USER');
$password = getenv('DB_PASSWORD');

if ($dsn === false || $username === false || $password === false) {
    throw new RuntimeException('Database environment variables are missing.');
}

$pdo = new PDO(
    $dsn,
    $username,
    $password,
    [
        PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
        PDO::ATTR_EMULATE_PREPARES => false,
    ]
);

$insert = $pdo->prepare(
    'INSERT INTO users (email, display_name, is_active)
     VALUES (:email, :display_name, :is_active)'
);

$users = [
    [
        'email' => 'first@example.com',
        'display_name' => 'First User',
        'is_active' => true,
    ],
    [
        'email' => 'second@example.com',
        'display_name' => 'Second User',
        'is_active' => false,
    ],
];

foreach ($users as $user) {
    $insert->bindValue(':email', $user['email'], PDO::PARAM_STR);
    $insert->bindValue(':display_name', $user['display_name'], PDO::PARAM_STR);
    $insert->bindValue(':is_active', $user['is_active'], PDO::PARAM_BOOL);
    $insert->execute();
}

$requestedSort = $_GET['sort'] ?? 'created';
$allowedSortColumns = [
    'name' => 'display_name',
    'created' => 'created_at',
];
$sortColumn = $allowedSortColumns[$requestedSort] ?? 'created_at';

$select = $pdo->prepare(
    "SELECT id, email, display_name, created_at
     FROM users
     WHERE is_active = :is_active
     ORDER BY {$sortColumn} DESC"
);
$select->bindValue(':is_active', true, PDO::PARAM_BOOL);
$select->execute();

foreach ($select as $row) {
    echo $row['display_name'] . PHP_EOL;
}
Why Interviewers Ask This

Interviewers ask this question to verify that a PHP developer understands safe database access, parameter binding, the separation of SQL code from data, and the correct use of PDO. They also want to know whether the candidate understands repeated execution, avoids misleading performance claims, and recognizes that placeholders bind complete data values rather than identifiers or arbitrary SQL fragments.

Common interview mistakes

Common mistakes include concatenating or interpolating untrusted values into SQL, manually escaping input instead of binding it, placing quotes around placeholders, mixing named and positional placeholders, reusing one named placeholder where the driver requires unique markers, and passing more or fewer values than the statement contains. Other mistakes include trying to bind a table name, column name, operator, sort direction, or an entire IN list; assuming prepared statements provide authorization or business validation; claiming they always improve speed; ignoring driver differences between native and emulated preparation; and fetching very large result sets into memory even though parameter binding itself does not require that buffering.

Interview tip

Start with the practical rule: keep SQL fixed and bind every changing data value. Explain that this prevents values from altering SQL structure. Mention statement reuse as a possible secondary efficiency benefit, not a guaranteed speedup. Finish with the main limitation: identifiers and SQL syntax cannot be bound and must come from trusted allowlist mappings.

Interviewer may ask next
Can a prepared-statement placeholder represent a table name, column name, sort direction, or a complete list for an IN clause?

No. A placeholder represents one complete data value. It cannot represent an identifier, keyword, operator, clause, or multiple values. Dynamic identifiers and sort directions must be selected through a hard-coded allowlist. An IN clause requires one placeholder for every list element, followed by binding each value separately.

Do prepared statements always improve performance, and should PDO emulation always be disabled?

No. Reusing a prepared statement may reduce repeated parsing, planning, or metadata work, but the result depends on the driver, database, query, connection lifetime, and number of executions. Setting PDO::ATTR_EMULATE_PREPARES to false requests native preparation where supported, but PDO may fall back to emulation for a query the driver cannot prepare natively. The application should test its actual production driver and use prepared statements primarily for safe, correct value handling.

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.