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)

21. What is the difference between self, parent, and static in PHP?Language SpecificMedium

Question Details

Explain compile-time class resolution, inheritance, late static binding, factory methods, and cases where self prevents polymorphic behavior.

Short Interview Answer (30-60 seconds)

The practical difference is how PHP chooses the class. self refers to the class where the current method is declared. parent refers to the immediate parent of the class where the current code is written. static uses late static binding and refers to the class that was called at runtime. I use self when behavior must stay fixed, parent when I need the parent implementation, and static when inherited code must respect the child class.

Detailed Explanation

See the Code while reading this explanation.

These three words tell PHP which class to use when one class extends another. The choice affects whether a child class can change inherited behavior. self keeps the operation tied to the class that contains the method. parent moves the operation to that class's immediate parent. static follows the class used in the original call. This difference matters most in shared factory methods and reusable base classes. Choosing the wrong word can create the wrong object, call the wrong method, or ignore a value supplied by a child class.

Useful Questions to Ask the Interviewer
  1. Should an inherited method follow the child class?
  2. Is the method expected to create an object?
  3. Must the parent implementation also run?
What is the difference between self, parent, and static in PHP? diagram
How to Explain It in an Interview

self is resolved using the class where the current method was declared. For example, if Document declares a factory containing new self(), that factory creates a Document even when it is called as Report::createFixed(). This fixed resolution can prevent polymorphic behavior.

parent is resolved using the immediate parent of the class where the parent expression is written. It is commonly used inside an overridden method to call the implementation that the child replaced. A parent:: call is also a forwarding call. This means it preserves the class from the original runtime call if the parent method later uses static::.

static uses late static binding. PHP remembers the called class from the most recent non forwarding call. A direct call such as Report::createFlexible() sets Report as the called class. Therefore, new static() inside the inherited method creates a Report. Calls made with self::, parent::, or static:: normally forward that called class instead of replacing it.

Use self when the implementation must remain tied to the declaring class. Use parent when an override must reuse its immediate parent implementation. Use static for extensible factories, overridable class constants, shared configuration, and other inherited behavior that should follow the called class.

The keywords themselves do not copy objects or create extra objects. In the example, each new expression allocates one object. The meaningful difference is its class. The lookup cost is normally insignificant compared with application work, so production decisions should focus on correct inheritance behavior. Visibility and final declarations still apply. For example, late binding cannot legally call an inaccessible private method in a child class, and a final method cannot be overridden.

Example

The example uses Document as the base class and Report as the child class. createFixed uses new self(), so PHP creates Document because that method is declared in Document. createFlexible uses new static(), so PHP creates Report when the method is called through Report. declaredClass uses self::class and returns Document. calledClass uses static::class and returns Report. Report::description() uses parent::description() to run the immediate parent implementation before adding its own text.

Code
<?php

declare(strict_types=1);

class Document
{
    public static function createFixed(): self
    {
        // self is resolved to Document because this method is declared here.
        return new self();
    }

    public static function createFlexible(): static
    {
        // static follows the class used for the runtime call.
        return new static();
    }

    public static function declaredClass(): string
    {
        return self::class;
    }

    public static function calledClass(): string
    {
        return static::class;
    }

    public function description(): string
    {
        return 'Base document';
    }
}

class Report extends Document
{
    public function description(): string
    {
        // parent calls the immediate parent implementation.
        return parent::description() . ' with report details';
    }
}

$fixed = Report::createFixed();
$flexible = Report::createFlexible();

echo get_class($fixed) . PHP_EOL;
echo get_class($flexible) . PHP_EOL;
echo Report::declaredClass() . PHP_EOL;
echo Report::calledClass() . PHP_EOL;
echo $flexible->description() . PHP_EOL;
Where it is used

These keywords are used in inherited factory methods, base entity classes, reusable service classes, shared configuration methods, and overridden methods. A base factory can use new static() so each child creates its own object type. An overridden method can use parent::method() to keep shared parent work. A helper can use self when its behavior must remain tied to the class that declares it.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands inheritance, class scope resolution, method overriding, and late static binding in PHP. They also want to know whether the candidate can design an inherited factory or shared base class without accidentally fixing behavior to the wrong class.

Common interview mistakes

A common mistake is treating self and static as interchangeable. self remains tied to the declaring class, while static follows the called class. Another mistake is using new self() in a base factory that must return child objects. Developers may also think parent can select any ancestor, but it refers to the immediate parent of the class containing that expression. Another mistake is assuming parent:: discards late binding. It is a forwarding call, so a parent method that uses static:: can still see the original called class. Developers must also remember that visibility, final methods, constructor requirements, and return types still apply.

Interview tip

State the three resolution rules first. Then show one inherited factory where new self() creates the base class and new static() creates the called child class. Mention that parent:: calls the immediate parent and preserves the called class for later static:: resolution.

Interviewer may ask next
What called class is used when a child method invokes parent::factory() and the parent factory contains new static()?

The original runtime called class is used. parent::factory() is a forwarding call, so it does not replace the called class with the parent. If Report makes the original call, new static() in the parent factory creates a Report. This matters because parent logic can remain reusable while still preserving child specific behavior. Visibility and constructor compatibility must still allow the object to be created.

When should a production design use new self() instead of new static() in a factory method?

It should use new self() when the factory must always create the declaring class and child classes must not change that result. This gives fixed and predictable behavior. The tradeoff is that the factory is not polymorphic, so it is unsuitable when subclasses are expected to create their own types. The keyword does not provide a meaningful performance or memory advantage over new static(); the design choice should be based on required inheritance behavior.

22. What are typed properties, union types, intersection types, and nullable types in PHP?Language SpecificMedium

Question Details

Explain initialization rules, coercive versus strict calls, variance constraints, nullability, false or true literal types where applicable, and API-design tradeoffs.

Short Interview Answer (30-60 seconds)

These features let me describe PHP values more precisely. A typed property accepts only its declared type and may remain uninitialized until I assign it. A union accepts any one listed type. An intersection requires an object that satisfies every listed class or interface type. A nullable type accepts the declared type or null. I also remember that strict types affects scalar coercion at the calling or assignment location, while inheritance rules control whether child declarations may become broader or narrower.

Detailed Explanation

See the Code while reading this explanation.

These PHP features let a program clearly state what kind of value is allowed. A class field can require one kind of value. A method can allow one value from several choices. It can also require one object to provide several abilities at the same time. Another form allows either a normal value or no value, represented by null. These rules help PHP detect incorrect values earlier. They also help developers understand how a class or method should be used without reading all of its internal code.

Useful Questions to Ask the Interviewer
  1. Which PHP versions must the application support?
  2. Is null a real business value or only a missing value?
  3. Should the public API accept several representations or one normalized value?
What are typed properties, union types, intersection types, and nullable types in PHP? diagram
How to Explain It in an Interview

A typed property declares the type a class property may contain, such as public int $count. If the property has no default and the constructor does not assign it, it is uninitialized. Reading it then throws an Error. Declaring ?string does not initialize the property to null. Use public ?string $name = null when null should be the initial value.

A union type such as int|string accepts a value matching any one member. PHP first prefers an exact match. When strict types is disabled, compatible scalar values may be coerced. With strict types enabled, incompatible scalar values normally cause a TypeError, although an integer is accepted where a float is declared. Strictness is determined by the file performing the call. For property assignment, it is determined by the code performing the assignment.

An intersection type such as Countable&Iterator accepts only an object that satisfies every listed class or interface type. Pure intersection types were added in PHP 8.1. PHP 8.2 added disjunctive normal form types, so a declaration may use a grouped intersection inside a union, such as (Countable&Iterator)|array. The parentheses are required.

A nullable type means the declared type or null. ?User is equivalent to User|null. The short question mark form cannot be combined with additional union members. For several alternatives, write User|string|null. From PHP 8.4, relying on an implicitly nullable parameter through a null default is deprecated, so null should be declared explicitly.

