Accessing private properties in PHP

February 20, 2025

Object-Oriented Programming (OOP) is all about organizing your code into neat little packages called objects. These objects contain both data (properties) and the code that works with that data (methods). A core idea in OOP is encapsulation, which basically means bundling everything up together and controlling access to it. Think of it like a well-organized toolbox – you know what's inside, but you access the tools through specific openings. As Wikipedia puts it, encapsulation "refers to the bundling of data with the methods that operate on that data, or the restricting of direct access to some of an object's components."

In PHP, we use access modifiersprivate, protected, and public – to implement encapsulation. private properties are the most tightly controlled; they're meant to be used only within the class that defines them. This is crucial for keeping your code organized and preventing unintended side effects. But, sometimes, you need to peek inside. Maybe you're writing tests, debugging a tricky issue, or working with some old code.

Most online guides show you how to get at private properties in child classes, but they often skip over the parent class case. This article will cover both, and reveal the secrets!

The methods we'll explore come from Serhii Korniushov's reflection-utils library. If you want to follow along, install it via Composer:

composer require serhiikorniushov/reflection-utils

Let's Test It Out

Following the principles of Test-Driven Development (TDD), we'll start with the tests. This helps us define exactly what we expect to happen. We'll use the same tests for all the different access methods. Here's the basic class structure we'll be working with (Class A inherits from Class B):

class A extends B
{
    private string $privateProperty;
    private string $privatePropertyA;
    protected string $protectedPropertyA;

    public function initValuesA()
    {
        $this->privateProperty = 'class A value';
        $this->privatePropertyA = 'class A value';
        $this->protectedPropertyA = 'class A protected value';
    }

    public function getPrivatePropertyA(): string
    {
        return $this->privatePropertyA;
    }

    public function getPrivatePropertyClassA(): string
    {
        return $this->privateProperty;
    }
}

class B
{
    private string $privateProperty;
    private string $privatePropertyB;
    protected string $protectedPropertyB;

    public function initValuesB()
    {
        $this->privateProperty = 'class B value';
        $this->privatePropertyB = 'class B value';
        $this->protectedPropertyB = 'class B protected value';
    }

    public function getPrivatePropertyB(): string
    {
        return $this->privatePropertyB;
    }

    public function getProtectedPropertyB(): string
    {
        return $this->protectedPropertyB;
    }

    public function getPrivatePropertyClassB(): string
    {
        return $this->privateProperty;
    }
}

You can find the complete test code on GitHub.

Reading

This test checks if we can correctly read the values of private and protected properties:

/**
 * @param class-string $implementation  The class implementing the read/write methods.
 * @param class-string|null $class       The parent class (or null for the current class).
 * @param string $property            The name of the property to access.
 * @param string|null $expectedValue   The expected value of the property.
 * @return void
 */
#[DataProvider('propertiesReadProvider')]
public function testRead(string $implementation, ?string $class, string $property, ?string $expectedValue): void
{
    $a = new A;
    $a->initValuesA();
    $a->initValuesB();
    $result = $implementation::read($a, $property, $class);
    self::assertEquals($expectedValue, $result);
}

public static function propertiesReadProvider(): \Generator
{
    $implementations = [MangledObjectReflection::class, ...self::IMPLEMENTATIONS];
    foreach ($implementations as $implementation) {
        yield [$implementation, A::class, 'privateProperty', 'class A value'];
        yield [$implementation, B::class, 'privateProperty', 'class B value'];
        yield [$implementation, null, 'protectedPropertyB', 'class B protected value'];
        yield [$implementation, null, 'property does not exists', null];
    }
}

Writing

This test verifies that we can modify the values of private and protected properties:

/**
 * @param class-string $implementation  The class implementing the read/write methods.
 * @param class-string|null $class       The parent class (or null for the current class).
 * @param string $property            The name of the property to access.
 * @param bool $expectedResult        Whether the write operation should succeed.
 * @param string|null $expectedValue   The expected new value of the property.
 * @param string|null $getter          The name of a getter method to verify the change.
 * @return void
 */
#[DataProvider('propertiesWriteProvider')]
public function testWrite(string $implementation, ?string $class, string $property, bool $expectedResult, ?string $expectedValue, ?string $getter): void
{
    $a = new A;
    $a->initValuesA();
    $a->initValuesB();
    $result = $implementation::write($a, $property, $expectedValue, $class);
    self::assertEquals($expectedResult, $result);
    if ($getter) {
        self::assertEquals($expectedValue, $a->$getter());
    }
}

