0% прочитано

PHP __debugInfo(): Safely Debugging Objects That Contain Secrets

While developing AVN Center, I added a Product Registry object that contains a secret hash. The application needs that value, but it should not accidentally appear during debugging. This article shows how __debugInfo() defines a safe diagnostic representation while preserving enough context to investigate problems.

28 вересня 2026 р. 9 хв читанняPHP

During development we constantly inspect object state. That can happen through dump(), dd(), var_dump(), a debugger, or temporary diagnostics.

With ordinary data this is easy to ignore. The situation changes when an object contains a password, key, token, or secret hash.

While developing the Product Registry module in AVN Center, I ran into exactly this case. ProductRegistrationCredentialAuthenticationRecord contains both registration credential data and a CredentialSecretHash.

The hash is required by the application, but I do not need to see its value while debugging. More importantly, I do not want it to appear accidentally in the normal diagnostic representation of the object.

A private property does not automatically mean its value is safe in every diagnostic context. The diagnostic boundary still needs to be defined explicitly.

Real Case: the Object Contains a Secret but Should Not Display It

PHP - Real Product Registry authentication record
final readonly class ProductRegistrationCredentialAuthenticationRecord{    public function __construct(        private ProductRegistrationCredential $credential,        private CredentialSecretHash $secretHash,    ) {}     public function credential(): ProductRegistrationCredential    {        return $this->credential;    }     public function secretHash(): CredentialSecretHash    {        return $this->secretHash;    }

This is not an artificial example with a fictional password. It is a real object used to work with product registration credentials.

It stores two objects: ProductRegistrationCredential and CredentialSecretHash. Both are needed by application logic, so simply removing the hash from the class is not an option.

But runtime requirements and debugging requirements are different. The application needs the hash itself. Diagnostics only need to know that it exists.

private and readonly Solve Different Problems

Both properties are private, and the class itself is readonly. That is useful, but it does not replace control over the diagnostic representation.

private controls normal property access. readonly prevents the initialized state from being changed again.

Neither rule answers a different question: which information about this object is safe to expose during debugging?

State encapsulation and a safe diagnostic representation are separate responsibilities.

What __debugInfo() Gives Us

PHP - Explicit diagnostic representation
public function __debugInfo(): array{    return [        'credential_id' =>            $this->credential                ->id()                ->toString(),         'registration_id' =>            $this->credential                ->registrationId()                ->toString(),         'environment' =>            $this->credential                ->environment()

__debugInfo() lets a class explicitly define the representation used by standard PHP object inspection during debugging.

The important part is not merely returning an array. The class no longer depends on an accidental representation of its internal state.

I decide which values are useful for diagnosis and which values should remain hidden.

Hiding Everything Would Also Be Unhelpful

I could make __debugInfo() almost empty. That would reduce exposure, but it would also make diagnostics much less useful.

When investigating product authentication, it is useful to see:

  • the credential identifier;
  • the registration identifier;
  • the environment;
  • the authentication profile;
  • the current status.

That context is enough to identify the record without exposing the secret itself.

Why I Keep secret_hash but Replace Its Value With [REDACTED]

PHP - Show presence without showing the secret
'secret_hash' => '[REDACTED]',

I could simply omit secret_hash from the result. Keeping the key with [REDACTED] provides one extra piece of useful information: the field is intentionally hidden.

That is different from a value being missing, not loaded, or accidentally lost.

During debugging I can see that the object does contain a secret component, while its actual value is deliberately withheld.

The presence of a secret can be useful for diagnostics. The secret value almost never is.

Explicitly Listing Safe Fields Is Safer

PHP - Do not expose internal state automatically
public function __debugInfo(): array{    return get_object_vars(        $this    );}

I do not want a diagnostic representation built around "show everything except a few forbidden fields".

For an object containing sensitive data, the opposite approach is safer: explicitly list only what is allowed to be shown.

A new property then cannot suddenly appear in diagnostics just because someone adds it to the class months later.

What I Do Not Want to Return From __debugInfo()

PHP - Returning internal sensitive objects
public function __debugInfo(): array{    return [        'credential' =>            $this->credential,         'secret_hash' =>            $this->secretHash,    ];}

This version provides very little control.

It returns complete internal objects and effectively pushes the responsibility further down: now their diagnostic representations must also be safe.

That is especially undesirable for CredentialSecretHash. If this class is responsible for safe inspection, the sensitive object should not be returned at all.

Having secretHash() Does Not Conflict With Redaction

PHP - Application access is separate from diagnostic output
public function secretHash(): CredentialSecretHash{    return $this->secretHash;}

At first this can look contradictory: if the class exposes secretHash(), why hide the value in __debugInfo()?

Because these access paths exist for different purposes.

secretHash() exists for application code that genuinely needs the value object. __debugInfo() exists for a person inspecting object state during debugging.

Programmatic access does not automatically mean the same value should be visible in diagnostic output.

__debugInfo() Is Not Universal Secret Protection

This boundary is important. __debugInfo() controls the diagnostic representation in PHP mechanisms that use this magic method.

It does not mean the secret is automatically protected everywhere else.

Separate care is still required for:

  • array conversion;
  • JSON;
  • serialization;
  • application logs;
  • exception context;
  • external API payloads;
  • metrics and telemetry.

A safe __debugInfo() protects one specific boundary. It should not create a false sense that every other path is protected as well.

__debugInfo() makes debugging safer, but it does not automatically make a secret safe throughout the entire system.

I Treat __debugInfo() as a Separate Diagnostic Contract

Needed at Runtime

Needed for Debugging

ProductRegistrationCredential

credential_id

CredentialSecretHash

[REDACTED]

Complete registration credential object

registration_id

Complete entity state

environment

Objects and value objects

authentication_profile and status

This distinction led me to a simple rule: a diagnostic representation should not be a copy of the object internal state.

Its job is to provide enough information to investigate a problem while exposing the minimum necessary data.

Why This Matters in Real Development

Secret exposure during debugging rarely looks like one dramatic mistake.

A developer is simply trying to understand an authentication problem, runs dump($record), copies the result into a message, adds it to a task description, or temporarily writes it into a log.

The less manual discipline required for safe diagnostics, the better.

If the class already knows that the secret should not be displayed, that rule is applied consistently whenever the relevant inspection mechanism is used.

Diagnostics Should Expose the Minimum Necessary Data

I am not trying to hide everything. Instead I ask a simple question for every field: will this help diagnose the problem?

Identifiers help locate the record. The environment shows where it is used. The authentication profile and status explain its current mode.

The actual secret hash adds almost nothing to that diagnosis, so exposing it has no practical value.

My Short Approach for Objects Containing Secrets

  • First decide which data is actually useful for diagnostics.
  • Do not expose internal object state automatically.
  • Do not include secret values in the result.
  • If the presence of a secret matters, use an explicit marker such as [REDACTED].
  • Do not rely only on private or readonly.
  • Review JSON, serialization, logs, and other output paths separately.
  • A new sensitive property should not automatically appear in diagnostics.

When __debugInfo() Is Worth Using

I would not add __debugInfo() to every DTO or entity.

It is most useful when the internal state and the safe diagnostic representation are meaningfully different.

Examples include objects containing passwords, tokens, secret keys, signatures, hashes, private connection settings, or other values that should not be copied accidentally into diagnostic output.

If an object contains only ordinary non-sensitive data, a dedicated method may provide little value.

Conclusion

For ProductRegistrationCredentialAuthenticationRecord, I did not need to change the data model or remove CredentialSecretHash.

I only needed to separate two concerns clearly: what the object must contain at runtime and what a person should see during debugging.

__debugInfo() provides a very small control point for that. I keep identifiers, environment, authentication profile, and status while replacing the sensitive value with [REDACTED].

It is a small method, but it removes one unnecessary risk from everyday development.

To me, __debugInfo() is not a way to make a secret safe. It is a way to make debugging safer.