PHP also supports the standalone literal types false, true, and null from PHP 8.2. Use bool instead of true|false. Literal types can describe legacy return values precisely, but a result object, enum, exception, or nullable return may create a clearer new API.

Method parameters are contravariant, so a child may accept a broader type. Return types are covariant, so a child may return a narrower type. Normal properties that can be both read and written are invariant. PHP 8.4 property hooks can permit covariance for a property that is only read and contravariance for one that is only written.

Type declarations add runtime checks but do not normally create copies of values or allocate replacement objects. Their direct performance and memory cost is usually small. The larger production benefit is earlier failure, clearer contracts, and better static analysis.

Example

The example uses a typed integer property, a nullable string property with an explicit null default, a union parameter, an intersection parameter, and a literal false return type. The constructor initializes the required property before it can be read. The identifier function accepts either an integer or string and returns one normalized string form. The collection function accepts only an object implementing both Countable and Iterator. The lookup function demonstrates a legacy style User|false result, while the surrounding explanation notes that a clearer result design may be preferable for a new API.

Code
<?php

declare(strict_types=1);

final class User
{
    public int $id;
    public ?string $nickname = null;

    public function __construct(int $id)
    {
        // Initialize the required typed property before it is read.
        $this->id = $id;
    }
}

function normalizeId(int|string $id): string
{
    // The union allows either an integer or a string.
    return (string) $id;
}

function countAndRead(Countable&Iterator $items): int
{
    // The object must satisfy both interfaces.
    $items->rewind();

    if ($items->valid()) {
        echo (string) $items->current(), PHP_EOL;
    }

    return count($items);
}

function findUser(int $id): User|false
{
    // The literal false type represents a failed legacy lookup.
    if ($id <= 0) {
        return false;
    }

    return new User($id);
}

$user = findUser(10);

if ($user !== false) {
    echo normalizeId($user->id), PHP_EOL;
    var_dump($user->nickname);
}

$items = new ArrayIterator(['a', 'b', 'c']);
echo countAndRead($items), PHP_EOL;
Where it is used

Typed properties are common in domain models, services, configuration objects, data transfer objects, and framework components. Union types are useful when an API intentionally accepts a small set of representations, such as an identifier that may be an integer or string. Intersection types are useful when one object must provide several capabilities, such as being both countable and iterable. Nullable types are appropriate when absence is a valid and documented state. In production code, narrow and explicit declarations improve testing, refactoring, static analysis, and error detection. Broad unions and unnecessary null values should be avoided because they move complexity into every caller.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands how PHP enforces type contracts at runtime. They also evaluate knowledge of property initialization, scalar coercion, strict calls, inheritance variance, null handling, literal types, version boundaries, and the tradeoffs involved in designing clear public APIs.

Common interview mistakes

A common mistake is assuming that a nullable property automatically starts as null. It remains uninitialized unless it has a null default or receives an assignment. Another mistake is believing strict types disables every conversion everywhere. It mainly controls scalar coercion according to the file performing a call or property assignment, and an integer is still valid for a float declaration. Developers also create unions that are too broad, place scalar types in intersections, omit the required parentheses in a grouped intersection and union, write redundant combinations such as bool|false, or change inherited declarations in an invalid direction. Another mistake is using false and null as interchangeable failure values without documenting their different meanings.

Interview tip

Define each form in one sentence first. Then explain the uninitialized property state, one member versus every member, explicit nullability, coercive and strict behavior, and method variance. Finish with one practical API tradeoff, such as preferring a clear value object over a growing union.

Interviewer may ask next
What happens if code reads a nullable typed property before assigning any value?

It throws an Error because nullable and initialized are separate ideas. A declaration such as public ?string $name permits either a string or null after assignment, but the property initially remains uninitialized. Giving it = null creates an actual initialized null value. This matters because even a type safe property can fail at runtime when construction does not establish a valid initial state.

When should an API use a union type instead of a shared interface or value object?

Use a union when the alternatives are few, intentional, and represent the same clear concept, such as int|string for an external identifier. Prefer a shared interface or value object when each alternative needs different handling, validation, or behavior. A union is simple for callers at first, but a dedicated abstraction can keep the contract clearer and prevent repeated type checks as the API grows.

23. How does PHP copy-on-write affect arrays and strings?Language SpecificMedium

Question Details

Explain assignment without immediate copying, when mutation triggers separation, references, function calls, memory observations, and why the model matters for performance reasoning.

Short Interview Answer (30-60 seconds)

PHP normally uses copy on write for arrays and strings. Assigning one variable to another usually lets both values share the same internal data instead of copying all of it immediately. If one variable is later changed, PHP separates that value so the other variable remains unchanged. Explicit references made with an ampersand are different because both names refer to the same variable, so a change through either name is visible through both.

Detailed Explanation

See the Code while reading this explanation.

PHP avoids making a full duplicate each time one variable receives an array or a piece of text from another variable. Both variables can initially use the same saved information. This saves work when the second variable is only read. When one variable must change, PHP creates a separate value for that variable before applying the change. The original variable then keeps its old contents. This behavior is useful, but a later change can still require extra time and memory.

Useful Questions to Ask the Interviewer
  1. Should I compare normal assignment with assignment by reference?
  2. Should I include function arguments passed by value and by reference?
  3. Should I explain nested arrays and objects stored inside arrays?
  4. Should memory measurements be discussed as approximate observations?
How does PHP copy-on-write affect arrays and strings? diagram
How to Explain It in an Interview

PHP arrays and strings have value semantics. After normal assignment, changing the new variable must not change the original variable. PHP can provide this result without immediately duplicating all stored data.

For example, after $copy = $original, both variables may share the same internal array or string data. PHP keeps internal usage information for that shared data. Reading either variable does not require separation.

If code mutates one shared value, PHP separates it before applying the change. Changing an array element, appending an element, removing an element, or changing a string character can trigger this process. The cost of separation grows with the amount of container data or string data that must be copied.

Passing an array or string to a function by value follows the same principle. The function parameter may initially share internal data with the caller value. If the function only reads it, a full copy is normally unnecessary. If the function mutates its local parameter, PHP separates the local value and the caller value remains unchanged.

An explicit reference uses different semantics. With $second =& $first, both names refer to the same variable. A change through either name is visible through both names. This is aliasing, not normal copy on write value behavior.

Array separation copies the array container, but nested refcounted values may remain shared until they are themselves changed. Objects are also important because copying an array does not clone objects stored inside it. Both arrays can still contain handles to the same object.

Memory measurements are only observations. Exact numbers depend on the PHP memory manager, allocated capacity, reused blocks, and the specific build. Assignment is therefore not guaranteed to have zero cost, and a measured increase should not be treated as the exact size of a copied value.

Example

The example assigns an array and a string normally, then mutates only their copied variables. The original array and string remain unchanged because PHP separates the copied value when required. A function that accepts an array by value changes only its local parameter and returns the changed result. A function that accepts an array by reference changes the caller variable directly. The example also prints an observed memory difference around an array mutation, but it does not claim that the number equals the exact copied array size.

Code
<?php

declare(strict_types=1);

function changeByValue(array $items): array
{
    // The parameter may initially share internal array data.
    // This mutation separates the local value when required.
    $items['status'] = 'changed inside value function';

    return $items;
}

function changeByReference(array &$items): void
{
    // The ampersand allows direct mutation of the caller variable.
    $items['status'] = 'changed through reference';
}

$originalArray = [
    'status' => 'original',
    'count' => 10,
];

$copiedArray = $originalArray;

$memoryBeforeMutation = memory_get_usage();
$copiedArray['count'] = 20;
$memoryAfterMutation = memory_get_usage();