public static function propertiesWriteProvider(): \Generator
{
    foreach (self::IMPLEMENTATIONS as $implementation) {
        yield [$implementation, A::class, 'privateProperty', true, 'class A value changed', 'getPrivatePropertyClassA'];
        yield [$implementation, B::class, 'privateProperty', true, 'class B value changed', 'getPrivatePropertyClassB'];
        yield [$implementation, null, 'protectedPropertyB', true, 'class B protected value changed', 'getProtectedPropertyB'];
        yield [$implementation, null, 'property does not exists', false, null, null];
    }
}

Let's explore the different ways to get at those private properties.

Reflection

PHP's Reflection Class API provides a powerful way to inspect and modify class properties at runtime. According to the PHP manual, "The Reflection API is a set of classes that let you introspect classes, interfaces, functions, methods and extensions."

Reflection Class

This is a general-purpose reflection tool.

Reading

// Child class (A)
$value = ClassReflection::read($stub, 'privatePropertyA');

// Parent class (B) -  Notice the third argument!
$value = ClassReflection::read($stub, 'privatePropertyB', B::class);

To access a parent class property, we need to supply a read method third argument with the Full Qualified Class Name of the parent class.

Here's the code that makes it work:

/**
 * Reads a property from an object.
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return mixed              The property value, or null if it doesn't exist.
 * @throws \ReflectionException If the property can't be accessed.
 */
public static function read(object $object, string $property, ?string $class = null): mixed
{
    $reflectionClass = new \ReflectionClass($class ?? $object);
    try {
        $reflectionProperty = $reflectionClass->getProperty($property);
    } catch (\ReflectionException $e) {
        return null;
    }
    $reflectionProperty->setAccessible(true);

    return $reflectionProperty->getValue($object);
}

Writing

// Child class (A)
ClassReflection::write($stub, 'privatePropertyA', 'new value');

// Parent class (B) - Again, note the fourth argument.
ClassReflection::write($stub, 'privatePropertyB', 'new value', B::class);

And the code:

/**
 * Writes to a property of an object.
 *
 * @param object $object      The object to write to.
 * @param string $property    The name of the property.
 * @param mixed $value        The new value.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return bool              True if successful, false otherwise.
 * @throws \ReflectionException If the property can't be accessed.
 */
public static function write(object $object, string $property, mixed $value, ?string $class = null): bool
{
    $reflectionClass = new \ReflectionClass($class ?? $object);
    try {
        $reflectionProperty = $reflectionClass->getProperty($property);
    } catch (\ReflectionException $e) {
        return false;
    }
    $reflectionProperty->setAccessible(true);
    $reflectionProperty->setValue($object, $value);

    return true;
}

Reflection Property

This based on reflecting properties. ReflectionProperty Class

Reading

// Child class (A)
$value = PropertyReflection::read($stub, 'privatePropertyA');

// Parent class (B). Note the third argument.
$value = PropertyReflection::read($stub, 'privatePropertyB', B::class);

To access a parent class property, we need to supply a read method third argument with the Full Qualified Class Name of the parent class.

/**
 * Reads a property from an object.
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return mixed              The property value, or null if it doesn't exist.
 */
public static function read(object $object, string $property, ?string $class = null): mixed
{
    try {
        $reflectionProperty = new \ReflectionProperty($class ?? $object, $property);
    } catch (\ReflectionException $e) {
        return null;
    }
    $reflectionProperty->setAccessible(true);

    return $reflectionProperty->getValue($object);
}

Writing

// Child class (A)
PropertyReflection::write($stub, 'privatePropertyA', 'new value');

// Parent class (B). Note the fourth argument.
PropertyReflection::write($stub, 'privatePropertyB', 'new value', B::class);
/**
 * Writes to a property of an object.
 *
 * @param object $object      The object to write to.
 * @param string $property    The name of the property.
 * @param mixed $value        The new value.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return bool              True if successful, false otherwise.
 */
public static function write(object $object, string $property, mixed $value, ?string $class = null): bool
{
    try {
        $reflectionProperty = new \ReflectionProperty($class ?? $object, $property);
    } catch (\ReflectionException $e) {
        return false;
    }
    $reflectionProperty->setAccessible(true);
    $reflectionProperty->setValue($object, $value);

    return true;
}

