Opening: Why fundamentals matter in PHP interviews
When you're interviewing for a PHP role—whether it's your first senior position or you're switching teams—you'll inevitably face questions about the language itself. PHP interview questions on fundamentals test whether you truly understand how the language works under the hood, not just how to string together framework features. These aren't trick questions; they're designed to separate developers who know what they're doing from those who are just copying Stack Overflow snippets.
We've seen countless candidates stumble on questions that sound simple but reveal gaps in their mental model. In this article, we'll walk through the core PHP interview questions about fundamentals that come up repeatedly: loose vs. strict equality, OOP design patterns, autoloading mechanics, and what PHP 8 actually changed. By the end, you'll be equipped to explain these concepts clearly and confidently.
== vs === and type juggling: Know when PHP surprises you
This is often the first question interviewers ask, and for good reason. It reveals whether you understand PHP's type system.
Loose equality (==) compares values after performing type coercion. PHP will attempt to convert types to make the comparison work. Strict equality (===) compares both value and type without any conversion.
// Loose equality — PHP converts types
var_dump(0 == "0"); // true (string "0" becomes int 0)
var_dump(0 == false); // true (false coerces to 0)
var_dump("" == false); // true (empty string coerces to false)
var_dump(null == false); // true (null coerces to false)
var_dump("10" == "1e1"); // true (both become float 10)
// Strict equality — no conversion
var_dump(0 === "0"); // false (int vs string)
var_dump(0 === false); // false (int vs bool)
var_dump("" === false); // false (string vs bool)The real-world impact hits hardest in loops and conditionals. Imagine you're processing user IDs from an API:
$user_id = "0"; // string from JSON
if ($user_id == false) {
// This executes! The string "0" coerces to false
throw new Exception("Invalid user");
}
// Better:
if ($user_id === false) {
throw new Exception("Invalid user");
}Type juggling has rules, but they're complex. PHP's == comparison table contains surprising results that even senior developers forget. The golden rule: always use === unless you have a specific, documented reason not to. In modern codebases, strict comparison is the default mental model.
One more scenario that trips people up: comparing arrays.
$array1 = ["id" => 1, "name" => "Alice"];
$array2 = ["name" => "Alice", "id" => 1];
var_dump($array1 == $array2); // true (same key-value pairs, order doesn't matter)
var_dump($array1 === $array2); // true (also same with ===)
$array3 = [0 => "a", 1 => "b"];
$array4 = ["0" => "a", "1" => "b"];
var_dump($array3 == $array4); // true
var_dump($array3 === $array4); // false (string keys vs int keys)Abstract classes vs interfaces vs traits: Choosing the right abstraction
This question appears in nearly every senior engineer interview because the answer reveals your understanding of OOP design. Many developers use them interchangeably, but they solve different problems.
Abstract classes define common state and behavior for a family of related classes. They can have concrete methods, properties, and constructors. Use them when you want to share implementation.
abstract class PaymentProcessor {
protected $fee = 0.03; // Shared state
protected $currency = "USD";
abstract public function charge(float $amount): bool;
// Concrete method all subclasses inherit
public function calculateTotal(float $amount): float {
return $amount * (1 + $this->fee);
}
}
class StripeProcessor extends PaymentProcessor {
public function charge(float $amount): bool {
// Stripe-specific logic
$total = $this->calculateTotal($amount);
return true;
}
}Interfaces define a contract without implementation. They guarantee that a class has certain methods, but not *how* those methods work. Use them when you want to ensure API compatibility across unrelated classes.
interface PaymentGateway {
public function charge(float $amount): bool;
public function refund(string $transactionId, float $amount): bool;
}
class Stripe implements PaymentGateway {
public function charge(float $amount): bool { /* ... */ }
public function refund(string $transactionId, float $amount): bool { /* ... */ }
}
class PayPal implements PaymentGateway {
public function charge(float $amount): bool { /* ... */ }
public function refund(string $transactionId, float $amount): bool { /* ... */ }
}Traits are horizontal code reuse. They let you share methods across unrelated classes without inheritance. Use them when you have common functionality that doesn't fit into an inheritance hierarchy.
trait Loggable {
public function log(string $message): void {
echo "[" . date("Y-m-d H:i:s") . "] $message\n";
}
}
class User {
use Loggable;
}
class PaymentProcessor {
use Loggable;
}
// Both can call $this->log() without inheriting from a common parentA real-world scenario: you're building an e-commerce system. Payment processors share fees and currency logic (abstract class). But you also want to guarantee that any payment gateway has charge and refund methods (interface). And you want logging everywhere without a logging base class (trait).
abstract class BasePaymentProcessor implements PaymentGateway {
use Loggable;
protected $fee = 0.03;
public function charge(float $amount): bool {
$total = $amount * (1 + $this->fee);
$this->log("Charging $total");
// ...
}
}How autoloading works: The PSR-4 standard and Composer
Many developers use autoloading every day without understanding it. When you require the Composer autoloader and type new UserRepository(), how does PHP actually find that file?
Autoloading is a mechanism where PHP automatically loads a class file when you try to instantiate or reference a class. Instead of requiring files manually, you register a function that maps class names to file paths.
PSR-4 (PHP Standards Recommendation 4) is the modern standard. It maps namespace prefixes to directory paths. Composer implements PSR-4 by default.
// composer.json
{
"autoload": {
"psr-4": {
"App\\": "src/",
"Domain\\": "domain/"
}
}
}This tells Composer: "Any class in the App\ namespace lives in the src/ directory. Any class in Domain\ lives in domain/."
// src/Repositories/UserRepository.php
namespace App\Repositories;
class UserRepository {
// ...
}
// Somewhere else:
use App\Repositories\UserRepository;
new UserRepository(); // PHP calls the autoloader, which finds src/Repositories/UserRepository.phpWhen you run composer install, it generates a vendor/autoload.php file that registers a custom autoloader function. That function uses the mapping from composer.json to locate and include files dynamically.
Here's a simplified version of what happens under the hood:
spl_autoload_register(function ($class) {
// Map App\Repositories\UserRepository to src/Repositories/UserRepository.php
$prefix = "App\\";
$baseDir = "src/";
if (strpos($class, $prefix) === 0) {
$relativeClass = substr($class, strlen($prefix));
$file = $baseDir . str_replace("\\", "/", $relativeClass) . ".php";
if (file_exists($file)) {
require $file;
}
}
});A common misconception: developers think autoloading is "magic." It's not. It's a registered function that runs when a class isn't found, converting a namespace and class name into a file path and requiring it. Performance is excellent because files are only loaded when needed.
What changed in PHP 8: The big shifts
PHP 8 (released November 2020) was a major version bump for good reason. Several changes affect how you write code and how you approach interview questions.
Named arguments let you pass function arguments by name instead of position. This makes code clearer and more flexible:
// Before PHP 8:
function sendEmail($to, $subject, $body, $replyTo = null) {}
sendEmail("user@example.com", "Hello", "This is the body");
// PHP 8: Named arguments
sendEmail(
to: "user@example.com",
subject: "Hello",
body: "This is the body"
);
// Or out of order:
sendEmail(
subject: "Hello",
to: "user@example.com",
body: "This is the body"
);Union types let a variable accept multiple types. Before PHP 8, you used docblocks or had to accept mixed types without type hints:
// PHP 8
function process(int|string $id): int|string {
if (is_int($id)) {
return $id * 2;
}
return strlen($id);
}
// Better than:
function process($id) {
// What types does $id accept? No IDE help.Constructor property promotion reduces boilerplate. Previously, you'd declare properties and then assign them in the constructor:
// Before PHP 8:
class User {
private $id;
private $name;
private $email;
public function __construct($id, $name, $email) {
$this->id = $id;
$this->name = $name;
$this->email = $email;
}
}
// PHP 8:
class User {
public function __construct(
private int $id,
private string $name,
private string $email
) {}
}Attributes (formerly called annotations) allow metadata on classes, properties, and methods without external parsing:
// PHP 8
#[Route("/users", "GET")]
#[Middleware("auth")]
public function getUsers() {
// ...
}Nullsafe operator (?->) chains method calls without null checks:
// Before PHP 8:
$email = null;
if ($user !== null && $user->getProfile() !== null) {
$email = $user->getProfile()->getEmail();
}
// PHP 8:
$email = $user?->getProfile()?->getEmail();Match expression is more powerful than switch and returns a value:
// Before:
$status = "pending";
$message = "";
switch ($status) {
case "pending":
$message = "Waiting...";
break;
case "complete":
$message = "Done!";
break;
}
// PHP 8:
$message = match ($status) {
"pending" => "Waiting...",
"complete" => "Done!",
default => "Unknown"
};Weak comparisons changed behavior in edge cases. For example, comparing a string to zero now follows stricter rules. This is subtle but important for backwards compatibility.
Common mistakes developers make
- Assuming == is "good enough." It's not. Use === by default. The few times you need loose comparison, add a comment explaining why.
- Mixing abstract classes and interfaces without a clear pattern. If you're unsure, start with an interface. Upgrade to an abstract class only when you have shared implementation.
- Misunderstanding autoloading speed. Some developers think autoloading is slow and manually require files. It's not a performance concern; Composer generates an optimized classmap for production.
- Ignoring PHP 8 features in legacy codebases. Even if your framework doesn't require PHP 8, learning these features prepares you for the next major version upgrade and makes you a stronger interviewer.
- Not testing type coercion edge cases. Write unit tests that explicitly check boundary conditions with different types. You'll catch bugs before production.
Key takeaways
- Use
===for comparisons unless you have a documented reason for loose equality. Type juggling is powerful but dangerous. - Choose abstract classes for shared behavior, interfaces for contracts, and traits for horizontal code reuse. They solve different problems.
- Autoloading via PSR-4 and Composer is deterministic and fast. Understand how namespaces map to file paths, not just that it "works."
- PHP 8's features (named arguments, union types, constructor promotion, nullsafe operator, match expressions) reshape how you write and test code.
- These fundamentals appear in interviews because they reveal whether you understand PHP deeply or just use frameworks. Invest time here.






Comments
Loading comments…