skip to content

In PHP, how do you create an object when its class name is only known at run time, and when is ReflectionClass::newInstanceArgs() actually needed?

level: juniorimportance: should knowfreq 30%

answer

  1. new works with a string variable
  2. argument unpacking with ...$args
  3. string keys become named arguments
  4. inspect before you construct
  5. newInstanceWithoutConstructor() skips the constructor

basics

~20 s

Plain new already accepts a variable: new $class(...$args), with string keys passed as named arguments. ReflectionClass::newInstanceArgs() does the same; reflection is needed when you must inspect the class first, or build it without its constructor or lazily.

solid answer

~40 s

`new $className($a, $b)` and `new $className(...$args)` work with no reflection at all; since PHP 8.0 string keys in the unpacked array become named arguments. `ReflectionClass::newInstanceArgs(array $args = [])` does the same job, returning the object (or `null` on failure) and throwing `ReflectionException` for a non-public constructor. Reflection earns its place when you need **information before construction**: `isInstantiable()` to reject interfaces and abstract classes, `getConstructor()` (which is `null` when there is none) and its `getParameters()` to decide what to pass. It is also the only route to `newInstanceWithoutConstructor()` for hydrating objects from storage, and to `newLazyGhost()`/`newLazyProxy()`. A missing class gives `Error: Class "X" not found` with `new`, and `ReflectionException: Class "X" does not exist` with `new ReflectionClass('X')`.

code

php · 18 lines
php
<?php
declare(strict_types=1);

final class SmtpTransport
{
    public function __construct(public string $host, public int $port = 25) {}
}

$class = SmtpTransport::class;
$args  = ['port' => 587, 'host' => 'smtp.example.test'];

$a = new $class(...$args);                                  // no reflection needed
$b = (new ReflectionClass($class))->newInstanceArgs($args);  // same result

var_dump($a == $b);   // bool(true)

$raw = (new ReflectionClass($class))->newInstanceWithoutConstructor();
// $raw->host is uninitialized: the constructor never ran

go deeper

for a junior

Know that new $class(...$args) works with a class name in a variable and that the name must be fully qualified.

for a middle

Explain string-key unpacking as named arguments, what newInstanceArgs() adds, and the errors for missing or abstract classes.

for a senior

Choose reflection only where it adds information or bypasses construction, and never let request input pick the class to instantiate.

for a principal

Decide where dynamic instantiation belongs in an architecture, typically confined to a container or plugin loader with an allow-list of types.

## Dynamic instantiation without reflection PHP's `new` operator accepts a **class name held in a variable**, so most dynamic creation needs no Reflection API at all: ```php $class = App\Mail\SmtpTransport::class; $transport = new $class('smtp.example.test', 587); $transport = new $class(...$args); // positional from a list $transport = new $class(...['port' => 587, 'host' => 'smtp.example.test']); ``` The `...` operator **unpacks** an array (or any `Traversable`) into arguments. Integer keys become positional arguments; **string keys become named arguments**, a behaviour that arrived with named arguments in PHP 8.0. The name must be a fully qualified class name string — `use` imports do not apply to strings — which is why code writes `Foo::class` rather than a literal. Failure modes of plain `new`: - an unknown class throws `Error` with `Class "X" not found`; - an interface, trait, enum or abstract class throws `Error`, such as `Cannot instantiate abstract class X`; - a wrong argument throws `TypeError` or `ArgumentCountError`. ## What ReflectionClass adds `ReflectionClass` describes a class; three of its methods create instances: | Method | Behaviour | |---|---| | `newInstance(mixed ...$args)` | calls the constructor with the given arguments | | `newInstanceArgs(array $args = [])` | same, with an array; string keys are named arguments since 8.0 | | `newInstanceWithoutConstructor()` | allocates the object with default property values, constructor **not** called | `newInstanceArgs()` is documented as returning the object or `null` on failure, and throws `ReflectionException` when the constructor is not public, or when arguments are given to a class that has no constructor. An `Error` is thrown if a string key names no constructor parameter. So `newInstanceArgs($args)` and `new $class(...$args)` are equivalent for the common case. Choosing reflection only to call a constructor adds nothing. ## When reflection is actually needed 1. **Inspect first.** A plugin loader or container must know whether the class *can* be built and what it needs: `isInstantiable()` is `false` for interfaces, traits, abstract classes and classes with a non-public constructor; `getConstructor()` returns a `ReflectionMethod` or `null`; `getParameters()` lists what the constructor wants. 2. **Skip the constructor.** Hydrators and deserialisers that restore an object from a database row call `newInstanceWithoutConstructor()`, then set properties, because the constructor may validate input or have side effects. 3. **Create lazily.** Since PHP 8.4, `newLazyGhost()` and `newLazyProxy()` build objects whose initialisation is deferred. 4. **Report precisely.** `new ReflectionClass('X')` throws `ReflectionException` with `Class "X" does not exist`, which a loader can catch separately from constructor errors. ## Error behaviour side by side | Situation | `new $class(...)` | `ReflectionClass` route | |---|---|---| | Class does not exist | `Error`: `Class "X" not found` | `ReflectionException`: `Class "X" does not exist` | | Interface or abstract class | `Error`: `Cannot instantiate ...` | `isInstantiable()` is `false` beforehand | | Non-public constructor | `Error` from outside the class | `ReflectionException` from `newInstanceArgs()` | | Unknown named argument | `Error` | `Error` | | Wrong argument type | `TypeError` | `TypeError` | The useful difference is not the exception class but the ability to **ask first**: a loader can check `isInstantiable()` and inspect the constructor's parameters, then report "plugin X needs a Y" instead of surfacing a raw `ArgumentCountError` to the user. ## Checking a name before using it - `class_exists($class)` triggers the autoloader and returns a `bool` — enough for a quick guard. - `is_subclass_of($class, Plugin::class)` or `is_a($class, Plugin::class, true)` checks the type without creating an object. - Never build class names from raw request input: combined with autoloading, that lets a caller choose which class your code instantiates. ## Summary - Everyday dynamic creation: `new $class(...$args)`. - Reflection: inspecting constructors, bypassing them, or laziness.

  • Why would a PHP hydrator use newInstanceWithoutConstructor() instead of new?
    The object already existed once and its state is being restored from storage, so running the constructor again could re-validate, generate new IDs or trigger side effects. The hydrator allocates the object without the constructor and then writes the stored property values, typically through `ReflectionProperty::setValue()`.
  • What does ReflectionClass::isInstantiable() return for a class with a private constructor?
    `false`. It is `true` only for concrete classes whose constructor, if any, is public. Such a class, a singleton for instance, cannot be built with `new` from outside or with `newInstanceArgs()`, which throws `ReflectionException` for a non-public constructor.

saying these in an interview costs you the question

  • You need ReflectionClass to instantiate a class whose name is in a variable.
  • String keys in an unpacked argument array are ignored.
  • use imports apply to class names stored in strings.
  • newInstanceWithoutConstructor() still runs the constructor, just without arguments.
  • new with an unknown class name returns null instead of throwing.