If you are preparing for a senior PHP interview, this guide walks you through the topics that come up again and again — not the basics, but the tricky parts that separate strong candidates from average ones. Each section explains the concept, shows a small code example, and points out the traps where people often slip.

The article is written in plain English so it stays easy to read even if English is not your first language. Code examples do the heavy lifting; the prose around them just adds context.

A developer's workspace at night, dual monitors showing PHP code with floating holographic system-architecture diagrams above the desk.
A senior PHP role is rarely about a single class — it is about how the parts of a system fit together.

Article Plan

  1. PHP Core & OOP Deep Dive — types, interfaces, abstract classes, traits, late static binding, polymorphism quirks
  2. Type System Subtleties — covariance vs contravariance, uninitialized properties, equality vs identity
  3. Modern PHP Features — readonly, enums, generators, magic methods, anonymous functions
  4. SOLID Principles with PHP Examples
  5. Design Patterns You Should Know
  6. Testing in PHP — unit, integration, mocks vs stubs, FIRST principles
  7. Security Essentials — XSS, CSRF, SQL injection, hashing vs encryption
  8. HTTP, REST, and APIs — methods, idempotency, status codes
  9. Performance & Tooling — OPcache, Composer, Reflection, SPL
  10. PHP 8.x New Features Cheat Sheet
  11. Top Interview Questions to Practice

1. PHP Core & OOP Deep Dive

1.1 Data Types

PHP has 8 simple data types:

  • 4 scalar types: boolean, integer, float (also called double), string
  • 2 compound types: array, object
  • 2 special types: resource, NULL
  • Pseudo-types: mixed, number, callable, void, never, iterable

1.2 Interface vs Abstract Class vs Trait

These three look similar at first glance, but they solve different problems.

Interface is a pure contract. It only contains method signatures (and constants, since PHP 8.1). A class can implement many interfaces.

Abstract class is a partially built class. It can have state (properties), real methods, and a constructor. A class can extend only one abstract class.

Trait is for horizontal code reuse. It is not a type — you cannot type-hint against a trait. Think of it as "copy-paste at the compiler level."

Three glass shapes side by side: a wireframe icosahedron, a solid faceted icosahedron, and a cluster of glass cubes — visual metaphors for an interface, an abstract class, and a trait.
Same building block at three different levels of structure: contract, partial implementation, mix-in.

PHP
interface PaymentGateway {
    public function charge(Money $amount): TransactionId;
}

abstract class AbstractPaymentGateway implements PaymentGateway {
    public function __construct(protected LoggerInterface $logger) {}

    final public function charge(Money $amount): TransactionId {
        $this->logger->info('Charging', ['amount' => $amount]);
        return $this->doCharge($amount); // Template Method pattern
    }

    abstract protected function doCharge(Money $amount): TransactionId;
}

trait Timestampable {
    private ?\DateTimeImmutable $createdAt = null;
    public function getCreatedAt(): ?\DateTimeImmutable { return $this->createdAt; }
}

A quick "when to reach for which":

You need Pick Example in this article
Pure contract, no logic Interface PaymentGateway
Contract plus shared logic Abstract class with Template Method AbstractPaymentGateway
Small technical mix-in (timestamps, etc.) Trait Timestampable

1.3 Late Static Binding — self vs static

self:: refers to the class where the code is written. static:: refers to the class at runtime. This matters a lot in factory methods.

PHP
class Model {
    public static function create(): static {
        return new static();
    }
}

class User extends Model {}

$user = User::create(); // returns a User, NOT a Model
// If you replace `static` with `self`, it would always return a Model.

1.4 Private Methods Are Not Overridden

This one catches many developers off guard.

PHP
class Base {
    public function test(): string { return $this->doWork(); }
    private function doWork(): string { return 'Base'; }
}

class Child extends Base {
    private function doWork(): string { return 'Child'; }
}

echo (new Child())->test(); // outputs "Base"!

Private methods do not take part in polymorphism.


2. Type System Subtleties