echo "Normal array assignment:\n";
echo "Original count: {$originalArray['count']}\n";
echo "Copied count: {$copiedArray['count']}\n";
echo 'Observed memory difference: '
    . ($memoryAfterMutation - $memoryBeforeMutation)
    . " bytes\n\n";

$originalString = 'PHP copy on write';
$copiedString = $originalString;
$copiedString[0] = 'X';

echo "Normal string assignment:\n";
echo "Original string: {$originalString}\n";
echo "Copied string: {$copiedString}\n\n";

$valueResult = changeByValue($originalArray);

echo "Function parameter passed by value:\n";
echo "Caller status: {$originalArray['status']}\n";
echo "Returned status: {$valueResult['status']}\n\n";

changeByReference($originalArray);

echo "Function parameter passed by reference:\n";
echo "Caller status: {$originalArray['status']}\n";
Where it is used

This behavior matters when production code works with large configuration arrays, decoded JSON data, database result arrays, request data, import records, templates, or long strings. Read only processing can avoid an immediate full copy. A later mutation can create a memory increase, which is important in command line imports, queue workers, batch jobs, and long running PHP processes. It also affects function design. Passing a value normally is appropriate when a function should not change the caller variable. Passing by reference is appropriate only when changing the caller variable is an intentional and documented part of the function contract.

Why Interviewers Ask This

Interviewers ask this question to check whether the candidate understands the difference between visible PHP value behavior and the internal work performed by the engine. A strong answer explains shared storage, separation during mutation, value parameters, explicit references, nested values, and realistic memory observations. It also shows whether the candidate can reason about performance without incorrectly claiming that every assignment immediately copies the complete array or string.

Common interview mistakes

A common mistake is saying that every assignment immediately copies the complete array or string. Another mistake is saying that all PHP variables are references. Normal value assignment, explicit references, and object handle behavior are different concepts. Candidates may also claim that passing a large array by value always copies it before the function begins. PHP can delay the copy until a mutation requires separation. Another mistake is assuming that copying an array clones objects stored inside it. The copied array can still contain handles to the same objects. Developers may also treat memory_get_usage output as an exact copy size, even though the memory manager can reserve and reuse memory. Finally, adding references only as a performance trick can create unexpected shared changes and make the code harder to reason about.

Interview tip

Start with the practical result: normal assignment does not immediately require a full copy, but mutation can trigger separation. Then contrast normal assignment, a value function parameter, and an explicit reference. Mention nested objects and approximate memory measurements to show complete runtime understanding.

Interviewer may ask next
What happens when a copied PHP array contains another array or an object and a nested value is changed?

The top level array follows copy on write, but copying it does not immediately create fully independent copies of every nested value. A nested array may remain shared until that nested array is mutated, at which point its own separation can occur. An object is different because the array stores an object handle. Copying the array does not clone the object, so changing an object property through either array can affect the same object. This matters because a copied outer array is not automatically a deep copy.

Should a function accept a large array by reference only to avoid a copy?

No. A value parameter can initially share the array data with the caller, so calling the function does not automatically require a complete array copy. A reference changes the function contract because the function can directly mutate the caller variable. It should be used only when that caller mutation is intentional. The tradeoff is that a reference may support direct updates, but it introduces shared mutable state and makes the code more difficult to understand and maintain.

24. How do PHP attributes work, and when would you use them?Language SpecificMedium

Question Details

Explain declaring and targeting attributes, reflection-based reading, repeatable attributes, constructor arguments, metadata use cases, and runtime costs.

Short Interview Answer (30-60 seconds)

PHP attributes attach structured metadata to declarations such as classes, methods, properties, parameters, functions, and constants. I declare an attribute class with #[\Attribute], select its allowed targets, and accept constant expression arguments through its constructor. Attributes do nothing by themselves. Application or framework code must read them through reflection. I use them for stable code related metadata such as routes, validation rules, and serialization settings. I normally process and cache that metadata instead of repeating reflection and object creation in a hot path.

Detailed Explanation

See the Code while reading this explanation.

PHP attributes let developers place structured information beside the part of a program that the information describes. For example, a method can carry information saying which web address should call it. PHP stores this information, but it does not perform the requested action automatically. Another part of the application must read the information and decide what to do. This makes attributes useful for rules that belong closely to source code and change when that code changes. Useful questions for the interviewer are:

Useful Questions to Ask the Interviewer
  1. Which declarations must the attribute support?
  2. Can it appear more than once on one declaration?
  3. When and how often will the application read it?
How do PHP attributes work, and when would you use them? diagram
How to Explain It in an Interview

An attribute class is marked with #[\Attribute]. Its constructor defines the values that callers may supply. Attribute arguments may be positional or named, but they must be literal values or constant expressions. For example, #[Route(path: '/users', method: 'GET')] supplies values that can later be passed to the Route constructor.

The flags passed to #[\Attribute] restrict valid targets. PHP supports targets for classes, functions, methods, properties, class constants, parameters, and, beginning with PHP 8.5, global constants. Multiple target flags can be combined with the bitwise OR operator. Without an explicit target, the default is Attribute::TARGET_ALL. Attribute::IS_REPEATABLE allows the same attribute class to appear more than once on one declaration.

Reflection methods such as ReflectionClass::getAttributes and ReflectionMethod::getAttributes return ReflectionAttribute objects. getName returns the attribute class name. getArguments returns the stored arguments without creating the attribute object. newInstance creates the object and invokes its constructor.

An important edge case is deferred validation. getAttributes can return metadata even when an attribute uses an invalid target or is repeated without Attribute::IS_REPEATABLE. PHP reports that problem when newInstance is called. newInstance can also fail if the attribute class is missing or its constructor arguments are invalid.

Attributes are passive metadata. A Route attribute does not register a route by itself. A router must read it and build a route table. Good uses include routing, validation, event registration, dependency injection hints, test markers, and serialization rules. External configuration is usually better for values that operators must change without editing and deploying source code.

Reflection scans and each newInstance call take runtime work. Each created attribute object also uses memory. The exact cost depends on the number of declarations, attributes, arguments, and objects, so a fixed cost should not be claimed. Production systems commonly process attributes during startup, cache warming, container building, or the first lookup, then reuse a compact cached result. Long running workers must refresh that cache when deployed code changes.

Example

The example defines a repeatable Route attribute that is valid only on methods. Its constructor receives a path and an HTTP method. One controller method has two Route declarations. Reflection reads the matching metadata and calls newInstance for each declaration. That call creates each Route object and runs its constructor. The application then prints the values. This demonstrates that PHP stores metadata while application code remains responsible for interpreting it.

Code
<?php

declare(strict_types=1);

#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::IS_REPEATABLE)]
final class Route
{
    public function __construct(
        public readonly string $path,
        public readonly string $method
    ) {
        if ($path === '') {
            throw new \InvalidArgumentException('The route path cannot be empty.');
        }

        if ($method === '') {
            throw new \InvalidArgumentException('The HTTP method cannot be empty.');
        }
    }
}

final class UserController
{
    #[Route(path: '/users', method: 'GET')]
    #[Route(path: '/members', method: 'GET')]
    public function listUsers(): void
    {
    }
}

$reflection = new \ReflectionMethod(UserController::class, 'listUsers');

foreach ($reflection->getAttributes(Route::class) as $attribute) {
    // This creates the Route object and invokes its constructor.
    $route = $attribute->newInstance();

    echo $route->method . ' ' . $route->path . PHP_EOL;
}
Where it is used

Attributes are used in production for HTTP route definitions, validation constraints, event listener registration, dependency injection hints, object serialization names, authorization metadata, command registration, and test discovery. They work best for stable metadata that belongs beside a declaration. Applications commonly scan them during startup or cache building and store a simpler lookup structure for later requests.

Why Interviewers Ask This

Interviewers ask this to check whether the candidate understands how PHP represents structured metadata, how reflection discovers it, and when PHP validates and creates attribute objects. It also tests practical judgment about target restrictions, repeatable attributes, constructor arguments, runtime cost, memory cost, caching, and suitable production use cases.