The key to both of these is $reflectionProperty->setAccessible(true). This line temporarily disables the access restrictions, allowing us to read or write the private property.

Closure

PHP's Closures are anonymous functions that can be bound to a specific object scope. This is a clever way to get around access restrictions.

Reading

// Child class (A)
$value = ClosureReflection::read($stub, 'privatePropertyA');

// Parent class (B). Note the third argument.
$value = ClosureReflection::read($stub, 'privatePropertyB', B::class);
/**
 * Reads a property from an object using a closure.
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return mixed              The property value.
 */
public static function read(object $object, string $property, ?string $class = null): mixed
{
    $closure = \Closure::bind(static function (object $object, string $property) {
        return $object->$property;
    }, null, $class ?? $object);

    return $closure($object, $property);
}

Writing

// Child class (A)
ClosureReflection::write($stub, 'privatePropertyA', 'new value');

// Parent class (B). Note the fourth argument.
ClosureReflection::write($stub, 'privatePropertyB', 'new value', B::class);
/**
 * Writes to a property of an object using a closure.
 *
 * @param object $object      The object to write to.
 * @param string $property    The name of the property.
 * @param mixed $value        The new value.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return bool              True if successful, false otherwise.
 */
public static function write(object $object, string $property, mixed $value, ?string $class = null): bool
{
    $closure = \Closure::bind(static function (object $object, string $property, ?string $value) {
        if (property_exists($object, $property)) {
            $object->$property = $value;
            return true;
        }
        return false;
    }, null, $class ?? $object);

    return $closure($object, $property, $value);
}

We're creating a closure that looks like it's inside the class (because of \Closure::bind). The null for the second argument to bind means we're not binding to a specific object instance, but the third argument, $class ?? $object, sets the scope of the closure.

Closure by Reference

This is a variation on the closure approach, but it uses references to modify the property directly.

Reading

// Child class (A)
$value = ClosureReferenceReflection::read($stub, 'privatePropertyA');

// Parent class (B)
$value = ClosureReferenceReflection::read($stub, 'privatePropertyB', B::class);
/**
 * Reads a property from an object using a closure by reference.
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return mixed              The property value.
 */
public static function read(object $object, string $property, ?string $class = null): mixed
{
    $closure = \Closure::bind(static function &(object $object, string $property) {
        return $object->$property;
    }, null, $class ?? $object);

    $val = &$closure($object, $property);

    return $val;
}

Writing

// Child class (A)
ClosureReferenceReflection::write($stub, 'privatePropertyA', 'new value');

// Parent class (B)
ClosureReferenceReflection::write($stub, 'privatePropertyB', 'new value', B::class);
/**
 * Writes to a property of an object using a closure by reference.
 *
 * @param object $object      The object to write to.
 * @param string $property    The name of the property.
 * @param mixed $value        The new value.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return bool              True if successful, false otherwise.
 */
public static function write(object $object, string $property, mixed $value, ?string $class = null): bool
{
    if (!property_exists($object, $property)) {
        return false;
    }
    $closure = \Closure::bind(static function &(object $object, string $property) {
        return $object->$property;
    }, null, $class ?? $object);

    $val = &$closure($object, $property);
    $val = $value;

    return true;
}

The & in function &(...) and $val = &$closure(...) creates a reference, so any changes we make to $val are reflected in the original property.

The Array Cast

PHP lets you cast objects to arrays, and the way it does this exposes private properties in a predictable way. It's even documented and has tests to ensure it keeps working.

Reading

// Child class (A)
$value = ArrayReflection::read($stub, 'privatePropertyA');

// Parent class (B)
$value = ArrayReflection::read($stub, 'privatePropertyB', B::class);
/**
 * Reads a property from an object by casting it to an array.
 * Works faster when passed 3rd argument ($class) as class name of object due to omitting get_class() call.
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return mixed              The property value, or null if it doesn't exist.
 */
public static function read(object $object, string $property, ?string $class = null): mixed
{
    $obj = (array)$object;
    return $obj["\0" . ($class ?? get_class($object)) . "\0" . $property] ?? $obj["\0*\0" . $property] ?? null;
}

Writing

