# [RFC][Discussion] Add #[NoSerialize] attribute for excluding properties or classes from serialization

- Thread: https://externals.io/message/128988
- Original archive: https://news-web.php.net/php.internals/128988
- Started by Dmytro Kulyk on 2025-10-27T22:59:38+00:00
- Messages: 17
- RFC: Allow __sleep() to return null or no value to delegate to default serialization (Draft) — https://externals.io/rfc/sleep_without_return_array.md
- Reply by email to internals@lists.php.net
- More for agents: search with https://externals.io/search.md?q={query} · RFCs at https://externals.io/rfc.md · JSON API at https://externals.io/api · documentation at https://externals.io/llms.txt

---

## #128988 — Dmytro Kulyk — 2025-10-27T22:59:38+00:00

URL: https://externals.io/message/128988#128988

Hello internals,

I’d like to open a discussion about a new proposal introducing the
#[NoSerialize] attribute, which allows developers to explicitly
exclude properties — or even entire classes — from native PHP
serialization.

RFC: https://wiki.php.net/rfc/no_serialize_attribute
Implementation: https://github.com/php/php-src/pull/20074

The primary goal of my series of proposals is to gradually reduce the
responsibilities of __sleep(), which today is used both to filter
which properties should be serialized, and to perform pre-serialization
logic.

Before proposing significant behavioral changes to __sleep(), we first
need to introduce clear, attribute-based alternatives for its common
use cases.

This RFC represents one of these preparatory steps.