2.1 Covariance and Contravariance

These two words sound scary, but the idea is simple.

  • Covariance (return types): a child method can return a narrower type than the parent. This is allowed.
  • Contravariance (parameter types): a child method must accept the same type or a wider one. Narrowing parameters is forbidden because it breaks the Liskov Substitution Principle.
PHP
class AnimalFeeder {
    public function feed(Animal $a): Animal { return $a; }
}

class DogFeeder extends AnimalFeeder {
    public function feed(Dog $d): Dog { return $d; } // FATAL ERROR!
    // Narrowing the parameter from Animal to Dog is not allowed.
}

The correct version:

PHP
class DogFeeder extends AnimalFeeder {
    public function feed(Animal $a): Dog { return $a; } // OK
}

2.2 Uninitialized Typed Properties

A typed property without a default value is in a special "uninitialized" state — it is not the same as null.

PHP
class User {
    public string $name; // uninitialized
}

$u = new User();
isset($u->name);  // false
echo $u->name;    // Error: must not be accessed before initialization

2.3 Object Equality vs Identity

PHP
$id1 = new UserId('abc');
$id2 = new UserId('abc');

$id1 == $id2;  // true (compares properties)
$id1 === $id2; // false (different instances)

in_array($id2, [$id1]);        // true (loose comparison)
in_array($id2, [$id1], true);  // false (strict comparison)

For Value Objects, write an explicit equals() method instead of relying on ==.


3. Modern PHP Features

3.1 Readonly — A Slightly Misleading Word

Readonly does not mean "deeply immutable." It only protects the property reference, not the object inside.

PHP
final class Order {
    public function __construct(public readonly array $items) {}
}

$order = new Order([new Item('A'), new Item('B')]);
$order->items[] = new Item('C');     // ERROR — cannot reassign
$order->items[0]->name = 'Modified'; // OK! Mutating an inner object is allowed

In PHP 8.2 you can mark a whole class as readonly, which makes every property automatically readonly.

3.2 Enums (PHP 8.1)

Enum cases are singletons. They have no state — only constants and methods.

PHP
enum OrderStatus: string {
    case Pending = 'pending';
    case Paid = 'paid';
    case Cancelled = 'cancelled';

    public function canTransitionTo(self $new): bool {
        return match ([$this, $new]) {
            [self::Pending, self::Paid],
            [self::Pending, self::Cancelled] => true,
            default => false,
        };
    }
}

$s1 = OrderStatus::Paid;
$s2 = OrderStatus::from('paid');
var_dump($s1 === $s2); // true — same singleton
clone $s1;             // ERROR — enums cannot be cloned

A backed enum has a scalar value (string or int). A pure enum has none.

3.3 Generators Are One-Shot

A generator can only be traversed once.

PHP
function readLines(string $file): \Generator {
    $h = fopen($file, 'r');
    while (($line = fgets($h)) !== false) {
        yield trim($line);
    }
    fclose($h);
}

$lines = readLines('big.txt');
iterator_count($lines);              // exhausts the generator
foreach ($lines as $line) {}         // ERROR: closed generator

If you need to walk through the data again, either create a new generator or convert it with iterator_to_array().

3.4 Magic Methods

The most common ones you should be ready to discuss:

  • __construct() — initialization, dependency injection through the constructor
  • __destruct() — called when references are released; order is not guaranteed
  • __call() / __callStatic() — triggered when an inaccessible method is called
  • __get() / __set() — Doctrine proxies use these for lazy loading
  • __invoke() — lets you treat an object as a function (used in Symfony invokable controllers and Messenger handlers)
  • __toString() — converts the object to a string; since PHP 8.0, this implies Stringable
  • __clone() — runs after the object is cloned
  • __sleep() / __wakeup() — used during serialize/unserialize
  • __debugInfo() — customizes the output of var_dump()

3.5 Anonymous Functions

In PHP, an anonymous function is an object of the Closure class. That makes it slightly heavier than a regular object.