//Child Class (A)
ArrayReflection::write($stub, 'privatePropertyA', 'new value');

//Parent Class (B)
ArrayReflection::write($stub, 'privatePropertyB', 'new value', B::class);
/**
 * Write private or protected property value from object. If property is not exists, return null.
 * Works faster when passed 3rd argument ($class) as class name of object due to omitting get_class() call.
 * @param object $object
 * @param string $property
 * @param mixed $value
 * @param class-string|null $class parent class Full Qualified Class Name
 * @return mixed
 */
public static function write(object $object, string $property, mixed $value, ?string $class = null): bool
{
    $encodedProperties = [
        "\0" . ($class ?? get_class($object)) . "\0" . $property, // for private properties
        "\0*\0" . $property, // for protected properties
        $value, // new property value
    ];
    $success = false;
    array_walk($object, static function (mixed &$value, string $key, array $encodedProperties) use (&$success) {
        if ($key === $encodedProperties[0] || $key === $encodedProperties[1]) {
            $value = $encodedProperties[2];
            $success = true;
        }
    }, $encodedProperties);

    return $success;
}

When an object is cast to an array, private properties become array keys with the format \0ClassName\0propertyName. Protected properties use \0*\0propertyName. We use this knowledge to directly access the array elements. We use array_walk to change the required property.

Serialize

This approach leverages PHP's serialization mechanism to access private properties through string manipulation of the serialized object representation.

Reading

// Child class (A)
$value = SerializeReflection::read($stub, 'privatePropertyA');

// Parent class (B)
$value = SerializeReflection::read($stub, 'privatePropertyB', B::class);
/**
 * Reads a property from an object by serializing and unserializing it.
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return string|null        The property value, or null if it doesn't exist.
 */
public static function read(object $object, string $property, ?string $class = null): ?string
{
    $serialized = serialize($object);

    $propertyNeedle = "\0" . ($class ?? get_class($object)) . "\0" . $property . '";s:'; // for private properties
    $propertyPosition = strpos($serialized, $propertyNeedle);
    if ($propertyPosition === false) {
        $propertyNeedle = "\0*\0" . $property . '";s:'; // for protected properties
        $propertyPosition = strpos($serialized, $propertyNeedle);
    }
    if ($propertyPosition !== false) {
        $propertyPosition += strlen($propertyNeedle);
        $propertyLengthPosition = strpos($serialized, ':', $propertyPosition);
        $extractedPropertyLength = $propertyLengthPosition - $propertyPosition;
        $propertyLength = substr($serialized, $propertyPosition, $extractedPropertyLength);

        return substr($serialized, $propertyPosition + $extractedPropertyLength + 2, $propertyLength);
    }

    return null;
}

Writing

// Child class (A)
SerializeReflection::write($stub, 'privatePropertyA', 'new value');

// Parent class (B)
SerializeReflection::write($stub, 'privatePropertyB', 'new value', B::class);
/**
 * Writes to a property of an object by serializing, modifying, and unserializing it.
 *
 * @param object $object      The object to write to.
 * @param string $property    The name of the property.
 * @param mixed $value        The new value.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return bool              True if successful, false otherwise.
 */
public static function write(object &$object, string $property, mixed $value, ?string $class = null): bool
{
    $serialized = serialize($object);

    $propertyNeedle = "\0" . ($class ?? get_class($object)) . "\0" . $property . '";s:'; // for private properties
    $propertyPosition = strpos($serialized, $propertyNeedle);
    if ($propertyPosition === false) {
        $propertyNeedle = "\0*\0" . $property . '";s:'; // for protected properties
        $propertyPosition = strpos($serialized, $propertyNeedle);
    }
    if ($propertyPosition !== false) {
        $propertyPosition += strlen($propertyNeedle);
        $propertyLengthPosition = strpos($serialized, ':', $propertyPosition);
        $extractedPropertyLength = $propertyLengthPosition - $propertyPosition;
        $propertyLength = substr($serialized, $propertyPosition, $extractedPropertyLength);

        $serialized = substr_replace($serialized, $value, $propertyPosition + $extractedPropertyLength + 2, $propertyLength);
        $serialized = substr_replace($serialized, strlen($value), $propertyPosition, $extractedPropertyLength);
        $object = unserialize($serialized, [$object]);

        return true;
    }

    return false;
}