Common interview mistakes

A common mistake is assuming PHP automatically performs the behavior described by an attribute. Attributes only provide metadata. Another mistake is assuming target and repeatability rules are always rejected when PHP parses the declaration. For user defined attributes, those rules are validated when newInstance is called. Developers may also forget Attribute::IS_REPEATABLE, use arguments that are not constant expressions, place slow work or external calls inside an attribute constructor, create attribute objects repeatedly in a hot path, or use attributes for settings that should be changed without a code deployment.

Interview tip

Start by saying that attributes are passive structured metadata. Then explain the attribute class, target flags, constructor arguments, reflection, repeatability, and deferred validation. Clearly distinguish getArguments from newInstance. Finish with one production use case and explain that repeated reflection and object creation should usually be replaced by cached processed metadata.

Interviewer may ask next
When are invalid targets and nonrepeatable attribute uses detected?

They are detected when ReflectionAttribute::newInstance is called. Reflection getAttributes can still return a ReflectionAttribute for a declaration that uses the wrong target or repeats an attribute that lacks Attribute::IS_REPEATABLE. newInstance validates the attribute use while creating the object and throws an Error when the use is invalid. This matters because discovery and validation are separate steps, so production code should validate attributes while building its metadata cache rather than waiting for a later request.

What are the runtime and memory tradeoffs of reading attributes in production?

Reflection scanning takes runtime work, and every newInstance call allocates an attribute object and invokes its constructor. Keeping those objects or large processed maps also consumes memory. The exact amount depends on the number of declarations, attributes, arguments, and retained results. A common tradeoff is to scan once during startup or cache warming and store only the compact data needed at runtime. This reduces repeated work but requires cache invalidation after code changes and lifecycle care in long running workers.

25. How do PHP namespaces and Composer PSR-4 autoloading work together?Language SpecificMedium

Question Details

Explain namespace declarations, use imports and aliases, namespace-to-directory mappings, generated autoloaders, case sensitivity, and common deployment mistakes.

Short Interview Answer (30-60 seconds)

PHP namespaces give classes unique fully qualified names, while Composer PSR 4 rules map namespace prefixes to base directories. When PHP first needs an undefined class, the Composer autoloader uses that mapping to locate and include the matching file. The namespace, subdirectories, class name, and file name must use matching letter case. A use statement only creates a local name or alias. It does not include the class file by itself.

Detailed Explanation

Namespaces give PHP classes clear and unique names. Composer connects the beginning of each class name to a folder. When the program needs a class that has not been opened yet, Composer finds and opens the matching file. This avoids writing many manual file includes. The important rule is that the declared class name, folder structure, and file name must agree. A spelling or letter case difference may remain hidden on one computer but fail after deployment on a system that treats upper and lower case letters differently.

Useful Questions to Ask the Interviewer
  1. What namespace prefix and source directory does the project use?
  2. Should production autoload optimization be included in the discussion?
  3. Does the deployment environment use a case sensitive file system?
How do PHP namespaces and Composer PSR-4 autoloading work together? diagram
How to Explain It in an Interview

A namespace declaration becomes part of a class fully qualified name. For example, a class declared inside namespace App\Service with the name Mailer has the fully qualified name App\Service\Mailer.