PHP
// Standard closure
$fn = function(int $x): int { return $x * 2; };

// Arrow function (short closure, since PHP 7.4)
$fn = fn(int $x): int => $x * 2;

// First-class callable syntax (PHP 8.1)
$fn = strlen(...);

3.6 Trait Conflict Resolution

When two traits provide a method with the same name, PHP forces you to pick a winner.

PHP
class Order {
    use Loggable, Auditable {
        Loggable::log insteadof Auditable;
        Auditable::log as audit;
    }
}

$order->log('hello');   // calls Loggable::log
$order->audit('hello'); // calls Auditable::log under a new name

The precedence order, from highest to lowest: class method > trait method > parent class method.

3.7 Variadic Plus Named Arguments — A Subtle Trap

PHP
function format(string $template, string ...$args): string {
    return sprintf($template, ...$args);
}

format('%s %s', ...$parts);                // OK
format(template: '%s %s', args: $parts);   // ERROR: Unknown named parameter $args

4. SOLID Principles with PHP Examples

S — Single Responsibility

A class should do one thing. A UserRegistrationService should not validate input, hash passwords, send emails, and write logs all at once.

O — Open/Closed

Code should be open for extension, closed for modification. Adding a new payment type should mean a new class implementing PaymentGateway, not editing a long if/switch block.

L — Liskov Substitution

Subclasses should not break the behavior of their parents. If SquareRepository throws an exception where the parent never did, that is a violation.

I — Interface Segregation

Many small interfaces are better than one giant one. Symfony itself splits user-related contracts into UserInterface, PasswordAuthenticatedUserInterface, EquatableInterface, and so on.

D — Dependency Inversion

Depend on abstractions, not concrete classes. A controller should depend on UserRepositoryInterface, not on DoctrineUserRepository.

PHP src/Domain/Repository/UserRepository.php
// Domain layer — defines the contract
interface UserRepository {
    public function findById(UserId $id): ?User;
    public function save(User $user): void;
}

// Infrastructure layer — provides the implementation
final class DoctrineUserRepository implements UserRepository {
    public function __construct(private EntityManagerInterface $em) {}
    // ...
}

5. Design Patterns You Should Know

Creational

  • Factory Method — a shared interface for creating objects, where subclasses decide the actual type.
  • Abstract Factory — creates families of related objects without binding to concrete classes.
  • Builder — builds complex objects step by step.
  • Prototype — copies objects without exposing how they are made.
  • Singleton — a single shared instance with global access.

Structural

  • Adapter — bridges incompatible interfaces.
  • Bridge — separates abstraction from implementation.
  • Composite — treats a tree of objects as a single one.
  • Decorator — adds behavior at runtime by wrapping an object.
  • Facade — gives a simple front to a complex system.
  • Flyweight — shares common state between many objects.
  • Proxy — a stand-in object that controls access.

Behavioral

  • Chain of Responsibility — passes a request along a chain of handlers.
  • Command — wraps a request as an object.
  • Iterator — walks through items one by one.
  • Mediator — reduces coupling by routing communication through one object.
  • Memento — saves and restores state.
  • Observer — pub/sub notifications (the basis of EventDispatcher).
  • State — changes behavior based on internal state.
  • Strategy — interchangeable algorithms behind one interface.
  • Template Method — a fixed algorithm with steps subclasses can override.
  • Visitor — adds new operations without changing the classes they operate on.

Active Record vs Data Mapper

A common interview question.

  • Active Record (Laravel Eloquent): the object is a row and knows how to save itself.
  • Data Mapper (Doctrine): the object knows nothing about the database; persistence is handled separately.

Active Record is convenient for small projects. Data Mapper scales better for complex domains because the model stays clean.


6. Testing in PHP

6.1 Levels of Testing

  • Unit tests — test one piece in isolation, usually with mocks or stubs.
  • Integration tests — test a group of pieces working together; in Symfony this often uses KernelTestCase and a real database.
  • Functional / system tests — test the system through its public interface; in Symfony, WebTestCase with real HTTP requests.