It introduces an explicit, engine-level mechanism (#[NoSerialize]) to
declare which properties or classes should be skipped during
serialization — without requiring __sleep() to handle that
responsibility.

Please also consider the related RFCs (to be discussed separately):
- https://wiki.php.net/rfc/sleep_without_return_array
- https://wiki.php.net/rfc/not_serializable_attribute

Together, these proposals aim to clarify and simplify PHP’s object
serialization model by moving away from overloaded magic methods
toward more explicit, declarative mechanisms.

Best regards,
Dmytro Kulyk

---

## #128990 — Ilija Tovilo — 2025-10-28T15:43:54+00:00

URL: https://externals.io/message/128988#128990 · In reply to: #128988

Hi Dmytro


>
> I’d like to open a discussion about a new proposal introducing the
> #[NoSerialize] attribute, which allows developers to explicitly
> exclude properties — or even entire classes — from native PHP
> serialization.
>
> RFC: https://wiki.php.net/rfc/no_serialize_attribute
> Implementation: https://github.com/php/php-src/pull/20074

Thank you for your proposal. I have a few comments.

> When applied to a class, instances will be serialized as NULL.

I don't understand the rationale for diverging from the existing
@not-serializable behavior of internal classes, which throw when
attempted to be serialized (e.g. Random\Engine\Secure). I see there's
also a separate RFC to introduce that behavior:
https://wiki.php.net/rfc/not_serializable_attribute I don't think
there's a need for both of these attributes. To demonstrate, how would
we even decide whether an internal class like PDO should get the
#[NoSerialize] or #[NotSerializable] attribute? Informing the user of
potentially incorrect serialization is the prudent option, and if they
would like to skip the serialization of a property containing a PDO
object they can simply mark it as #[NoSerialize].

The RFC says:

> This approach ensures that data structures containing such objects (for example, arrays, collections, or parent objects) remain valid and can be safely unserialized without errors, while clearly indicating that the value was intentionally omitted.

But is replacing unserializable objects with `NULL` in nested arrays
really the safe choice? In these cases, I feel like a custom
serializer that consciously replaces the object is warranted. This
might also cause problems for typed properties:

    #[NoSerialize]
    class PDO {}

    class Foo {
        /* Will be happily serialized as NULL, but can't be restored because the
         * property is not nullable. */
        public PDO $connection;
    }

To summarize, I'd prefer if #[NoSerialize] on classes would cause
 `serialize()` to throw, like we already do for @not-serializable.

> Class-level #[NoSerialize] is inherited by child classes unless explicitly overridden.

Can you clarify how this would work? How can you override this
attribute by omission?

    #[NoSerialize]
    class Foo {}

    /* What do I add here to remove #[NoSerialize]? */
    class Bar extends Foo {}

> Out of scope: JSON (json_encode(), JsonSerializable) and `var_export()` remain unaffected.

Any future attempt to exclude properties marked as #[NoSerialize] from
 `json_encode()` would be backwards incompatible after this RFC has been
implemented. To avoid this BC break, `json_encode()` would need a
separate attribute (e.g. #[NoJsonEncode]). That sounds reasonable, but
should be spelled out in the RFC.

> Invalid Targets & Compile-Time Diagnostics

What's the rationale for warning for some, but erroring for others?

Ilija

---

## #128996 — Dmytro Kulyk — 2025-10-29T14:35:40+00:00

URL: https://externals.io/message/128988#128996 · In reply to: #128990

Thanks for the feedback!


>
> Hi Dmytro
>
> 
> > […]
>
> Thank you for your proposal. I have a few comments.
>
> > […]
>
> I don't understand the rationale for diverging from the existing
> @not-serializable behavior of internal classes, which throw when
> attempted to be serialized (e.g. Random\Engine\Secure). I see there's
> also a separate RFC to introduce that behavior:
> https://wiki.php.net/rfc/not_serializable_attribute I don't think
> there's a need for both of these attributes. To demonstrate, how would
> we even decide whether an internal class like PDO should get the
> #[NoSerialize] or #[NotSerializable] attribute? Informing the user of
> potentially incorrect serialization is the prudent option, and if they
> would like to skip the serialization of a property containing a PDO
> object they can simply mark it as #[NoSerialize].
>
> The RFC says:
>
> > […]
>
> But is replacing unserializable objects with `NULL` in nested arrays
> really the safe choice? In these cases, I feel like a custom
> serializer that consciously replaces the object is warranted. This
> might also cause problems for typed properties:
>
>     #[NoSerialize]
>     class PDO {}
>
>     class Foo {
>         /* Will be happily serialized as NULL, but can't be restored because the
>          * property is not nullable. */
>         public PDO $connection;
>     }
>
> To summarize, I'd prefer if #[NoSerialize] on classes would cause
> `serialize()` to throw, like we already do for @not-serializable.

You’re absolutely right that the current internal behavior (e.g.
Random\Engine\Secure, CurlHandle, etc.) throws when serialization is
attempted, and that the proposed #[NotSerializable] RFC formalizes
this for userland classes.
The intent behind #[NoSerialize] is not to diverge from that model,
but to provide a softer, complementary mechanism targeting a different
use case — non-critical, transient classes that can safely degrade to
 `NULL` without breaking surrounding data structures.
In this example, PDO (and other resource-backed classes) should indeed
use #[NotSerializable], because serialization is semantically invalid
and must throw.
#[NoSerialize], on the other hand, would apply to wrappers,
containers, or domain objects where serialization of the rest of the
structure should continue even if one field or nested object cannot be
meaningfully serialized.

>
> > […]
>
> Can you clarify how this would work? How can you override this
> attribute by omission?
>
>     #[NoSerialize]
>     class Foo {}
>
>     /* What do I add here to remove #[NoSerialize]? */
>     class Bar extends Foo {}

Class-level #[NoSerialize] is transparently inherited by child classes.
There’s currently no way to override or cancel this behavior in
descendants; the attribute remains effective throughout the
inheritance chain.


As an alternative, #[NotSerializable] could be extended to
#[NotSerializable(bool $soft = false)], which would provide the same
behavior while keeping it in a separate, more consistent attribute —
avoiding the double semantics currently implied by #[NoSerialize].

> > […]
>
> Any future attempt to exclude properties marked as #[NoSerialize] from
> `json_encode()` would be backwards incompatible after this RFC has been
> implemented. To avoid this BC break, `json_encode()` would need a
> separate attribute (e.g. #[NoJsonEncode]). That sounds reasonable, but
> should be spelled out in the RFC.

It has been added to section 2.5

> > […]
>
> What's the rationale for warning for some, but erroring for others?

Initially, the idea was to make all of these cases emit warnings, but
I didn’t figure out how to implement that consistently within the
attribute validators.
At this point, I don’t see any practical reason to allow the attribute
on unsupported targets, so all such cases will be changed to
compile-time errors instead of warnings.

Dmytro

---

## #128998 — tim@bastelstu.be — 2025-10-29T15:34:53+00:00

URL: https://externals.io/message/128988#128998 · In reply to: #128996

Hi

Thank you for your RFC. It is really well-written and nicely explains 
the various (edge) cases. I've only read through the `#[\NoSerialize]` 
one, since it is the topic of this RFC discussion. I agree with Ilija on 
all points.

Am 2025-10-29 15:35, schrieb Dmytro Kulyk:
> #[NoSerialize], on the other hand, would apply to wrappers,
> containers, or domain objects where serialization of the rest of the
> structure should continue even if one field or nested object cannot be
> meaningfully serialized.

But even for wrappers you can't guarantee that the wrapper is stored in 
a nullable property. When applying the attribute to a field, omitting 
the field makes sense, since the author of the property is the same 
person who is capable of adding the attribute and thus is in full 
control. This is different for classes where the author of the class 
doesn't know how and where the class is being used. I thus agree with 
Ilija that this should throw to clearly indicate that the user of the 
class is doing something incorrect and to not silently corrupt data. If 
an author of a class needs more fine-grained control, they can either 
add the attribute to individual properties themselves - or implement 
`__serialize()`.

> > […]
> 
> Class-level #[NoSerialize] is transparently inherited by child classes.
> There’s currently no way to override or cancel this behavior in
> descendants; the attribute remains effective throughout the
> inheritance chain.

Indeed, however the “unless explicitly overridden” bit in the RFC text 
implies that this is possible.

> As an alternative, #[NotSerializable] could be extended to
> #[NotSerializable(bool $soft = false)], which would provide the same
> behavior while keeping it in a separate, more consistent attribute —
> avoiding the double semantics currently implied by #[NoSerialize].

I don't think that sub classes should be able to make a non-serializable 
parent class serializable, since they clearly won't be able to correctly 
restore the state of the parent class. In fact I would consider this an 
improvement over the existing serialization hooks where the child class 
can simply skip calling the parent's serializer and thus create a 
situation that the parent class does not expect to occur.

> Initially, the idea was to make all of these cases emit warnings, but
> I didn’t figure out how to implement that consistently within the
> attribute validators.
> At this point, I don’t see any practical reason to allow the attribute
> on unsupported targets, so all such cases will be changed to
> compile-time errors instead of warnings.

Yes, every clear error that is detectable at compile-time should be a 
hard error to make it easy for users to detect the mistake.

Best regards
Tim Düsterhus

---

## #129003 — Dmytro Kulyk — 2025-10-29T22:23:05+00:00

URL: https://externals.io/message/128988#129003 · In reply to: #128998

Thanks for the feedback.

> Thank you for your RFC. It is really well-written and nicely explains
> the various (edge) cases. I've only read through the `#[\NoSerialize]`
> one, since it is the topic of this RFC discussion. I agree with Ilija on
> all points.
>
> Am 2025-10-29 15:35, schrieb Dmytro Kulyk:
> > […]
>
> But even for wrappers you can't guarantee that the wrapper is stored in
> a nullable property. When applying the attribute to a field, omitting
> the field makes sense, since the author of the property is the same
> person who is capable of adding the attribute and thus is in full
> control. This is different for classes where the author of the class
> doesn't know how and where the class is being used. I thus agree with
> Ilija that this should throw to clearly indicate that the user of the
> class is doing something incorrect and to not silently corrupt data. If
> an author of a class needs more fine-grained control, they can either
> add the attribute to individual properties themselves - or implement
> `__serialize()`.
>

You are right, this behavior can be non-obvious and lead to unexpected results.

> > […]
>
> Indeed, however the “unless explicitly overridden” bit in the RFC text
> implies that this is possible.
>
> > […]
>
> I don't think that sub classes should be able to make a non-serializable
> parent class serializable, since they clearly won't be able to correctly
> restore the state of the parent class. In fact I would consider this an
> improvement over the existing serialization hooks where the child class
> can simply skip calling the parent's serializer and thus create a
> situation that the parent class does not expect to occur.

It already makes no sense because the class-level attribute will be
excluded from the RFC, and I have no plan to add something like this
to #[NotSerializable]. It may be added in a future RFC.

> > […]
>
> Yes, every clear error that is detectable at compile-time should be a
> hard error to make it easy for users to detect the mistake.
>

Best regards
Dmytro Kulyk

---

## #129005 — tim@bastelstu.be — 2025-10-30T08:23:03+00:00

URL: https://externals.io/message/128988#129005 · In reply to: #129003

Hi

I am just noticing that your RFC still is marked as Draft, both within 
the RFC text and within the overview at https://wiki.php.net/rfc. Don't 
forget to adjust the status to “Under Discussion” for proper visibility.

Am 2025-10-29 23:23, schrieb Dmytro Kulyk:
> It already makes no sense because the class-level attribute will be
> excluded from the RFC, and I have no plan to add something like this
> to #[NotSerializable]. It may be added in a future RFC.

I am seeing that you adjusted the RFC to remove the support for applying 
`#[\NoSerialize]` to classes. Given that we determined that serializing 
classes should either throw or work (i.e. that serializing them to 
`null` is never right), I don't think that two separate attributes are 
necessary. While a combined attribute would behave somewhat differently 
for classes and properties, the name of `#[\NoSerialize]` and 
`#[\NotSerializable]` is very similar and thus confusing in itself. I 
would suggest to merge the RFCs of `#[\NoSerialize]` and 
`#[\NotSerializable]` into a single attribute, since in both cases you 
are indicating “I don't want this serialized” and I think it is 
reasonable to expect from users to learn that there is some difference 
in behavior for properties compared to classes.

Best regards
Tim Düsterhus

---

## #129014 — Dmytro Kulyk — 2025-10-30T22:24:09+00:00

URL: https://externals.io/message/128988#129014 · In reply to: #129005

> I am just noticing that your RFC still is marked as Draft, both within
> the RFC text and within the overview at https://wiki.php.net/rfc. Don't
> forget to adjust the status to “Under Discussion” for proper visibility.
>
> Am 2025-10-29 23:23, schrieb Dmytro Kulyk:
> > […]
>
> I am seeing that you adjusted the RFC to remove the support for applying
> `#[\NoSerialize]` to classes. Given that we determined that serializing
> classes should either throw or work (i.e. that serializing them to
> `null` is never right), I don't think that two separate attributes are
> necessary. While a combined attribute would behave somewhat differently
> for classes and properties, the name of `#[\NoSerialize]` and
> `#[\NotSerializable]` is very similar and thus confusing in itself. I
> would suggest to merge the RFCs of `#[\NoSerialize]` and
> `#[\NotSerializable]` into a single attribute, since in both cases you
> are indicating “I don't want this serialized” and I think it is
> reasonable to expect from users to learn that there is some difference
> in behavior for properties compared to classes.
>

Thanks, that’s a fair point — combining both proposals would
definitely simplify the user-facing story and avoid naming confusion.

If we go in that direction, I’d suggest keeping the name
#[NoSerialize], because it’s more general and clearly describes the
action rather than the failure mode.
#[NotSerializable] reads more like an error state, while
#[NoSerialize] works naturally for both contexts — skipping properties
silently and preventing class serialization (by throwing).

This way, the attribute name remains short and declarative, while the
actual behavior (throwing vs skipping) can be determined by where it’s
applied — on a class or on a property — without introducing another
attribute.

Best,
Dmytro

---

## #129015 — tim@bastelstu.be — 2025-10-31T08:44:07+00:00

URL: https://externals.io/message/128988#129015 · In reply to: #129014

Hi

Am 2025-10-30 23:24, schrieb Dmytro Kulyk:
> If we go in that direction, I’d suggest keeping the name
> #[NoSerialize], because it’s more general and clearly describes the
> action rather than the failure mode.
> #[NotSerializable] reads more like an error state, while
> #[NoSerialize] works naturally for both contexts — skipping properties
> silently and preventing class serialization (by throwing).
> 
> This way, the attribute name remains short and declarative, while the
> actual behavior (throwing vs skipping) can be determined by where it’s
> applied — on a class or on a property — without introducing another
> attribute.

That makes sense to me and I don't have a suggestion for a better name.

Best regards
Tim Düsterhus

---

## #129024 — Eric Norris — 2025-10-31T12:16:47+00:00

URL: https://externals.io/message/128988#129024 · In reply to: #129015

> Hi
>
> Am 2025-10-30 23:24, schrieb Dmytro Kulyk:
> > […]
>
> That makes sense to me and I don't have a suggestion for a better name.
>

I hope I'm not starting a bikeshed discussion, but I've been following
along - can I suggest "DoNotSerialize"? "NoSerialize" sounds odd to me,
whereas "DoNotSerialize" can be seen as both an instruction to PHP - "don't
serialize this property when serializing the class" - and an instruction to
developers - "do not attempt to serialize this class".

---

## #129025 — tim@bastelstu.be — 2025-10-31T12:23:17+00:00

URL: https://externals.io/message/128988#129025 · In reply to: #129024

Hi

Am 2025-10-31 13:16, schrieb Eric Norris:
> I hope I'm not starting a bikeshed discussion, but I've been following
> along - can I suggest "DoNotSerialize"? "NoSerialize" sounds odd to me,
> whereas "DoNotSerialize" can be seen as both an instruction to PHP - 
> "don't
> serialize this property when serializing the class" - and an 
> instruction to
> developers - "do not attempt to serialize this class".

I don't have a particularly strong opinion on naming, but I'd like to 
note that #[\NoSerialize] is in line with #[\NoDiscard] (added in PHP 
8.5, https://wiki.php.net/rfc/marking_return_value_as_important).

Best regards
Tim Düsterhus

---

## #129026 — Eric Norris — 2025-10-31T12:29:36+00:00

URL: https://externals.io/message/128988#129026 · In reply to: #129025

> Hi
>
> Am 2025-10-31 13:16, schrieb Eric Norris:
> > […]
>
> I don't have a particularly strong opinion on naming, but I'd like to
> note that #[\NoSerialize] is in line with #[\NoDiscard] (added in PHP
> 8.5, https://wiki.php.net/rfc/marking_return_value_as_important).
>

Fair. I had considered that this might already be inconsistent with other
attributes, but I was responding by phone and didn't look it up - thanks.
I'm still partial to something that sounds more "natural" from a language
perspective to me, but I can appreciate the argument to prefer consistency.

(This may come up in the future, so I wonder if we have / should have a
naming policy?)

---

## #129122 — Dmytro Kulyk — 2025-11-07T00:13:01+00:00

URL: https://externals.io/message/128988#129122 · In reply to: #129015

Hi all,

I’ve updated the RFC to include the new class-level behavior —
applying #[NoSerialize] to a class now forbids its serialization by
throwing, using the same internal mechanism as built-in
non-serializable classes (e.g., CurlHandle, Random\Engine\Secure).

This update aligns the proposal with existing engine semantics and
unifies both property-level skipping and class-level prohibition under
a single attribute.
The inheritance model has also been clarified: class-level
#[NoSerialize] is inherited (“sticky”) and cannot be overridden in
subclasses.

Updated RFC:
https://wiki.php.net/rfc/no_serialize_attribute

Best regards,
Dmytro Kulyk

---

## #129130 — tim@bastelstu.be — 2025-11-07T10:57:22+00:00

URL: https://externals.io/message/128988#129130 · In reply to: #129122

Hi

Am 2025-11-07 01:13, schrieb Dmytro Kulyk:
> I’ve updated the RFC to include the new class-level behavior —
> applying #[NoSerialize] to a class now forbids its serialization by
> throwing, using the same internal mechanism as built-in
> non-serializable classes (e.g., CurlHandle, Random\Engine\Secure).
> 
> This update aligns the proposal with existing engine semantics and
> unifies both property-level skipping and class-level prohibition under
> a single attribute.
> The inheritance model has also been clarified: class-level
> #[NoSerialize] is inherited (“sticky”) and cannot be overridden in
> subclasses.
> 
> Updated RFC:
> https://wiki.php.net/rfc/no_serialize_attribute

Thank you. The updated RFC is looking good to me. I also appreciate that 
you kept a changelog within the RFC.

One thing you could mention and that should be done as part of the RFC 
is migrating all existing classes with `@not-serializable` to make use 
of the attribute instead. That's also what has been done when the 
`#[\Deprecated]` attribute was introduced. Other than that, I don't have 
any further comments.

Best regards
Tim Düsterhus

---

## #129226 — Dmytro Kulyk — 2025-11-14T22:51:23+00:00

URL: https://externals.io/message/128988#129226 · In reply to: #129130

пт, 7 лист. 2025 р. о 12:57 Tim Düsterhus <tim@bastelstu.be> пише:
>
> Thank you. The updated RFC is looking good to me. I also appreciate that
> you kept a changelog within the RFC.
>
> One thing you could mention and that should be done as part of the RFC
> is migrating all existing classes with `@not-serializable` to make use
> of the attribute instead. That's also what has been done when the
> `#[\Deprecated]` attribute was introduced. Other than that, I don't have
> any further comments.
>

Thank you for the review and the helpful suggestion.

I agree that all internal classes currently annotated with
@not-serializable should also receive the corresponding
#[\NoSerialize] attribute as part of this RFC. This ensures that
internal metadata, reflection, and userland tooling remain fully
aligned, and that the behavior is consistently exposed at both the
engine and stub levels.

The RFC has been updated to explicitly state that these attributes
will be added for all such internal classes.

I also have one technical question regarding stub generation:
When attributes are emitted into generated arginfo.h files, should the
generator automatically include zend_attributes.h?
This would avoid requiring extensions to include zend_attributes.h
explicitly when they do not use attributes themselves but only include
generated stubs that reference them. Before making any implementation
adjustments, I’d appreciate confirmation that this approach is
acceptable within the current stub-generation design.

```
/* This is a generated file, edit the .stub.php file instead.
 * Stub hash: 2fc12d1fde65efbec4305f4934a3f4b25282a552 */

#include "zend_attributes.h"
...
    zend_add_class_attribute(class_entry, ZSTR_KNOWN(ZEND_STR_NO_SERIALIZE), 0);
...
```

Best regards,
Dmytro

---

## #129314 — Nicolas Grekas — 2025-11-19T20:45:43+00:00

URL: https://externals.io/message/128988#129314 · In reply to: #129226

Hi Dmytro


пт, 7 лист. 2025 р. о 12:57 Tim Düsterhus <tim@bastelstu.be> пише:
> > […]
>
> Thank you for the review and the helpful suggestion.
>
> I agree that all internal classes currently annotated with
> @not-serializable should also receive the corresponding
> #[\NoSerialize] attribute as part of this RFC. This ensures that
> internal metadata, reflection, and userland tooling remain fully
> aligned, and that the behavior is consistently exposed at both the
> engine and stub levels.
>
> The RFC has been updated to explicitly state that these attributes
> will be added for all such internal classes.
>
> I also have one technical question regarding stub generation:
> When attributes are emitted into generated arginfo.h files, should the
> generator automatically include zend_attributes.h?
> This would avoid requiring extensions to include zend_attributes.h
> explicitly when they do not use attributes themselves but only include
> generated stubs that reference them. Before making any implementation
> adjustments, I’d appreciate confirmation that this approach is
> acceptable within the current stub-generation design.
>
> ```
> /* This is a generated file, edit the .stub.php file instead.
>  * Stub hash: 2fc12d1fde65efbec4305f4934a3f4b25282a552 */
>
> #include "zend_attributes.h"
> ...
>     zend_add_class_attribute(class_entry,
> ZSTR_KNOWN(ZEND_STR_NO_SERIALIZE), 0);
> ...
> ```


Thanks for the RFC.

I think it’s an easy sell, but not necessarily for the right reasons. Let
me explain.

First, on property-level usage:
The RFC states that the attribute covers a common use case: transient or
resource-based properties in frameworks and libraries. On which fact is
this based? I checked code bases I know well, Symfony in particular, and I
had a hard time finding any implementation of __serialize()/__sleep() that
the property-level attribute would replace (ignoring BC). Symfony usually
exhibits most common and uncommon patterns, so if this were widespread, I
would expect to see some. More generally, having objects that embed both
value-state and a PDO connection looks like a design smell: such objects
behave differently before and after serialization, which is inherently
brittle. Such cases exist in the wild, of course, but making them a
first-class, favored use case in the language seems questionable.

Second, on class-level not-serializability:
This is a very common need. We had a thread a year ago discussing a
#[NotSerializable] attribute with identical semantics (
https://externals.io/message/121969). My position was and still is:
attaching this semantics to classes fits an interface far better than an
attribute. Even if it’s a negative contract, it’s still a contract, and
contracts propagate naturally to child classes. Attributes don’t, and
making this one an exception raises the question: why break attribute rules
when PHP already has the right tool - interfaces - to express this? The
engine currently uses a flag internally, but it could just as well use an
interface, removing a special case and aligning internal and userland
behavior. This follows a long and good tradition of replacing ad-hoc engine
features with userland-visible constructs.

Finally, a practical concern:
I wrote and still maintain the symfony/var-exporter
<https://packagist.org/packages/symfony/var-exporter> component, which
implements PHP’s serialization semantics in userland but outputs executable
PHP code. It is used in performance-sensitive cache systems. With the
proposed attribute, we would need reflection-based checks on every property
during both serialization and unserialization. Even with caching, this
would noticeably hurt performance. This is another argument in favor of an
interface BTW: instanceof/is_subclass_of() checks are much cheaper than
reading attributes for userland (not even counting the proposed inheritance
rule).

Thanks for considering my feedback.
Best regards,

Nicolas

---

## #129322 — tim@bastelstu.be — 2025-11-20T11:08:29+00:00

URL: https://externals.io/message/128988#129322 · In reply to: #129226

Hi

Am 2025-11-14 23:51, schrieb Dmytro Kulyk:
> I also have one technical question regarding stub generation:
> When attributes are emitted into generated arginfo.h files, should the
> generator automatically include zend_attributes.h?

There was a previous discussion in some GitHub PR about this for 
attributes added previously. Personally I think it should automatically 
include `zend_attributes.h` to be self-contained. If it's simple to 
change, I think creating a PR is the best way to move that discussion 
forward, since folks have something “real” to see.

Best regards
Tim Düsterhus

---

## #132444 — Dmytro Kulyk — 2026-09-07T13:18:28+00:00

URL: https://externals.io/message/128988#132444 · In reply to: #129314

Hi Nicolas,

Thank you for the review, and sorry for the delay.

> I checked code bases I know well, Symfony in particular, and I had a
> hard time finding any implementation of __serialize()/__sleep() that
> the property-level attribute would replace

You are right, and the RFC was wrong to imply otherwise. There is no
such __sleep() in Symfony.

The class-level case is there, six times, written identically in
Lock\Lock, Semaphore\Semaphore, Process\Pipes\UnixPipes,
ErrorHandler\BufferingLogger, HttpClient\Chunk\ErrorChunk and
Form\Util\OrderedHashMapIterator:

    public function __serialize(): array
    {
        throw new \BadMethodCallException('Cannot serialize '.__CLASS__);
    }

For the property-level case, Magento 2 has 31 classes with __sleep(),
chained so that each level strips its own injected services. Each
class below extends the one above it:

    Framework\Model\AbstractModel: -8 properties
    Framework\Model\AbstractExtensibleModel: -2
    Eav\Model\Entity\Attribute\AbstractAttribute: -12
    Eav\Model\Entity\Attribute: -4
    Catalog\Model\ResourceModel\Eav\Attribute: -4

Every level is array_diff(parent::__sleep(), [...]) with a mirrored
__wakeup() re-injecting the same services, so each property is named
twice. Same shape in their collection hierarchy, in Store and in User
(13 properties). Smaller whitelist form: Laravel's RateLimited and
Doctrine's PersistentCollection.

> having objects that embed both value-state and a PDO connection looks like a design smell

Agreed as a principle, and PDO was a bad headline example. But this is
the normal shape of an ORM model in Magento and Laravel, and it will
not be redesigned away. __serialize() is not a substitute: it requires
describing the whole format by hand, which is what produces those
 `array_diff()` chains.

The introduction has been rewritten around these examples.

> contracts propagate naturally to child classes. Attributes don't, and
> making this one an exception raises the question: why break attribute
> rules

It is not an exception. #[AllowDynamicProperties] already behaves this
way, since 8.2:

    #[AllowDynamicProperties]
    class P {}
    class C extends P {}

    $c = new C;
    $c->dyn = 1;                 // no deprecation: flag is inherited
    $r = new ReflectionClass('C');
    $r->getAttributes(AllowDynamicProperties::class);   // array(0) {}

zend_inheritance.c copies ZEND_ACC_ALLOW_DYNAMIC_PROPERTIES from the
parent, alongside ZEND_ACC_NOT_SERIALIZABLE.

The interface alternative was argued at length in the 2024 thread and
the list did not converge: Eberlei, Banyard and Dusk for the
attribute, you and Larry for the interface.

One cost that was not raised there: this RFC migrates 107 internal
classes across 64 stub files. As an interface, all of them would gain
a new entry in `class_implements()` — observable userland state, and a
larger BC surface than an attribute.

If the list prefers an interface, that is the list's call, but it
should be made deliberately rather than as a side effect here.

> we would need reflection-based checks on every property during both
> serialization and unserialization

I checked var-exporter. The cost is per-class on 7.x, and on 8.1 it is
not reflection at all.

Class-level is free: it sets the existing ZEND_ACC_NOT_SERIALIZABLE.
On 7.x Registry::unserialize() calls native `unserialize()`, which
already guards that flag, and getClassReflector() probes with
serialize($proto) once per class. ext-deepclone tests the flag
directly (deepclone.c:519, :5234).

Property-level on 8.1: VarExporter::export() delegates to
deepclone_to_array(), whose fast path already iterates
ce->properties_info_table and tests prop_info->flags —
ZEND_ACC_STATIC, ZEND_ACC_PUBLIC, ZEND_ACC_PROTECTED_SET,
ZEND_ACC_PRIVATE_SET (deepclone.c:2453-2489). ZEND_ACC_NO_SERIALIZE is
one more condition in a loop that already runs. The polyfill does use
reflection, but per class on both sides: $scopeMaps for export
(DeepClone.php:785-795), $propertyScopes for hydration (:453).

For the engine the comparison runs the other way: a ce_flags bit test
is cheaper than instanceof, which is why the engine uses a flag.

Three changes to the RFC since your message:

* The introduction cites the codebases above instead of asserting that
the use case is common.
* Internal classes are migrated explicitly, as @deprecated was
migrated to #[\Deprecated] in 8.4, rather than having the attribute
injected during code generation. @not-serializable stays supported for
third-party extensions. This follows Tim's suggestion.
* `unserialize()` is now symmetric: a #[NoSerialize] property present in
the payload is parsed and discarded rather than restored. The
attribute now means "not part of the serialized form" in both
directions, matching transient in Java and [NonSerialized] in C#, and
removing an injection surface. The old behaviour is recorded under
Rejected Features.

Best regards,
Dmytro Kulyk
