skip to content

In PHP 8.1 and later, how do you read or write a private property through ReflectionProperty, and what does setAccessible() still do?

level: middleimportance: should knowfreq 35%

answer

  1. 8.1: accessible by default
  2. setAccessible() is a no-op
  3. 8.5: calling it is deprecated
  4. getValue($obj) / setValue($obj, $v)
  5. readonly: initialize once, never modify

basics

~20 s

Since PHP 8.1, ReflectionProperty::getValue() and setValue() work on private and protected properties directly. setAccessible() does nothing, and PHP 8.5 deprecates calling it. Reflection can initialize an uninitialized readonly property but cannot change an initialized one.

solid answer

~40 s

Before PHP 8.1 you had to call `setAccessible(true)` or reflection threw `ReflectionException` for non-public members. PHP 8.1 made every `ReflectionProperty` and `ReflectionMethod` accessible by default, turning `setAccessible()` into a no-op, and PHP 8.5 marks it `#[\Deprecated]`, so calling it now emits a deprecation: `... is deprecated since 8.5, as it has no effect since PHP 8.1`. So you write `$p = new ReflectionProperty(Order::class, 'status'); $p->getValue($order); $p->setValue($order, 'paid');`. `setValue()` runs in the **declaring class's scope**, which is why it can initialize a readonly property left uninitialized (for example after `newInstanceWithoutConstructor()`), but once initialized it throws `Error: Cannot modify readonly property Order::$status`. Typed properties still enforce their type on write, and reading an uninitialized typed property throws unless you check `isInitialized()` first.

code

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

final class Order
{
    public function __construct(private readonly string $status) {}
}

$ref   = new ReflectionClass(Order::class);
$prop  = $ref->getProperty('status');

$order = new Order('new');
echo $prop->getValue($order), PHP_EOL;        // new  (no setAccessible needed)

try {
    $prop->setValue($order, 'paid');
} catch (Error $e) {
    echo $e->getMessage(), PHP_EOL;           // Cannot modify readonly property Order::$status
}

$blank = $ref->newInstanceWithoutConstructor();
$prop->setValue($blank, 'paid');              // allowed: first initialization
echo $prop->getValue($blank), PHP_EOL;        // paid

go deeper

for a junior

Remember that reflection can read private properties with getValue() and that setAccessible() is no longer needed.

for a middle

Explain the version history: required before 8.1, a no-op from 8.1, deprecated in 8.5, and how readonly behaves under setValue().

for a senior

Clean deprecated reflection calls out of a codebase, and push back on tests or services that mutate private state instead of using an API.

for a principal

Set a policy for where reflective access is allowed, such as hydration and framework code only, and enforce it through review or static analysis.

## Reflection and visibility PHP's `private` and `protected` modifiers restrict **normal** property access: `$order->status` from outside `Order` throws `Error: Cannot access private property`. The Reflection API is the sanctioned escape hatch used by hydrators, serializers, test helpers and ORMs to read or write state that has no public accessor. ## How it changed across versions | Version | Behaviour of `ReflectionProperty::getValue()` on a private property | |---|---| | before 8.1 | throws `ReflectionException` unless `setAccessible(true)` was called first | | 8.1 - 8.4 | works directly; `setAccessible()` is accepted and does nothing | | 8.5 | works directly; calling `setAccessible()` emits a deprecation notice | The same applies to `ReflectionMethod::invoke()` for private methods. In 8.5 the method stub carries `#[\Deprecated(since: '8.5', message: "as it has no effect since PHP 8.1")]`, so a leftover `setAccessible(true)` line produces a deprecation diagnostic on every call — worth removing in a codebase that treats deprecations as failures in CI. ## Reading and writing - `new ReflectionProperty(Order::class, 'status')` or `$refClass->getProperty('status')` gets the property. - `getValue($order)` reads it; for a static property pass `null`. - `setValue($order, 'paid')` writes it; since 8.3, static properties require `setValue(null, $value)` — the single-argument form is deprecated. - `isInitialized($order)` tells you whether a typed property has been assigned; reading an uninitialized typed property throws `Error` ("must not be accessed before initialization"). - Type checks still apply: writing a value the declared property type cannot accept throws `TypeError`. ## Readonly properties A `readonly` property can be initialized once, and only from inside its declaring class. Reflection interacts with that rule in a specific way: 1. `setValue()` executes as if it were inside the declaring class, so it **can initialize** a readonly property that is still uninitialized. This is exactly what a hydrator does after `newInstanceWithoutConstructor()`. 2. Once the property holds a value, `setValue()` throws `Error: Cannot modify readonly property Order::$status`, just like direct assignment. Reflection is **not** a way around immutability. 3. The same applies to an enum's `value` and `name`, which are readonly: `setValue(Suit::Hearts, 'x')` fails. ## Related reflective accessors - `ReflectionMethod::invoke($obj, ...$args)` and `invokeArgs()` call private and protected methods directly since 8.1, with the same no-op `setAccessible()` history. - `ReflectionProperty::getRawValue()` and `setRawValue()` (PHP 8.4) read and write the stored value while **bypassing property hooks**; they throw an `Error` for a virtual property, which has no stored value. Hydrators use them when a `set` hook would re-validate data already known to be valid. - `ReflectionClass::getProperties(ReflectionProperty::IS_PRIVATE)` filters by modifier; the constants include `IS_PUBLIC`, `IS_PROTECTED`, `IS_PRIVATE`, `IS_READONLY` and, since 8.4, `IS_PRIVATE_SET` and `IS_VIRTUAL`. - `ReflectionProperty::isReadOnly()` and `isPromoted()` let a hydrator decide in advance which properties it may initialize once. ## When to use it and when not - **Legitimate:** mapping database rows onto entities, restoring serialized state, framework internals, and tests of legacy code that cannot yet be refactored. - **Smell:** application code reaching into another class's private state; a test that sets private fields instead of using the public API couples the test to implementation details. - **Alternatives:** a named constructor or a `withStatus()` method; a closure bound to the class scope with `Closure::bind()`; or asymmetric visibility (`public private(set)`) since 8.4 when the value may be public to read. ## A quick migration checklist 1. Delete every `setAccessible(true)` call: it has been dead code since 8.1 and is deprecated in 8.5. 2. Replace single-argument `setValue($value)` on static properties with `setValue(null, $value)`. 3. Guard reads of typed properties with `isInitialized()` where objects may be partially built.

  • What does a leftover $prop->setAccessible(true) line do when run on PHP 8.5?
    It still has no effect on access, but the method carries `#[\Deprecated(since: '8.5')]`, so every call raises a deprecation: `Method ReflectionProperty::setAccessible() is deprecated since 8.5, as it has no effect since PHP 8.1`. Remove the line; nothing else changes.
  • How do you set a static private property through reflection in current PHP?
    Call `setValue(null, $value)` on its `ReflectionProperty`. Passing only the value was deprecated in PHP 8.3; the explicit `null` object argument makes the static case unambiguous. `getValue()` with no argument or `null` reads it.

saying these in an interview costs you the question

  • You must call setAccessible(true) before reading a private property in PHP 8.5.
  • setAccessible() has been deprecated since PHP 8.1.
  • Reflection can overwrite an initialized readonly property.
  • Reflection writes skip the property's declared type check.
  • Reflection cannot initialize a readonly property from outside its class.