This method searches for the serialized representation of the property within the serialized string and extracts or replaces the value.

Mangled Object

PHP 7.4 introduced get_mangled_object_vars, which provides a more direct way to get all properties, including private ones, in a format similar to the array cast.

Reading

// Child class (A)
$value = MangledObjectReflection::read($stub, 'privatePropertyA');

// Parent class (B)
$value = MangledObjectReflection::read($stub, 'privatePropertyB', B::class);
/**
 * Reads a property from an object using get_mangled_object_vars().
 *
 * @param object $object      The object to read from.
 * @param string $property    The name of the property.
 * @param class-string|null $class The parent class (or null for the current class).
 * @return mixed              The property value, or null if it doesn't exist.
 */
public static function read(object $object, string $property, ?string $class = null): mixed
{
    $obj = get_mangled_object_vars($object);
    return $obj["\0" . ($class ?? get_class($object)) . "\0" . $property] ?? $obj["\0*\0" . $property] ?? null;
}

This is essentially a built-in version of the array cast trick, but it's more reliable because it's an official PHP function. There is no write method for it, because get_mangled_object_vars returns property values, but not links to them.

Benchmark Performance

Let's see how these methods stack up in terms of speed. You can find the full benchmark code on GitHub.

PHP:	8.4.3
Host:	Darwin mac.lan 24.2.0 Darwin Kernel Version 24.2.0: Fri Dec  6 19:03:40 PST 2024; root:xnu-11215.61.5~2/RELEASE_ARM64_T6041 arm64
Iterations:	1000000

Read Operations

Accessing MethodProperty ScopeTime (Million Iterations)
ArrayReflectionParent Class86
PropertyReflectionParent Class89
PropertyReflectionChild Class90
ArrayReflectionChild Class104
ClosureReflectionChild Class105
ClosureReflectionParent Class105
ClosureReferenceReflectionChild Class107
ClosureReferenceReflectionParent Class108
MangledObjectReflectionParent Class112
ClassReflectionChild Class116
ClassReflectionParent Class118
MangledObjectReflectionChild Class127
SerializeReflectionParent Class306
SerializeReflectionChild Class346

The array cast (ArrayReflection) and PropertyReflection are the clear winners for reading. Serialization is significantly slower.

Write Operations

Accessing MethodProperty ScopeTime (Million Iterations)
ClosureReferenceReflectionParent Class37
PropertyReflectionChild Class100
PropertyReflectionParent Class102
ClassReflectionChild Class127
ClassReflectionParent Class129
ClosureReferenceReflectionChild Class131
ClosureReflectionChild Class134
ClosureReflectionParent Class135
ArrayReflectionParent Class426
ArrayReflectionChild Class447
SerializeReflectionParent Class717
SerializeReflectionChild Class769

ClosureReferenceReflection is blazing fast for writing, likely due to the direct reference manipulation. PropertyReflection come second. Serialization, again, is the slowest.

Notice that accessing child class properties is sometimes slightly slower than parent class properties. This is because the code often needs to determine the class name using get_class(). If you provide the class name explicitly (as the third argument), this overhead is eliminated.

The benchmarks were run using Serhii Korniushov's MicroBench tool, which is a handy way to measure the performance of small snippets of PHP code. You can install it with:

composer require --dev serhiikorniushov/micro-bench

Best Practices

  • Use Sparingly: These techniques are powerful, but they should be used with care. They break encapsulation, which can make your code harder to maintain and understand.
  • Testing and Debugging Only: The primary use cases for accessing private properties are in testing and debugging.
  • Performance Matters: Choose the method that best balances readability and performance for your specific situation.
  • Don't Break Encapsulation in Production (Usually): Avoid using these tricks to bypass proper object design in your production code. If you find yourself needing to access private properties regularly, it's a sign that you should refactor your code.
  • Consider adding public getter and setter methods.
  • Rethink the relationship between your classes.

Conclusion

PHP offers a surprising number of ways to get at private properties, from the official Reflection API to quirky tricks like array casting and serialization. While these methods can be incredibly useful in certain situations (like testing or dealing with legacy systems), remember the core principles of OOP. Encapsulation is there for a reason.

For most everyday coding, stick to well-defined public interfaces and proper getter/setter methods. Save these "backdoor" techniques for when you really need them, and always prioritize clean, maintainable code.