A use declaration imports that name into the current file. For example, use App\Service\Mailer allows the file to write Mailer instead of the full name. An alias such as use App\Service\Mailer as EmailMailer provides another local name. Imports are resolved when PHP compiles the file. They do not load files, and each file has its own imports. ([php.net](https://www.php.net/manual/en/language.namespaces.importing.php))

Composer PSR 4 configuration maps a namespace prefix to one or more base directories. A composer.json mapping of App\\ to src means that App\Service\Mailer maps to src/Service/Mailer.php. Composer removes the mapped prefix, converts the remaining namespace separators into directory separators, and adds the PHP file extension. Subdirectory names and the final file name must match the referenced class name letter case. ([php-fig.org](https://www.php-fig.org/psr/psr-4/))

Composer generates vendor/autoload.php and supporting files under vendor/composer. The application normally requires vendor/autoload.php once. Composer then registers an autoload function with PHP. When PHP encounters an undefined class like App\Service\Mailer, PHP calls registered autoload functions before reporting that the class cannot be found. ([php.net](https://www.php.net/manual/en/language.oop5.autoload.php))

Namespaces and autoloading therefore solve different problems. Namespaces identify classes. Composer locates their files. PHP does not require one class per file, but PSR 4 works best when each autoloaded class has a predictable matching file.

In production, run Composer install from the locked dependency file and regenerate autoload data after changing mappings. Optimized autoloading builds a class map for known classes. This uses additional generated metadata and some memory, but reduces repeated file system checks. Authoritative class maps require more care because classes generated after deployment cannot be discovered unless they are present in the generated map.

Where it is used

This approach is used in modern PHP applications, reusable Composer packages, command line programs, web applications, background workers, and test suites. A project may map App to src for production classes and Tests to tests for development classes. Third party packages provide their own mappings, and Composer combines them into one generated autoloader. Long running workers should deploy updated code and autoload metadata together, then restart workers so existing processes do not continue using already loaded class definitions.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands the difference between naming a PHP class and locating its source file. It also tests whether the candidate can configure Composer correctly, follow PSR 4 path rules, explain PHP autoload behavior, and diagnose production failures caused by incorrect prefixes, directory paths, file names, letter case, or stale generated autoload data.

Common interview mistakes

Common mistakes include believing that a use declaration includes a file, placing a class in a directory that does not match the configured prefix, declaring the wrong namespace inside the file, and using file or directory letter case that differs from the class name. Other mistakes include forgetting to require vendor/autoload.php, changing composer.json without regenerating autoload data, deploying source files without matching Composer files, running Composer update instead of Composer install during a normal locked deployment, and enabling an authoritative class map when the application creates classes dynamically at runtime.

Interview tip

Explain the two responsibilities separately. First say that namespaces identify classes. Then say that Composer maps those names to files. Use one example from App\Service\Mailer to src/Service/Mailer.php. Finish by mentioning that use only imports a name, letter case must match, and production deployment must regenerate the autoloader when mappings change.

Interviewer may ask next
What happens when the namespace or file path uses different letter case?

The class may fail to autoload because PSR 4 requires class references, subdirectory names, and the final file name to use matching letter case. PHP class names are generally resolved without letter case differences after a class is loaded, but PSR 4 file lookup still requires case correct references and paths. This matters because a mismatch may appear to work on a case insensitive development file system and then produce a class not found error on a case sensitive production file system. The fix is to correct the declaration, reference, directory names, and file name rather than relying on the operating system.

What are the tradeoffs of optimized and authoritative Composer autoloading?

Optimized autoloading builds a class map for known classes, which reduces file system checks and usually improves production class lookup. The generated map adds metadata and consumes some memory, but that cost is normally small compared with the benefit in a large application. An authoritative class map goes further by treating classes missing from the map as nonexistent. This makes failed lookups faster, but runtime generated classes cannot be discovered unless they were included when the map was built. Production deployment must rebuild the map whenever classes or autoload mappings change. ([getcomposer.org](https://getcomposer.org/doc/articles/autoloader-optimization.md))

26. How do closures capture variables in PHP?Language SpecificMedium

Question Details

Explain the use clause, capture by value versus by reference, arrow-function implicit capture, object context, and common loop or mutation surprises.

Short Interview Answer (30-60 seconds)

PHP closures capture outside local variables through the use clause. use ($value) saves the variable value when the closure is created, while use (&$value) shares the same variable, so later changes are visible in both places. Arrow functions capture used outside variables automatically by value. Capturing an object by value still gives access to the same object instance, so its properties can be changed.

Detailed Explanation

See the Code while reading this explanation.

A small saved function may need information from the place where it was created. PHP lets the programmer decide whether that information should stay as it was at creation time or follow later changes. This matters when the function runs later, such as during sorting, filtering, event handling, or deferred work. Choosing the wrong behavior can make a saved function return an old value, unexpectedly change outside state, or make every function created in a loop use the same final value.

Useful Questions to Ask the Interviewer
  1. Should changes made after creation be visible inside the closure?
  2. Does the closure only read the outside value, or must it change it?
  3. Is the captured value a scalar, an array, or an object?
How do closures capture variables in PHP? diagram
How to Explain It in an Interview

A normal PHP anonymous function does not automatically receive ordinary local variables from the surrounding scope. The variables must be listed in a use clause.

With use ($count), PHP captures the value held by $count when the closure is created. If the outer variable is later assigned a different value, the closure still sees its captured value. This is normally the safer choice because the closure has stable and predictable input.

With use (&$count), PHP captures the variable by reference. The closure and the surrounding code access the same variable container. A change made in either place is visible in the other place. This is useful for counters or result collection, but shared mutation can make code harder to understand and test.

Arrow functions use the fn syntax. They automatically capture every outside variable used in their expression by value. They do not have a use clause, and assigning to a captured scalar does not update the outer scalar.

Objects require an important distinction. Capturing an object variable by value does not clone the object. The captured value still identifies the same object instance, so the closure can change its properties. If the outer variable is later assigned a different object, the closure continues to use the object captured earlier.

A non static closure created inside an object method can use $this automatically. A static closure has no $this context.

Loop behavior is a common surprise. use ($item) saves the value from each iteration. use (&$item) shares the reused loop variable, so closures executed later can all see its final value.

Captured values remain reachable while the closure remains reachable. Large objects can therefore stay in memory. Arrays captured by value use PHP value semantics and normally benefit from copy on write behavior, so a separate array allocation is generally needed only when one side mutates it.

Example

The example creates two groups of closures inside one loop. Each value capture closure remembers the current number when it is created, so the first group returns 1, 2, and 3. Each reference capture closure shares the same loop variable, so the second group returns the final value 3 three times. The arrow function captures the multiplier value 4 when it is created, so changing the outer multiplier to 10 does not change its result. The object example shows that value capture does not clone an object, so the closure changes the same object instance.

Code
<?php

declare(strict_types=1);

$valueClosures = [];
$referenceClosures = [];

foreach ([1, 2, 3] as $number) {
    // Save the current number in this closure.
    $valueClosures[] = function () use ($number): int {
        return $number;
    };

    // Share the reused loop variable with this closure.
    $referenceClosures[] = function () use (&$number): int {
        return $number;
    };
}

echo "Value capture: ";
foreach ($valueClosures as $closure) {
    echo $closure() . ' ';
}
echo PHP_EOL;

echo "Reference capture: ";
foreach ($referenceClosures as $closure) {
    echo $closure() . ' ';
}
echo PHP_EOL;

$multiplier = 4;

// Arrow functions capture used outside variables by value.
$multiply = fn (int $input): int => $input * $multiplier;

$multiplier = 10;

echo "Arrow function result: " . $multiply(3) . PHP_EOL;

$state = new stdClass();
$state->count = 0;

$increment = function () use ($state): void {
    // Value capture does not clone the object.
    $state->count++;
};

$increment();
$increment();

echo "Object count: " . $state->count . PHP_EOL;
Where it is used

Closures are used in array callbacks, custom sorting, route handlers, middleware, event listeners, deferred jobs, dependency configuration, and callback based APIs. Value capture is useful when a callback must keep a stable setting or the current value from a loop iteration. Reference capture is useful when a callback must update a shared counter or collect results, but the mutation should be small and obvious. Arrow functions are useful for short mapping and filtering expressions. In long running workers and callback registries, developers should avoid capturing large service objects unless they are required because the closure can keep those objects and their related data in memory.

Why Interviewers Ask This

Interviewers ask this question to check whether a candidate understands when a closure receives an outside variable, whether later changes are visible, and how value capture differs from reference capture. It also tests knowledge of arrow functions, object behavior, loop surprises, memory retention, and safe production choices.

Common interview mistakes

A common mistake is assuming that a normal anonymous function automatically receives every outside local variable. Another mistake is expecting use ($value) to see a later assignment made to the outer variable. Developers may use use (&$value) inside a loop and then discover that every stored closure sees the final loop value. It is also incorrect to think that capturing an object by value creates a cloned object. The closure still accesses the same object instance. Other mistakes include expecting an arrow function to update an outer scalar, expecting $this inside a static closure, and capturing large objects in long lived callbacks without considering memory retention.

Interview tip

Start with the direct comparison between use ($value) and use (&$value). Then explain automatic value capture in arrow functions. Finish with the two main surprises: an object is not cloned by value capture, and reference capture inside a loop can make every stored closure see the final loop value.

Interviewer may ask next
What happens when several closures capture the same loop variable by reference?

They all share the same variable container. When the closures run after the loop, they normally read the loop variable's final value instead of the value from the iteration in which each closure was created. This matters because callbacks that look independent can return the same result. Capturing with use ($item) gives each closure the value from its own iteration.

What are the production tradeoffs of capturing arrays or objects in long lived closures?

The captured values remain reachable for as long as the closure remains reachable. A captured object can therefore keep the same object instance and its related object graph in memory. An array captured by value normally uses copy on write behavior, but mutation can require a separate array allocation. Capturing complete objects or large arrays is convenient, while capturing only the required small values can reduce memory retention and make dependencies clearer.

27. How do generators work in PHP?Language SpecificMedium

Question Details

Explain yield, lazy iteration, keys and values, send or return behavior where relevant, memory benefits, one-pass limitations, and appropriate use cases.

Short Interview Answer (30-60 seconds)

PHP generators let a function produce one key and value at a time with yield instead of building and returning a complete collection. Calling the function creates a Generator object, but the function body starts only when iteration begins. PHP pauses at each yield and keeps enough local state to continue later. This can greatly reduce memory when processing large or unbounded streams, but a generator is normally consumed in one pass and does not provide random access like an array.

Detailed Explanation

See the Code while reading this explanation.

A generator is useful when a program has many items but only needs to handle one item at a time. Instead of preparing every result before work can begin, the program prepares the next result only when it is requested. This can keep the program responsive and avoid holding a large collection in memory. The main questions are how each result is produced, whether the results must be read more than once, and whether the caller needs to look up any result directly.

Useful Questions to Ask the Interviewer
  1. Does the caller need one pass or several passes over the data?
  2. Must the caller access items by position at any time?
  3. Can the data source itself be read gradually?
  4. Does the caller need to send a value back into the generator?
How do generators work in PHP? diagram
How to Explain It in an Interview

A PHP function becomes a generator when its body contains yield. Calling that function returns a Generator object. PHP does not immediately run the complete function body. Execution starts when the generator is first inspected or advanced, such as through foreach, current, next, or send.

When PHP reaches yield, it exposes a value to the caller and pauses the function. Local variables and the current execution position remain available. When the caller advances the generator, PHP continues immediately after that yield. A yield can provide only a value, or it can provide an explicit key and value with yield $key => $value. Foreach receives those keys and values in the normal way.

A yielded expression can also receive data. Generator::send passes a value into the suspended yield expression and resumes execution. If the generator has not started, send first advances it to its initial yield. The generator function may use return to provide one final result. That result is not another yielded item. The caller reads it with getReturn only after the generator has finished.

The main memory benefit is that yielded items do not all need to exist in one PHP array at the same time. Memory normally depends on the generator state, the current item, and any buffers retained by the data source or caller. Therefore, a generator does not guarantee constant memory if the function, database driver, parser, or consumer still stores all results.

A generator is normally a one pass iterator. After it has advanced beyond its first yield, it cannot be rewound to the beginning. It also does not provide array style random access or a known count unless the application calculates that information separately. Use generators for large files, paged records, streamed input, pipelines, and sequences created as needed. Prefer an array when the data is small or must support repeated traversal, direct lookup, sorting, or several transformations.

Example

The example creates a generator that first yields a prompt. The caller starts it with current, then sends the step value into the suspended yield expression. The generator produces three square values with explicit keys. Each call to next resumes the function until the next yield. After the final value, the function returns a summary string. The caller reads that final return value with getReturn only after valid becomes false. The example uses manual advancement because send has already moved the generator beyond its initial yield.

Code
<?php

declare(strict_types=1);

function squareSequence(): Generator
{
    // Pause here and ask the caller to provide the step size.
    $step = yield 'request' => 'Send a positive step value';

    if (!is_int($step) || $step <= 0) {
        throw new InvalidArgumentException('Step must be a positive integer.');
    }

    // Produce one key and value at a time.
    for ($number = $step; $number <= $step * 3; $number += $step) {
        yield $number => $number * $number;
    }

    // This is the final generator result, not another yielded item.
    return 'Produced three square values';
}

$generator = squareSequence();

// current starts the generator and pauses at its first yield.
echo $generator->current() . PHP_EOL;

// send places 2 into the suspended yield expression and resumes execution.
$generator->send(2);

// Read each produced key and value without rewinding the generator.
while ($generator->valid()) {
    echo $generator->key() . ' => ' . $generator->current() . PHP_EOL;
    $generator->next();
}

// getReturn is valid after the generator has completed.
echo $generator->getReturn() . PHP_EOL;
Where it is used

Generators are useful for reading large files one record at a time, processing paged API results, walking database rows when the driver supports incremental fetching, creating data transformation pipelines, traversing large directory trees, and producing sequences that may be very large or have no fixed end. They are also useful when processing can begin before every result has been created. They are less suitable when callers need repeated iteration, direct item lookup, sorting of the complete result, or a reliable total count before processing.

Why Interviewers Ask This

Interviewers ask this question to check whether the candidate understands deferred execution, iteration state, controlled data production, and the difference between producing values one at a time and building a complete PHP array. It also tests judgment about memory usage, one pass processing, generator communication, and suitable production use cases.

Common interview mistakes

Common mistakes include saying that a generator creates all results in advance, assuming that it always uses constant memory, treating it as an array with random access, trying to iterate it again after it has been consumed, or calling getReturn before completion. Another mistake is confusing a value passed through send with a newly yielded value. send places the value into the suspended yield expression, resumes execution, and returns the next yielded value if one is reached. Developers should also remember that a lazy generator cannot reduce memory if another layer has already buffered the complete data set.

Interview tip

Explain the lifecycle in order: calling the function creates the Generator, iteration starts execution, yield exposes one key and value and pauses, advancing resumes the function, and return provides a final result through getReturn. Then state the main tradeoff clearly: lower collection memory and earlier processing in exchange for one pass behavior and no array style random access.

Interviewer may ask next
Can a PHP generator be rewound and iterated a second time?

No, a generator cannot normally be restarted after it has advanced beyond its first yield. It represents one suspended execution of one function call, so consumed values are not stored automatically for another pass. This matters when several consumers need the same data. In that case, create a new Generator by calling the generator function again, or materialize the values into an array when the extra memory cost is acceptable.

Does replacing an array with a generator always reduce production memory usage?

No, using a generator reduces memory only when the complete result is not stored elsewhere. The generator keeps its execution state and current values, but a database driver, file parser, application buffer, or consumer may still retain many or all items. The production benefit is strongest when every layer reads, transforms, and releases data gradually. The tradeoff is that lazy processing usually gives up repeated traversal, random access, and immediate knowledge of the complete result.

28. How do exceptions and the Throwable hierarchy work in modern PHP?Language SpecificMedium

Question Details

Compare Exception and Error, catch ordering, finally behavior, rethrowing with a previous exception, domain-specific exceptions, and boundary-level handling.

Short Interview Answer (30-60 seconds)

In modern PHP, every object used with throw must be an instance of a class that implements Throwable. Exception is the base class for user exceptions, while Error is the base class for many engine detected failures such as TypeError. I catch specific types before broad types, use finally only for reliable cleanup, rethrow the same object when no extra meaning is needed, and wrap a failure with a previous Throwable when translating it into a domain specific exception. I normally catch Throwable broadly only at an outer application boundary.

Detailed Explanation

See the Code while reading this explanation.

The practical goal is to handle each failure at the place that can make a useful decision. Modern PHP puts thrown failures into one family so code can catch either a specific problem or every throwable problem. Application code usually creates exceptions for operations that cannot continue. PHP itself can create errors for invalid calls, types, or other engine detected problems. Handler order decides which response runs. Cleanup must happen whether work succeeds or fails. Production code must also preserve the original cause and avoid showing private failure details to users.

Useful Questions to Ask the Interviewer
  1. Is the boundary an HTTP request, command, or queue worker?
  2. Which failures can the application recover from locally?
  3. Which failure details may be shown to the caller?
How do exceptions and the Throwable hierarchy work in modern PHP? diagram
How to Explain It in an Interview

Throwable is the common interface for objects that PHP allows to be thrown. Exception and Error both implement it. Exception is the base class for user exceptions. Error is the base class for many internal PHP errors, including TypeError and ValueError. A user class cannot implement Throwable directly. It must extend Exception or one of its subclasses.

Not every PHP warning or notice becomes a Throwable. Traditional errors still follow PHP error reporting unless code converts a supported error into ErrorException with an error handler. Some failures also happen before the relevant try block can run. For example, a syntax error in the main file cannot be caught by code in that same file.

PHP tests catch blocks from top to bottom and runs the first compatible handler. A domain specific exception must therefore appear before RuntimeException, Exception, or Throwable. PHP also permits one catch block to name several types when they need identical handling.

A finally block runs after try and any matching catch during normal exception handling. It also runs before a pending return completes. A return inside finally replaces an earlier return. If try and finally both throw, the Throwable from finally is propagated and the earlier Throwable is placed in its previous chain. Cleanup in finally should therefore be small and reliable.

Rethrowing with throw $error sends the same object onward and keeps its existing trace. Wrapping creates a new domain exception and passes the original Throwable as the previous constructor argument. Wrapping is useful when a lower level failure needs application meaning.

Catch a failure locally only when code can recover, add meaning, or clean up. Catching Throwable at an outer request, command, or worker boundary is useful for logging and producing a controlled failure result. Creating and throwing a Throwable allocates an object and records diagnostic information, including a trace, so exceptions should represent exceptional paths rather than routine branching.

Example

The example uses OrderProcessingException as a domain specific exception. The storage function throws a lower level RuntimeException. The service catches that specific type and wraps it in OrderProcessingException by passing the original Throwable as the previous argument. The finally block performs reliable cleanup whether the operation succeeds or throws. At the outer boundary, the domain exception is caught before Throwable. The expected domain failure receives a controlled response, while any other Throwable is logged and produces a general failure response. The code does not treat the failed operation as successful.

Code
<?php

declare(strict_types=1);

final class OrderProcessingException extends RuntimeException
{
}

function saveOrder(): void
{
    // Simulate a lower level storage failure.
    throw new RuntimeException('Database connection failed');
}

function processOrder(): void
{
    $resourceOpen = true;

    try {
        saveOrder();
    } catch (RuntimeException $error) {
        // Add domain meaning and preserve the original cause.
        throw new OrderProcessingException(
            'The order could not be processed',
            0,
            $error
        );
    } finally {
        // Keep cleanup small and reliable.
        if ($resourceOpen) {
            $resourceOpen = false;
            echo "Resource closed\n";
        }
    }
}

try {
    processOrder();
} catch (OrderProcessingException $error) {
    // Handle the expected domain failure first.
    echo $error->getMessage() . "\n";

    $previous = $error->getPrevious();

    if ($previous !== null) {
        echo 'Original cause: ' . $previous->getMessage() . "\n";
    }
} catch (Throwable $error) {
    // Final boundary for unexpected thrown failures.
    error_log((string) $error);
    echo "An unexpected failure occurred\n";
}
Where it is used

This behavior is used in HTTP request entry points, command line commands, queue workers, scheduled jobs, database transaction services, payment operations, and external service clients. A lower level component may throw RuntimeException or another specific exception. A service can translate it into a domain specific exception while preserving the original Throwable as the previous cause. The outer boundary can then log the complete chain, release resources, return a safe response, and ensure that the request, command, or job is still recorded as failed.

Why Interviewers Ask This

Interviewers ask this question to test whether a candidate understands how modern PHP represents thrown failures and moves control through try, catch, and finally blocks. It also tests whether the candidate can distinguish application exceptions from engine errors, order handlers correctly, preserve an original cause, design useful domain exceptions, and handle unexpected failures at a production boundary without hiding defects or reporting false success.

Common interview mistakes

Common mistakes include catching Throwable inside every method, placing Throwable or Exception before a more specific handler, and assuming that every warning or notice is an Error object. Another mistake is treating Error as a normal business outcome that should always be ignored or recovered from. Developers may also wrap an exception without passing the original Throwable as the previous cause, which loses useful diagnostic context. Returning or throwing from finally can replace an earlier return or failure. Logging a Throwable and then reporting success is also incorrect. Exceptions should not be used for common branches when a normal condition or return value expresses the result more clearly.

Interview tip

Explain the hierarchy first: Throwable is the common interface, and Exception and Error are its main branches. State that user exception classes extend Exception rather than implementing Throwable directly. Then explain specific catch ordering, reliable finally cleanup, the difference between rethrowing and wrapping, cause preservation through the previous argument, and broad Throwable handling at an outer boundary. Mention that traditional warnings and notices are not automatically Throwable objects.

Interviewer may ask next
What happens when both the try block and the finally block throw a Throwable?

The Throwable created in finally is the one propagated out of the construct. PHP places the earlier Throwable from try into the previous chain of the later Throwable. This matters because unreliable cleanup can change the visible failure and make diagnosis harder. The main tradeoff is that finally guarantees a place for cleanup, but code inside it must remain small and should avoid throwing unless the cleanup failure truly must replace the active result.

Should a service catch Throwable broadly to prevent the application from stopping?

No. A service should normally catch only the specific failures it can recover from or translate into meaningful domain exceptions. A broad Throwable catch belongs mainly at an outer request, command, or worker boundary where the application can log the failure, clean up, and return or record a failed result. Broad catches in every service add unnecessary handling work, can hide TypeError and other programming defects, and make control flow harder to understand. They do not remove the cost of creating the Throwable or recording its diagnostic trace.

29. What are the risks and semantics of PHP references?Language SpecificHard

Question Details

Explain reference sets, assignment by reference, foreach reference leakage, function parameters and returns by reference, interaction with arrays, and why references are not pointers.

Short Interview Answer (30-60 seconds)

PHP references create aliases, so multiple variable names can access the same variable content. They are not raw memory pointers. Changing the content through one alias is visible through every name in the same reference set. I use references only when shared mutation is intentional because they can hide side effects. The most common risk is a foreach variable that remains linked to the last array element until it is unset.

Detailed Explanation

See the Code while reading this explanation.

PHP normally lets code work with values without making two variable names permanently connected. A reference creates that connection. After the connection is made, changing the value through either name changes what both names see. This is sometimes useful when a function or loop must update an existing value directly. It is also risky because a change in one place can unexpectedly affect another place. Arrays make this especially important because individual elements can be connected, copied, or changed through loop variables.

Useful Questions to Ask the Interviewer
  1. Should the example modify the caller's original value?
  2. Should array element reference behavior be included?
  3. Should returning by reference be demonstrated in code?
What are the risks and semantics of PHP references? diagram
How to Explain It in an Interview

A PHP reference is an alias to the same variable content. When $second =& $first runs, both names join the same reference set. Neither variable points to the other. Both names access the same content. Assigning a new value through either name changes what every member of that set observes. PHP references do not expose memory addresses and do not support pointer arithmetic. ([php.net](https://www.php.net/manual/en/language.references.whatdo.php))

Normal assignment is different. With $copy = $array, PHP uses value semantics and normally delays copying the array storage until one side is changed. This is copy on write behavior. References should therefore not be added merely to avoid copying. They add alias tracking, may interfere with simple value reasoning, and do not provide a reliable performance or memory improvement.

Array elements can belong to reference sets. A subtle case occurs when an array containing a referenced element is copied. The copied arrays can still contain elements connected to the same referenced content. Updating that element through one array may therefore affect the other array.

In foreach ($items as &$item), the loop variable becomes an alias to each element in turn. After the loop, it remains linked to the last element. A later assignment to $item can overwrite that element. Calling unset($item) removes that variable name from the relationship and prevents this leakage. ([php.net](https://www.php.net/manual/es/control-structures.foreach.php))

A reference parameter is declared in the function signature, such as function update(array &$data): void. The caller passes a variable without adding & at the call site. The function can then change the caller's variable. Expressions and literal values cannot normally be supplied where a referenceable variable is required. ([php.net](https://www.php.net/references.pass))

A function that returns by reference places & before its name and must return a variable. The caller also uses =& to bind to the returned variable. Returning references is uncommon and should only be used when the caller truly needs an alias to existing storage, not as a performance optimization. ([php.net](https://www.php.net/references.return))

Example

The example first creates a reference set between two scalar variable names. It then shows that an array element can remain shared after the array is copied when that element belongs to a reference set. Next, it updates array elements with foreach by reference and immediately unsets the loop variable. It also demonstrates a reference parameter that changes the caller's array and a function that returns a reference to an existing array element. Every mutation is intentional and visible in the printed output.

Code
<?php

declare(strict_types=1);

// Two names join the same reference set.
$first = 10;
$second =& $first;
$second = 20;

echo "Reference set: {$first}, {$second}\n";

// An array element can itself belong to a reference set.
$sharedStatus = 'pending';
$original = ['status' => &$sharedStatus];
$copied = $original;

// Both elements still refer to the shared content.
$copied['status'] = 'approved';

echo "Original status: {$original['status']}\n";
echo "Copied status: {$copied['status']}\n";

// foreach by reference changes the original elements.
$numbers = [1, 2, 3];

foreach ($numbers as &$number) {
    $number *= 10;
}

// Remove the loop variable alias to the last element.
unset($number);

// This no longer changes the last array element.
$number = 999;

echo 'Numbers: ' . json_encode($numbers, JSON_THROW_ON_ERROR) . "\n";

// A reference parameter changes the caller's variable.
function markActive(array &$record): void
{
    $record['active'] = true;
}

$user = ['name' => 'Asha'];
markActive($user);

echo 'User: ' . json_encode($user, JSON_THROW_ON_ERROR) . "\n";

// A return by reference must return an existing variable.
function &statusSlot(array &$record): mixed
{
    return $record['status'];
}

$order = ['status' => 'new'];
$orderStatus =& statusSlot($order);
$orderStatus = 'sent';

echo "Order status: {$order['status']}\n";
Where it is used

References are used when an API intentionally changes a caller owned variable, when a foreach loop must update array elements in place, or when an interface must expose an alias to existing variable storage. They may also appear in older PHP libraries and callback APIs. In production code, references should stay inside small and clearly documented boundaries. Returning a new value is usually easier to test and understand when shared mutation is not required.

Why Interviewers Ask This

Interviewers ask this question to test whether the candidate can distinguish normal value assignment, explicit reference relationships, and object handle behavior in PHP. It also checks knowledge of reference sets, array behavior, foreach leakage, function parameter and return rules, memory tradeoffs, and production bugs caused by hidden shared mutation.

Common interview mistakes

A common mistake is saying that all PHP variables are references. Normal assignment uses value semantics, while an explicit reference relationship uses &. Another mistake is describing references as raw memory pointers. Developers may also forget to unset a foreach reference variable, add & at a function call instead of only in the parameter declaration, pass a literal or expression to a reference parameter, or return an expression instead of a variable from a function that returns by reference. A less obvious mistake is assuming that copying an array always breaks every relationship involving referenced elements.

Interview tip

Begin with the practical rule that references create aliases and should be used only for intentional shared mutation. Then contrast them with normal value assignment and object handle behavior. Mention reference sets, array element sharing, foreach leakage, parameter syntax, return syntax, and why references are not a performance shortcut.

Interviewer may ask next
What can happen when an array containing a referenced element is copied?

The referenced element can remain connected to the same variable content in both arrays. Changing that element through one array may therefore be visible through the other array. This matters because normal array copying suggests independent value behavior, but an embedded reference preserves shared mutation for that element. The safest approach is to avoid storing references in arrays unless that sharing is explicitly required.

Should a large array be passed by reference only to save memory or improve speed?

No. PHP normally uses copy on write behavior for arrays, so passing or assigning an array by value does not always create an immediate full copy. A reference is therefore not a guaranteed memory or speed optimization. It adds alias tracking and shared mutation, which can make code harder to reason about. Use a reference only when the function must intentionally replace or modify the caller's variable.

30. How does PHP's garbage collector handle cyclic references?Language SpecificHard

Question Details

Explain reference counting, why cycles require a collector, collection roots, when collection runs, observability, and implications for long-running workers.

Short Interview Answer (30-60 seconds)

PHP normally releases a value when its reference count reaches zero. A cycle is different because the values still point to each other, so their counts can stay above zero even when the application can no longer reach them. PHP records possible cycle roots and periodically scans them. Unreachable groups are then collected. In a long running worker, I would remove unnecessary references, watch memory and collector statistics, and request manual collection only at measured lifecycle boundaries.

Detailed Explanation

See the Code while reading this explanation.

This question asks how PHP removes unused values that still point to each other. Normally, PHP can remove a value when no part of the program uses it. A circular connection is harder because each value still appears to be used by another value in the same group. PHP therefore records suspicious values and checks whether anything outside the group can still reach them. This matters most in programs that remain active for a long time because unused circular groups can otherwise keep memory occupied between units of work.

Useful Questions to Ask the Interviewer
  1. Are we discussing the standard PHP 8.4 and PHP 8.5 runtime?
  2. Should I cover long running command line workers?
  3. Should I explain the functions for observing and requesting collection?
How does PHP's garbage collector handle cyclic references? diagram
How to Explain It in an Interview

PHP mainly manages values through reference counting. A managed value has a count that tracks references to it. When that count becomes zero, PHP can normally destroy the value immediately.

A cyclic reference prevents this simple rule from being enough. For example, object A can refer to object B while object B refers to object A. After the application removes both outside variables, the two objects still refer to each other. Their reference counts therefore do not reach zero, even though the application cannot reach the objects anymore.

PHP handles this case with its cyclic garbage collector. When a reference count is reduced but remains above zero, the related value can become a possible cycle root. PHP records possible roots in an internal buffer. During a collection run, PHP examines the connected candidate values and accounts for references that come from inside the candidate graph. Values that have no remaining reference from reachable application data are identified as unreachable and can be destroyed.

Automatic collection runs when the collector reaches its current root threshold. Modern PHP exposes the current threshold and number of buffered roots through gc_status(). The threshold should not be described as a permanent fixed value because modern runtimes can adjust collector capacity and thresholds.

The function gc_collect_cycles() requests a collection run and returns a count reported by the collector. In PHP 8.5, that return value no longer includes strings and resources that were collected indirectly through cycles. The functions gc_enable(), gc_disable(), and gc_enabled() control or report automatic cycle collection.

Collection reduces retained memory, but a collection run also takes processing time because PHP must inspect candidate graphs. Calling it after every small operation is usually unnecessary. Long running queue workers, command line services, and persistent application servers should clear job references, avoid accidental cycles, inspect gc_status(), watch real memory trends, and use manual collection only when measurement supports it.

Example

The example creates two Node objects that refer to each other. It then removes the two outside variables. Reference counting alone cannot immediately release the objects because each object still holds a reference to the other. gc_collect_cycles() requests a cycle collection run and returns the count reported by the current PHP runtime. The code does not assume one exact count because PHP 8.5 changed which indirectly collected strings and resources are included in that return value. gc_status() then displays collector state and timing information available in modern PHP.

Code
<?php

declare(strict_types=1);

final class Node
{
    public ?Node $other = null;
}

// Create two objects that refer to each other.
$first = new Node();
$second = new Node();
$first->other = $second;
$second->other = $first;

// Remove the references held by the application.
unset($first, $second);

// Request a scan for unreachable reference cycles.
$reportedCount = gc_collect_cycles();

echo "Collector reported count: {$reportedCount}\n";

// Inspect the current collector state and statistics.
print_r(gc_status());
Where it is used

This behavior appears in object graphs with links in both directions, such as a parent that stores children while each child stores its parent. It can also appear in event listener registries, closures that capture their owning object, dependency graphs, tree structures, and caches that connect objects to each other. It matters most in queue consumers, command line daemons, persistent application servers, test runners, and other processes that handle many tasks without exiting. Normal short PHP requests usually release request memory at the end, but avoiding unnecessary retained references is still good practice.

Why Interviewers Ask This

Interviewers ask this question to check whether the candidate understands how PHP combines reference counting with a separate cycle collector. It also tests knowledge of collection roots, collection timing, diagnostic functions, version differences, memory behavior, and the lifecycle risks of long running PHP workers.

Common interview mistakes

A common mistake is saying that unset always destroys a value immediately. unset removes one variable reference, but references inside a cycle can keep the counts above zero. Another mistake is saying that all PHP memory is managed only by the cyclic collector. Reference counting remains the main immediate cleanup mechanism, while the collector handles unreachable cycles. It is also incorrect to describe the root threshold as a permanent fixed number in modern PHP. Developers may call gc_collect_cycles() after every job without measuring its cost, or disable automatic collection and forget that unreachable cycles can accumulate. Another mistake is expecting the operating system memory number to fall immediately after collection. PHP can release values while its memory manager keeps allocated regions available for reuse by the same process.

Interview tip

Begin with the key contrast. Reference counting handles ordinary cleanup, but circular references need a separate reachability scan. Then explain possible roots, the internal root buffer, the current collection threshold, gc_collect_cycles(), gc_status(), the PHP 8.5 return value change, and why long running workers require measurement and lifecycle discipline.

Interviewer may ask next
What happens when automatic cyclic garbage collection is disabled?

Normal reference counting still works, but automatic scans for unreachable cycles stop. Values whose reference counts reach zero can still be destroyed immediately. Unreachable cycles can remain allocated because their members continue to reference one another. gc_collect_cycles() can still be called explicitly to request a collection run. This matters in a long running process because retained cycles can increase memory use. Disabling automatic collection may avoid a collection run during a sensitive section, but it transfers timing and memory responsibility to the application.

Should a long running PHP worker call gc_collect_cycles() after every job?

No, not by default. Each collection run must inspect candidate roots and connected values, so unnecessary calls add processing work. The worker should first remove job references, avoid retaining callbacks and object graphs, inspect gc_status(), and measure memory across many jobs. A manual collection call can be placed at a sensible job or batch boundary when measurements show that cyclic data is accumulating. The tradeoff is lower retained memory against added collector work and possible latency during the collection run.

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.