↓ Skip to main content

PHP test tasks and interview questions, from both sides of the table

·9 mins
Hennadii Alforov
Author
Hennadii Alforov
Senior Backend Engineer with 15+ years in IT. I build and scale backend systems for high-traffic platforms - billing, payments, hosting and domain services.

A test task shouldn't take more than 5 hours. I wrote that in 2015, in the Russian version of this post, together with a less popular opinion: test tasks only make sense for junior roles. Back then I'd been to more than 20 interviews in three years and kept every task I got.

Since then I've been on both sides of the table many times. I interview PHP developers, I help a company hire them and review their test tasks, and recently I've been a candidate again myself. This post has the old tasks, the questions I ask, and what I got asked in 2025 and 2026.

The test tasks I got, 2013-2017
#

The old post was a collection of tasks with my solutions. The demos lived on the old version of this site and are gone. The code was PHP 5, and today I'd write all of it differently. The tasks themselves still show what companies asked for.

In 2013 it was a guest book. The most common test task of that year, and it didn't go away for a long time.

In 2014 a well-known Ukrainian bank asked for a web page with a small FAQ editor: questions and answers. Nice people, an interesting task that didn't take long. The salary was low, so we said goodbye.

In 2015 a startup wanted a function that removes duplicate characters from a string, plus a parser for a news site. The salary they mentioned at the start was good, which is why I did the task. After it I understood they didn't have much money. Today the function is one line:

function removeDuplicateChars(string $text): string
{
    return implode('', array_unique(mb_str_split($text)));
}

removeDuplicateChars('programming'); // "progamin"

In 2016 an outsourcing company in Kharkiv sent the biggest one: a URL shortener. PHP 5.6+, OOP, PSR-2 and PSR-4, PDO with MySQL, Composer, Bootstrap, PHPUnit tests and a README with install steps. Users had to be able to pick their own short link and create links that expire. The bonus part was click statistics with geography and user agents. I built it on Yii2, passed the interview and never heard back from them. The code is still on GitHub: yii2-minify-url, archived.

That's the kind of task the 5-hour limit is about. It arrives as a "small" test task, and with tests, custom links, expiry and geo statistics it realistically takes three days.

How I interview PHP developers
#

My interview is one hour:

  • 5 minutes about previous experience
  • 30 minutes of questions about OOP, SOLID, PHP, MySQL and so on
  • 15 minutes of coding: the code works, but it needs refactoring
  • 5 minutes for the candidate's questions

The questions and their order change with the candidate's level and with how the previous answers went. These are the ones I come back to most.

PHP itself
#

  • What happens when a class uses two traits with a method of the same name? PHP stops with a fatal error until you choose one with insteadof. The other one can stay under a new name with as.
  • Can an interface have a private method? No. PHP answers "Access type for interface method must be public".
  • Why do we need interfaces at all, and how is an abstract class different?
  • What's the difference between self and static? Late static binding: static points to the class that was actually called.
  • How does yield work?
  • What does it cost to pass a 100 MB string into a function? Almost nothing, thanks to copy-on-write. I measured it: the peak memory stays at 100 MB while the function only reads the string and goes to 200 MB on the first write.
  • Closures and anonymous functions: what's the difference, and what can each of them see from the outer scope?

Patterns and testing
#

  • Name a design pattern that isn't Singleton or Factory. It's a plus if the candidate knows the groups the patterns fall into and can explain something like Observer.
  • Which patterns does Doctrine use? Unit of Work, Data Mapper, lazy loading through proxies.
  • For a senior: GRASP. A senior should at least have heard of it.
  • What is a mock for, and how is a unit test different from a functional one?

Tools, HTTP and databases
#

  • composer install or composer update, and why is composer.lock in the repository? How does Composer's autoloader find a class?
  • git merge or git rebase? What does cherry-pick do? How do you undo the last commit if it isn't pushed yet?
  • HTTP/1.1 and HTTP/2: what changed? Which requests are cached by default and which aren't? Which status code does a create endpoint return?
  • InnoDB or MyISAM? Which transaction isolation levels are there? What's the difference between TRUNCATE TABLE and DELETE FROM?
  • How do CSRF, SQL injection and XSS work, and how do you protect against each?

Symfony and Laravel
#

  • Walk me through a Symfony request. There's no middleware there, everything runs on kernel events.
  • Event listeners or subscribers?
  • What replaced ParamConverter? Today it's attributes like #[MapRequestPayload].
  • And the Laravel side: the request goes through a middleware pipeline. What are facades, and what do they cost you? What do you think about raw DB:: queries? Cursor or offset pagination? Scoped or singleton bindings?

