PHP
29 questions | entry to principal | answers written to be said out loud, not read
entry
Explain == versus === and why type juggling bites people.
=== compares both value and type with no conversion, and that is what I reach for by default. == first coerces one side to match the other's type before comparing, which is where classic surprises like 0 == 'abc' being true in old PHP versions came from, though PHP 8 fixed several of the worst string-to-number comparison cases. Type juggling in general means PHP will silently convert types in arithmetic, comparisons, and string contexts, and I treat unexpected juggling as a code smell.
What are scalar types and union types used for?
Scalar type declarations let me put int, float, string, and bool right on function parameters and return types, so PHP validates or coerces the value instead of me checking it by hand. Union types, added in PHP 8, let a parameter or return type accept more than one type, written as int|string for example, which is honest when a function genuinely handles more than one shape of input. I prefer a narrow union or a single type over no type at all, because the type declaration is documentation the interpreter actually enforces.
When do you reach for match instead of switch?
match uses strict comparison, has no fall-through, requires every branch to be exhaustive or have a default, and is an expression that returns a value directly. switch uses loose comparison, falls through unless I remember a break on every case, and is a statement, not an expression. I default to match for anything that maps an input to a value, and I only reach for switch when I genuinely need multiple cases to share one block of logic.
What does constructor property promotion save you from writing?
Before PHP 8, giving a class a typed property meant declaring the property, adding a matching constructor parameter, and then writing $this->name = $name for every single one. Constructor promotion lets me put the visibility modifier directly on the constructor parameter, and PHP declares the property and assigns it for me in one line. I use it by default now for simple data-holding classes and DTOs, it removes boilerplate without hiding any real behavior.
What are named arguments and when do they actually help?
Named arguments let me pass a value by matching the parameter's name instead of its position, so I write createUser(email: $email, isAdmin: true) instead of counting positions. They really pay off with functions that have several optional parameters, because I can skip the ones I do not need instead of passing default placeholders just to reach the one I care about. They also make a call site self-documenting, anyone reading it sees exactly which value maps to which parameter without opening the function signature.
What do readonly properties actually enforce?
A readonly property can be set exactly once, typically inside the constructor, and any attempt to modify it afterward throws an Error, even from inside the class itself. That gives me real immutability for value objects and DTOs without hand-writing a bunch of get-only accessors and defensive checks. I use readonly constantly now for things like money amounts, ids, and configuration values that should never change after construction.
junior
What do PHP-FPM and opcache actually do for you?
PHP-FPM is the process manager that keeps a pool of worker processes warm so Nginx or Apache does not have to spawn a new PHP process per request, and it lets me tune how many workers exist and how they scale. Opcache caches the compiled bytecode of my PHP files in shared memory so PHP does not have to re-parse and recompile the same source on every single request. Together they are why PHP in production feels nothing like running php script.php from the command line, and turning opcache off in production is a real performance mistake.
What does declare(strict_types=1) actually change?
Without it, PHP will coerce scalar arguments and return values to match a function's declared types, so passing the string '5' to a function expecting int just silently works. With strict_types declared at the top of a file, that coercion is disabled for scalar type declarations in that file, and a mismatched type throws a TypeError instead of being converted. I turn it on in new code because I want type mismatches caught immediately instead of silently converted into a bug three layers deeper.
How do nullable types and the null-safe operator work together?
A nullable type is written with a leading question mark, like ?User, meaning the value can be a User or null, and it forces me to be explicit that null is a legitimate value rather than an oversight. The null-safe operator, arrow question mark, lets me chain calls on a possibly-null value, so $user?->address?->city returns null the moment any link in the chain is null instead of throwing. I use it to replace long nested isset checks, but it can also silently swallow a null I actually wanted to know about.
How do closures capture variables, and what is first-class callable syntax?
A closure only sees the outer variables I explicitly pull in with use, and by default it captures them by value at the time the closure is created, so later changes to the original variable are not reflected inside the closure unless I use a reference with use (&$var). PHP 8.1 added first-class callable syntax, so instead of writing 'strlen' or [$this, 'method'] as a string or array callable, I can write strlen(...) or $this->method(...) and get a real Closure object that tools and static analysis understand. I reach for that syntax now whenever I am passing an existing function or method as a callback.
What are PHP attributes and where do you actually use them?
Attributes are structured metadata attached to classes, methods, properties, or parameters using the #[...] syntax, and they replace the old convention of putting machine-readable annotations inside a docblock comment. Frameworks read them through reflection at runtime to wire up things like routes, validation rules, or dependency injection without me writing separate configuration files. I like them because the metadata lives right next to the code it describes and the compiler actually parses the syntax, instead of a comment that tooling has to regex out.
What problem do PHP enums solve that constants didn't?
Before enums, I represented a fixed set of options with class constants, but those were just plain strings or ints, so nothing stopped a typo or a completely wrong value from being passed around. A PHP enum is a real type, so a parameter typed as Status only accepts an actual Status case, and I get autocomplete and static analysis instead of hoping a string matches. Backed enums also give each case an underlying scalar value for things like storing it in a database column, while pure enums are just for in-memory identity.
What is the difference between self:: and static::?
self:: always resolves to the class where the code is physically written, regardless of which subclass actually called it. static:: uses late static binding, so it resolves to whichever class was actually called at runtime, which matters a lot for patterns like a base class's static factory method that should return an instance of the calling subclass. I default to static:: inside anything meant to be extended, and I only reach for self:: when I deliberately want to lock the reference to the defining class.
How does Composer autoloading work with PSR-4 and PSR-12?
PSR-4 is the standard that maps a namespace prefix to a directory on disk, so I configure it once in composer.json and Composer generates an autoloader that can find any class file by its fully qualified name without me writing a single require statement. PSR-12 is a separate standard for code style, things like brace placement, spacing, and import ordering, and I enforce it with a linter rather than by hand. Together, PSR-4 is what makes third-party packages just work after composer install, and PSR-12 is what keeps a codebase readable across different engineers and teams.
How do prepared statements protect you from SQL injection?
PDO with a prepared statement sends the SQL query and the user-supplied values to the database as two separate things, the query with placeholders first, then the bound values afterward, so the database never treats user input as part of the SQL syntax. That is completely different from building a query with string concatenation, where a value like ' OR 1=1 can change the meaning of the query itself. I always use bound parameters for anything coming from a user, and I never trust an escaping function alone to be the only defense.
How do you prevent XSS when rendering PHP output?
I escape data at the point I output it into HTML, using htmlspecialchars with ENT_QUOTES and the right character set, so any user-controlled string is rendered as text instead of being interpreted as markup or a script tag. The context matters, escaping for an HTML attribute, a URL, or inline JavaScript are different rules, and a templating engine that auto-escapes by default removes a whole class of mistakes compared to hand-rolling every echo. I treat every piece of user input as untrusted the moment it reaches a view, regardless of what validation happened earlier in the request.
How should passwords be stored, and what does password_hash give you?
I never store a password in plain text or with a fast general-purpose hash like plain sha256, because those are cheap to brute-force at scale once a database leaks. password_hash uses a slow, salted algorithm, bcrypt by default or argon2id if I ask for it, and it automatically generates and stores a unique salt inside the resulting hash string, so I never manage salts by hand. On login I use password_verify to compare the plaintext against the stored hash, and I check password_needs_rehash so I can transparently upgrade anyone still on an older cost setting.
senior
Walk me through what happens when a request hits a PHP application.
PHP is shared-nothing, so every request starts with a fresh interpreter state, there is no long-lived process holding application memory between requests like you would get with Node. A web server like Nginx hands the request to PHP-FPM, which picks a worker process, PHP bootstraps the application, runs the script, and then tears everything down. That means I cannot rely on in-memory globals surviving between requests, anything that needs to persist goes in a database, cache, or session store.
How do PHP arrays behave with copying and references, and when would you reach for a collection object instead?
PHP arrays are copy-on-write value types, so assigning an array to a new variable or passing it to a function gives that code its own copy the moment it writes to it, which is different from how objects behave. A reference, written with an ampersand like function addItem(array &$items), lets a function mutate the caller's actual array instead of a copy, but I use references sparingly because they make data flow harder to follow. For richer behavior like fluent chaining, type safety for the values, or reusable operations like a typed collection of DTOs, I reach for an actual collection object instead of a plain array.
How do you decide between an interface and an abstract class?
An interface defines a contract with no implementation and no state, and a class can implement several of them, so I use interfaces when I care about what a type can do, not how. An abstract class can hold shared state, default method implementations, and a constructor, but PHP only allows single inheritance, so I use it when several related classes genuinely share behavior, not just a shape. If I am unsure, I start with an interface, because it is much easier to add an abstract base class later than to remove one that other code already depends on.
What are traits good for, and where do they cause problems?
A trait lets me share a concrete method implementation across classes that are not related through inheritance, which solves a real gap since PHP only allows extending one class. The problem is traits inject their methods directly into the class, so they carry no separate identity, an object using a trait cannot be type-hinted or instanceof-checked against that trait, and composing several traits with conflicting method names gets messy fast. I use traits sparingly, mostly for small, self-contained, cross-cutting behavior like a timestamps mixin, and I reach for composition or an interface plus a service when the behavior is bigger than that.
How do you think about exceptions and error handling in PHP?
I throw exceptions for exceptional, unexpected conditions, like a failed database connection or a violated invariant, not for normal control flow like a not-found lookup that a caller should just check for. PHP unifies errors and exceptions under the Throwable interface, so a try/catch can catch both an Exception and an Error like a TypeError if I genuinely need to. I catch as narrowly as I can, I always let unexpected errors bubble up to a top-level handler that logs them, and I never silently swallow an exception with an empty catch block.
How does a CSRF token actually stop an attack?
A CSRF token is a random, unpredictable value the server generates and embeds in a form, tied to the user's session, and the server rejects the request unless the submitted token matches. A malicious site can trick a logged-in user's browser into submitting a request to my app, and the browser will happily attach the session cookie automatically, but the attacker has no way to know or guess the token value. I always pair CSRF protection with SameSite cookies, and I make sure state-changing actions never happen on a plain GET request in the first place.
What do you check before trusting an uploaded file?
I never trust the original filename or the client-supplied MIME type, both are just strings the browser sent and an attacker fully controls. I validate the actual file content, enforce a strict size limit and an allowlist of extensions, generate a new random filename on my side, and store uploads outside the public web root or in object storage rather than a directory PHP itself would execute. For anything like profile images I also re-process the file, for example re-encoding an image, which strips embedded scripts that a crafted file might carry.
How do generators help with memory, and how does that relate to caching strategy?
A generator uses yield to produce one value at a time instead of building the entire result set in memory, so I can iterate over millions of database rows or a huge file with roughly constant memory instead of exhausting the process. That is a different lever than caching, caching avoids redoing expensive work a second time by storing the result, generators avoid holding a huge working set in memory at all. In practice I use generators for large, one-pass iteration, and I use a cache layer like Redis for data that is expensive to compute but read repeatedly.
staff
How do you approach dependency injection in a PHP application?
I have a class declare what it needs as constructor parameters, typically typed against interfaces, rather than instantiating its own dependencies or reaching for a global. A container resolves the full object graph at runtime, so swapping a real dependency for a test double or a different implementation is just a container binding change, not a code change. This is really about decoupling and testability, the container is just the mechanical piece that wires it together, the design discipline of not letting a class construct its own collaborators is the actual point.
What do you keep in mind about PHP sessions in production?
By default PHP stores session data as files on local disk, which quietly breaks the moment I run more than one application server behind a load balancer, because a user's next request can land on a server that never saw their session file. I move session storage to something shared, like Redis or the database, so any server can read the same session. I also make sure the session cookie is set with HttpOnly, Secure, and SameSite, and I regenerate the session id after login to prevent session fixation.
How do you approach testing a PHP application with PHPUnit?
I write unit tests around isolated logic like a pricing calculation or a validator, mocking out collaborators through the interfaces they were injected with, and I write integration tests around things like a repository that actually talks to a real or in-memory database. I care more about testing behavior at a class or module boundary than mocking every single internal call, over-mocking makes tests brittle without actually proving the code works. I run the suite in CI on every change and treat a flaky or skipped test as something to fix immediately, not something to ignore.
principal
When is PHP still the right choice for a new project?
PHP is genuinely strong for web applications and APIs with a request-response shape, the shared-nothing model means one request's bug or memory leak cannot take down another request, and deployment is simple, you push files and PHP-FPM handles concurrency for you. The ecosystem around Composer, mature frameworks, and hosting is enormous, so I get a lot of production-ready tooling without building it myself. I would reach for something else for long-running processes, real-time bidirectional connections, or heavy CPU-bound work that benefits from persistent in-memory state, those fit a different execution model better.
Fast recall
=== = strict, no type coercion | == = loose, coerces types | strict_types = no implicit scalar coercion | readonly = settable once, in constructor | enum = fixed set of cases | match = strict comparison, returns value | traits = reusable methods, no shared state | PSR-4 = namespace maps to file path | opcache = caches compiled bytecode | PDO = prepared statements, parameter binding | password_hash = bcrypt or argon2 hashing | generator = yield, lazy iteration | shared-nothing = fresh memory per request