6.2 Mocks vs Stubs

The two terms are often confused.

  • Stubs feed canned data into the system so the test can run.
  • Mocks verify behavior — they check that certain methods were called with certain arguments.

6.3 FIRST Principles

Good tests follow these rules:

  • Fast — tests run quickly so developers run them often.
  • Independent — one test does not depend on another.
  • Repeatable — the same result every time, on any machine.
  • Self-Validating — pass or fail, no need to read logs.
  • Timely — written along with (or before) the code, not weeks later.

6.4 Sample Test Structure

PHP tests/Unit/RegisterUserHandlerTest.php
final class RegisterUserHandlerTest extends TestCase {
    public function testRegistersNewUser(): void {
        $repo = $this->createMock(UserRepository::class);
        $repo->expects($this->once())->method('save');

        $handler = new RegisterUserHandler($repo);
        $handler(new RegisterUserCommand('a@b.com', 'pwd'));
    }
}

6.5 The Clock Trick

Testing code that uses new \DateTimeImmutable() is painful — you cannot mock the current time. The fix is to inject a clock.

PHP
interface Clock {
    public function now(): \DateTimeImmutable;
}

final class SystemClock implements Clock { /* real time */ }
final class FrozenClock implements Clock { /* fixed time for tests */ }

This is standardized as PSR-20.


7. Security Essentials

7.1 XSS (Cross-Site Scripting)

Attackers inject JavaScript into your page. Defense:

  • Escape output with htmlentities() or htmlspecialchars().
  • Set a strong Content Security Policy.
  • Use HttpOnly cookies so JavaScript cannot read session tokens.

7.2 CSRF (Cross-Site Request Forgery)

An attacker tricks an authenticated user into performing actions they did not intend. Defense: use CSRF tokens for state-changing requests.

7.3 SQL Injection

User input ends up inside a SQL query. Defense: always use prepared statements with parameter binding. Doctrine and PDO do this for you when used correctly.

7.4 Encoding vs Encryption vs Hashing

Three different things people often mix up:

  • Encoding is for transport (Base64, UTF-8). Anyone can reverse it.
  • Encryption is for confidentiality (AES, RSA). It is two-way; you can decrypt with the key.
  • Hashing is for integrity and password storage (bcrypt, argon2). It is one-way — you cannot reverse it.

8. HTTP, REST, and APIs

8.1 HTTP Methods and Idempotency

A method is idempotent if calling it many times produces the same result as calling it once.

Method Purpose Idempotent Has body in response
GET Fetch data Yes Yes
POST Create No Yes
PUT Full replacement Yes Sometimes
PATCH Partial update No* Sometimes
DELETE Remove Yes Sometimes
HEAD Like GET, no body Yes No
OPTIONS Describe support Yes Yes

* PATCH can be designed to be idempotent, but the spec does not guarantee it.

8.2 Status Codes

Range Meaning Examples
1xx Informational 100 Continue
2xx Success 200 OK, 201 Created, 204 No Content
3xx Redirection 301 Moved Permanently, 302 Found, 304 Not Modified, 308 Permanent Redirect
4xx Client error 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 409 Conflict, 422 Unprocessable Entity
5xx Server error 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable

8.3 REST Best Practices

  • Use nouns for resources (/users, /posts), not verbs.
  • Use plurals for collections: /users, /users/{id}.
  • Version the API in the URL: /api/v1/....
  • Filter through query parameters: ?status=active&sort=name.

8.4 SOAP vs REST

SOAP is a strict messaging format (XML envelopes, WS-* standards). REST is a flexible architectural style that uses HTTP as transport with JSON or XML payloads.

8.5 CORS

Browsers send a preflight OPTIONS request before a cross-origin call. The server replies with headers like Access-Control-Allow-Origin and Access-Control-Allow-Methods to grant or deny access.

8.6 HTTP Caching

Use Cache-Control, ETag, and Last-Modified headers. They let clients and proxies skip work when nothing has changed.