The other side: what I got asked in 2025-2026
#

Over the last year I was on the candidate side a few times. Every company had its own format: live coding, a take-home task, or a long technical conversation without any code.

Live coding taught me the most. I started from the database schema, spent most of the time on the structure, and the small things slipped: a data transformation in the controller instead of a service, 200 instead of 201 for a create endpoint. It's easier to explain what code does and why than to write it while someone watches.

The technical conversation was the most interesting one. It opened with a warm-up puzzle: in a 40-storey building, which lift button gets pressed most? The ground floor. Then came the questions:

  • idempotency keys, and the inbox/outbox pattern with the write inside the main transaction
  • a queue that keeps growing, and what backpressure means
  • composite indexes, and whether it matters if WHERE lists user_id before status (it doesn't)
  • retries for GET and POST, and returning the cached response for a repeated idempotency key
  • adding a column to a table with 500 million rows
  • declare(strict_types=1)
  • big numbers like ETH amounts with 18 decimals, finding a memory leak, deploy strategies, secret rotation
  • hashing vs encryption vs encoding, dirty reads, PHP runtimes like Swoole, RoadRunner and FrankenPHP
  • the five methods of Iterator, when finally doesn't run, DTO vs value object

Half of what I'd prepared never came up. Short answers to the ones worth writing down:

  • A dirty read is when a transaction sees data that another transaction wrote but hasn't committed yet. If that one rolls back, what you read never existed.
  • Encoding (base64) is just a format and always reversible. Encryption (AES, RSA) is reversible with a key. Hashing (SHA-256, bcrypt) isn't reversible at all. A signature like HMAC is a hash plus a key, and that's how payment webhooks are checked. A password on the endpoint alone isn't enough, as I wrote in the Symfony HTTP Basic guide.
  • finally doesn't run after exit() or die(), after a fatal error like running out of memory, after a segfault or kill -9. I checked the first one on PHP 8.5.
  • A DTO carries data between layers. A value object is part of the domain and won't let itself be created in an invalid state.
  • 1 ETH is 10^18 wei, and a 64-bit integer holds about 9.2 × 10^18, so about 9 ETH. Store such amounts as DECIMAL or a string and do the math with BCMath, never with floats.
  • Iterator has current(), key(), next(), rewind() and valid(). There's no prev().

One non-technical question I liked: a teammate writes poor code and keeps building the wrong thing, what do you do? There's no right answer, they want the reasoning. Mine is layers. Linters and static analysis first, so style never becomes a review argument. Then an AI review before mine, so the obvious problems get fixed before anyone sees them. Then my own review, about the approach and not the syntax. And if it keeps repeating, it's not a review problem anymore: it's a one-on-one talk about what the person is missing. I wrote more about where AI helps and where it doesn't in the HREvio post.

How I prepare now
#

The best preparation tool I've found lately is Gemini Notebook (formerly NotebookLM). You make a notebook for one goal, say a senior PHP interview, and add sources: official docs, a few good articles, a conference talk from YouTube, your own notes. Posts from this blog work too. Then you ask questions, and it answers from those sources and shows where each answer comes from.

The part I use most is the studio next to the chat. From the same sources it builds a quiz, flashcards, a mind map or an audio overview you can listen to away from the screen. I take a quiz on one topic, read up on whatever I got wrong, and take a new one. It can still be wrong, like any model, so the sources have the final word.

What I'd tell a candidate now
#

  • If you get to choose, take the take-home task.
  • Ask about the format at the HR stage, and ask whether AI tools are allowed. With AI allowed the task is normal work. Without it they're testing the bare language.
  • In live coding, get a working solution first and refactor later. Say your compromises out loud: a compromise you announced isn't a mistake. Have something working by the halfway mark.
  • Small things look like carelessness. 201 for a created resource, not 200.
  • Using a hint well is not a failure.

SOLID, the cheat sheet from 2016
#

I wrote this for myself in 2016 and it still works as a quick check:

  • Single responsibility: every class has one job. Count the reasons it might change. More than one means it should be split.
  • Open/closed: open for extension, closed for modification. Treat the class as a black box and see whether you can still change its behaviour.
  • Liskov substitution: a subclass can replace its parent without breaking the program. Check that you didn't strengthen the preconditions or weaken the postconditions.
  • Interface segregation: many specific interfaces beat one general one. If an interface has lots of methods that do unrelated things, split it.
  • Dependency inversion: depend on abstractions, not on details. If a class creates the objects it depends on, make it depend on an interface instead.