Skip to content

RFC: restrict Opia v1 dot access to arrays - #2

Open
AyobamiH wants to merge 2 commits into
Meulah:mainfrom
AyobamiH:agent/opia-property-access-safety
Open

RFC: restrict Opia v1 dot access to arrays#2
AyobamiH wants to merge 2 commits into
Meulah:mainfrom
AyobamiH:agent/opia-property-access-safety

Conversation

@AyobamiH

Copy link
Copy Markdown

Summary

This PR proposes restricting Opia v1 dot-path traversal to arrays.

The current RFC permits reads from initialized, declared public properties while prohibiting magic access. That boundary no longer guarantees that a read is free of application-code execution. PHP 8.4 property hooks can execute arbitrary code during property access, while lazy objects may invoke an initializer or proxy factory when accessed.

Proposed contract

Dot access resolves exact string keys on arrays. Object dot access produces an unsupported-access error before the renderer performs property inspection or access.

Objects may cross registered-function boundaries, but this rule grants templates no direct property or index access to them.

Any future object-like traversal would require an explicit, versioned data-access contract that preserves Opia’s non-execution guarantee.

Rationale

Opia aims to prevent templates from executing arbitrary application code. Allowing property reads while trying to distinguish safe objects from hooked, magical, proxied, or lazy objects creates a runtime-dependent security boundary.

Array-only traversal gives version one a smaller and more deterministic contract. It is easier to implement consistently, test across supported PHP versions, and reason about during security review.

Scope

This PR changes only docs/opia-language-rfc.md. It does not introduce or modify runtime code.

Validation

The proposal was checked against the PHP documentation for property hooks and lazy-object initialization behavior.

Runtime tests were not run because this is an RFC-only change. The repository’s GitHub Actions workflow can still perform its standard PHP validation before merge.

@Amospikins

Copy link
Copy Markdown
Collaborator

Thanks for this PR. I like the direction, especially the principle that Opia dot access should remain pure data access and should not accidentally invoke application behavior through PHP object semantics.

Before merging, I want to clarify a few things so the RFC is very explicit about the boundary.

  1. Should all objects be rejected for direct dot access in Opia v1, including deliberately simple readonly view-data objects?

For example:

finalreadonlyclass UserViewData
{
publicfunction__construct(
publicstring$name,
publicstring$email,
) {}
}

Would this:

['user' => newUserViewData('Sanmi', 'sanmi@example.com')]

make this invalid?

[[ user.name ]]

My current understanding is yes, and only this should be valid:

[
'user' => [
'name' => 'Sanmi',
'email' => 'sanmi@example.com',
],
]

If that is the intention, I think the RFC should show both examples explicitly so there is no ambiguity.

  1. Is the core invariant you are proposing essentially this?

Dot access in Opia is guaranteed to be data lookup only and must never execute object behavior.

If yes, I think that wording should probably become a first-class rule in the RFC because it is a strong and useful language guarantee.

  1. How should registered template functions interact with objects?

For example, would this still be permitted:

[[ format_user_name(user) ]]

where user is an object, provided format_user_name is an explicitly registered template function?

If so, I think the RFC should make the distinction very clear:

  • Opia itself cannot traverse arbitrary objects.
  • Registered functions may receive objects because they are an explicit application-controlled execution boundary.
  1. What happens with nested data structures?

For example:

[
'user' => [
'profile' => [
'name' => 'Sanmi',
],
],
]

I assume this remains valid:

[[ user.profile.name ]]

but this would fail as soon as any intermediate value is an object:

[
'user' => [
'profile' => newProfileViewData(...),
],
]

Is that the intended behavior?

  1. Should the RFC explicitly say that array access uses exact keys and does not attempt any convenience conversion?

For example, if the array contains:

[
'first_name' => 'Sanmi',
]

then:

[[ user.firstName ]]

should fail rather than trying to infer first_name.

I would prefer strict exact-key access.

  1. Should numeric/index access follow the same principle?

For example:

[
'users' => [
['name' => 'A'],
['name' => 'B'],
],
]

Would:

[[ users[0].name ]]

remain supported?

If yes, I think the RFC should define clearly which key types are supported for indexed access.

  1. Should arrays implementing unusual PHP behavior be a concern at all, or is the rule simply based on native arrays only?

I assume native arrays are the only structure intended for dot/index traversal in v1, while things like ArrayAccess, iterators, proxies, collections, and framework model objects should all be rejected.

  1. What error should developers get when they accidentally pass an object?

Something explicit like:

Opia cannot access properties on PHP objects.
Expected array data for "user", received App\User.
Convert the value to plain template data before rendering.

I think this would make the restriction much easier to understand in practice.

  1. I also think this PR creates a useful architectural boundary for applications:
Domain objects
↓
Controller / presenter
↓
Plain template data
↓
Opia

Was that separation part of the intended design, or is this purely a security restriction?

If it is intentional, I think it is worth mentioning in the RFC because it explains why the restriction exists beyond just “objects are dangerous.”

  1. Finally, do you see object traversal returning later through an explicit contract rather than being generally enabled?

For example, something like a future:

interface OpiaData
{
publicfunctiontoOpiaData(): array;
}

or another explicitly safe projection mechanism.

I am not suggesting we add that now. I actually prefer keeping v1 array-only. I just want the RFC to make clear that future object support, if it ever exists, should be explicit and versioned rather than weakening the array-only rule later.

Overall, I am positive on the proposal. The main thing I want before merging is for the RFC to make the valid and invalid boundaries extremely explicit, especially around simple readonly DTOs, nested values, registered functions, and the developer-facing error message.

@AyobamiH

Copy link
Copy Markdown
Author

Thanks for the detailed review. Yes, your understanding matches the intended boundary, and I agree these rules should be made explicit in the RFC before merge.

  1. All PHP objects are rejected for direct dot and index access in v1, including simple readonly DTOs. [[ user.name ]] is invalid when user is a UserViewData; the equivalent native-array structure is valid. I’ll add both examples.

  2. The first-class invariant is: dot access is data lookup only and must never execute object behaviour.

  3. [[ format_user_name(user) ]] is permitted when format_user_name is explicitly registered. The object remains opaque to Opia, but registered functions may receive or return it because they are trusted, application-controlled execution boundaries. A returned object still cannot be traversed by template expressions.

  4. Nested native arrays remain traversable, so [[ user.profile.name ]] is valid. Resolution stops with an unsupported-access error before observing object state if any traversed value, such as profile, is an object.

  5. Dot access uses exact string keys. There is no camelCase-to-snake_case or other convenience conversion, so firstName does not resolve first_name.

  6. Native-array index access accepts integer or string keys. [[ users[0].name ]] remains valid, and numeric keys require bracket syntax.

  7. For dot and index traversal, native PHP arrays are the only supported structure. ArrayAccess, proxies, collections, models and Traversable objects are not traversed. This is separate from , whose support for trusted Traversable values remains an unresolved iterable-policy decision elsewhere in the RFC.

  8. Object traversal is an unsupported-access error, not an undefined-value error. The intended development diagnostic is along these lines:

Opia cannot access properties on PHP objects.
Expected native array data for "user", received App\User.
Convert the value to plain template data before rendering.

It should include the offending path and type, but never the runtime value. The stable error code will be assigned through the separate error catalogue.

  1. Security is the primary reason, but the application boundary is intentional: domain objects should be projected into plain template data before rendering. The RFC should explain that architectural benefit without prescribing a specific controller or presenter structure.

  2. Any future object-like access must be introduced through an explicit, versioned data-access contract. I would not have Opia automatically invoke a method such as toOpiaData(), because that invocation would itself execute application code. Projection should happen before rendering unless a future RFC defines an equivalently safe pre-materialized contract.

I’ll revise the RFC to make these valid and invalid boundaries explicit rather than leaving them implied.

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@AyobamiH@Amospikins