9. Performance & Tooling

9.1 OPcache

OPcache stores compiled PHP bytecode in memory so the engine does not parse files on every request. With preloading (PHP 7.4+), you can load core files at startup and skip even more work. Both should be enabled in production.

9.2 Composer and Autoloading

Composer is the PHP package manager. It uses PSR-4 autoloading: a namespace prefix maps to a directory, and spl_autoload_register() does the loading work behind the scenes.

JSON composer.json
{
    "autoload": {
        "psr-4": { "App\\": "src/" }
    }
}

9.3 Sessions vs Cookies

  • Cookies live on the client, sent with every request.
  • Sessions live on the server; the client only holds a session ID in a cookie.

Sessions are safer for sensitive data because the browser only sees an opaque ID.

9.4 Reflection API

Reflection lets you inspect classes, methods, properties, and parameters at runtime. Symfony's DI container uses Reflection to figure out what to inject — that is how autowiring works.

9.5 SPL (Standard PHP Library)

A built-in toolbox of useful pieces:

  • Data structures: SplStack, SplQueue, SplHeap, SplObjectStorage
  • Iterators
  • Standard exceptions (InvalidArgumentException, RuntimeException, etc.)
  • spl_autoload_register()

9.6 RoadRunner

A modern PHP application server written in Go. It keeps long-running PHP workers alive, so the framework does not bootstrap on every request. Speeds up Symfony and Laravel apps significantly.


10. PHP 8.x New Features Cheat Sheet

A senior developer should be comfortable with everything in this list:

  • Named arguments — call functions by parameter name.
  • Match expression — a strict, expression-based alternative to switch.
  • Nullsafe operator (?->) — short-circuits chains when something is null.
  • Constructor property promotion — declare and assign properties in the constructor signature.
  • Attributes (#[Route], #[AsMessageHandler]) — native metadata, replacing docblock annotations.
  • Union types (8.0), intersection types (8.1), DNF types (8.2).
  • Enums (8.1).
  • Readonly properties (8.1) and readonly classes (8.2).
  • First-class callable syntax (strlen(...)) — 8.1.
  • Fibers (8.1) — primitives for cooperative multitasking.

11. Top Interview Questions to Practice

A handwritten notebook open on a wooden desk with a fountain pen and a steaming cup of coffee, a laptop in the background showing colorful code.
The questions below are worth working through with a pen and paper before you ever face them across a video call.

If you can answer all of these clearly, with examples, you are in good shape:

  1. What is the difference between an interface, an abstract class, and a trait? When do you use each?
  2. What is the difference between self and static, and what does a factory method return?
  3. Why are private methods not overridden by child classes? Predict the output of a small example.
  4. Explain covariance and contravariance. Why is narrowing a parameter type forbidden?
  5. Does readonly make an object immutable? Can you mutate something inside it?
  6. What does PHP do when two traits define the same method?
  7. Why is a generator a one-shot iterator? How do you walk through it twice?
  8. What is the difference between == and === for objects?
  9. What is an uninitialized typed property, and how is it different from null?
  10. Explain the difference between Active Record and Data Mapper.
  11. When does password_hash() need a salt? (Trick question — it generates one for you.)
  12. What is OPcache, and what does preloading add on top?
  13. How does PSR-4 autoloading work under the hood?
  14. What is the difference between a mock and a stub?
  15. Which HTTP methods are idempotent, and why does it matter?
  16. How do you defend against XSS, CSRF, and SQL injection in PHP?
  17. When would you choose composition over inheritance? Give a real example.
  18. What new features in PHP 8.x have changed the way you write code?

Final Thoughts

Senior PHP interviews are less about memorizing syntax and more about understanding why things work the way they do. The questions above test exactly that — they probe the edges of the language, the boundaries of your abstractions, and the way you reason about real systems.

Read the code examples, run them yourself, change a line, and see what breaks. That habit will teach you more than any cheat sheet.

Good luck with your interview.