====== PHP RFC: Add #[NoSerialize] attribute for excluding properties or classes from serialization ====== * Version: 0.7 * Date: 2025-10-06 * Author: Dmytro Kulyk, lnkvisitor.ts@gmail.com * Status: Under Discussion * Implementation: https://github.com/php/php-src/pull/20074 ===== Introduction ===== Serialization is a fundamental PHP mechanism that allows objects to be converted into a storable or transferable representation. However, not every property of an object should necessarily be serialized. Frameworks and libraries often contain transient or resource-based properties—such as database connections, file handles, or caches—that should not be persisted. Currently, developers must manually handle this by overriding __sleep() or __serialize(), which can lead to repetitive boilerplate and maintenance overhead. ===== Proposal ===== This proposal introduces a new #[NoSerialize] attribute that can be applied to properties to exclude them from native PHP serialization or to classes to forbid serialization entirely. It provides a declarative alternative to manually filtering properties within __sleep() or __serialize(), making serialization rules easier to maintain and more self-documenting. ==== 1. Syntax and Definition ==== Usage example: name = "User"; $object->connection = new PDO('sqlite::memory:'); echo serialize($object); // Serialized output will be `O:7:"Example":1:{s:4:"name";s:4:"User";}`. ?> ==== 2. Semantics ==== === 2.1 Property-level Behavior === When the #[NoSerialize] attribute is applied to a **property**, it instructs the engine to skip that property during native serialization. * When serialize() is invoked and the class does not define its own __serialize() or __sleep(), properties marked with #[NoSerialize] are skipped automatically. * The resulting serialized data omits these properties entirely. * Upon unserialize(), all properties present in the serialized payload are restored normally. * The #[NoSerialize] attribute does **not** affect deserialization — if a property exists in the serialized data (for example, from an older class version or custom serializer), it will still be deserialized. * Skipped properties (those not serialized) remain uninitialized if they have no default, or are restored to their declared default value. class SessionWrapper { public string $id; #[NoSerialize] public $resource; // transient field public function __construct() { $this->resource = fopen('php://memory', 'r+'); } } $s = new SessionWrapper(); $s->id = 'abc'; var_dump(unserialize(serialize($s))); /* object(SessionWrapper)#2 (1) { ["id"]=> string(3) "abc" } */ == Interaction with __serialize() and __sleep() == If a class defines its own serialization logic via __serialize() or __sleep(), the #[NoSerialize] attribute on properties has **no effect**. These methods are entirely user-defined, and PHP does not automatically filter out properties or classes marked with #[NoSerialize]. This design maintains explicit and consistent behavior with existing PHP semantics — developer-defined serialization always takes precedence. Example: class Custom { public string $a = 'A'; #[NoSerialize] public string $b = 'B'; public function __serialize(): array { return ['a' => $this->a, 'b' => $this->b]; } } echo serialize(new Custom()); // Output still contains both 'a' and 'b' Developers who wish to respect #[NoSerialize] inside __serialize() can do so manually via reflection: class Custom { public string $a = 'A'; #[NoSerialize] public string $b = 'B'; public function __serialize(): array { $result = []; foreach ((new ReflectionObject($this))->getProperties() as $prop) { if (!$prop->getAttributes(NoSerialize::class)) { $result[$prop->getName()] = $prop->getValue($this); } } return $result; } } == Inheritance and Traits: == * #[NoSerialize] applied to a property affects only that declaration and is not inherited if a subclass redeclares the property. * Properties introduced via traits preserve the attribute when composed into a class. * Promoted constructor properties can include the attribute as usual. class Example { public function __construct( public string $name, #[NoSerialize] public ?PDO $db = null ) {} } === 2.2 Class-level Behavior === When the #[NoSerialize] attribute is applied to a **class**, any attempt to serialize an instance of that class will **throw an exception**, explicitly forbidding its serialization. This behavior uses the same internal mechanism as for built-in non-serializable classes (such as Random\Engine\Secure or CurlHandle). This ensures that invalid or unintended serialization attempts are immediately visible to developers and cannot result in partial or lossy data structures. #[NoSerialize] class Connection { public PDO $pdo; public function __construct() { $this->pdo = new PDO('sqlite::memory:'); } } class Wrapper { public string $name = 'foo'; public Connection $conn; public function __construct() { $this->conn = new Connection(); } } $w = new Wrapper(); echo serialize($w); /* Fatal error: Cannot serialize instance of class Connection marked with #[NoSerialize] */ Notes: * Class-level #[NoSerialize] forbids serialization entirely by throwing a fatal error. * It can be used to mark classes that represent resources, handles, or runtime-only objects. * This provides a clear and consistent failure mode, identical to the mechanism used for internal non-serializable classes. == Inheritance: == * Class-level #[NoSerialize] is inherited by all child classes and cannot be “overridden”. The prohibition is permanent (“sticky”) and automatically propagated throughout the inheritance chain. * Applying #[NoSerialize] again in a subclass when the parent already has it is a no-op (but allowed). === 2.3 Interaction with other serialization forms === * Out of scope: JSON (json_encode(), JsonSerializable) and var_export() remain unaffected. * The attribute affects only native PHP serialization (serialize(), unserialize()). Future changes extending the behavior of #[NoSerialize] to json_encode() or other formats would be **backward-incompatible** once this RFC is implemented. For that reason, any future proposal in this direction would need to introduce a **separate attribute**, such as #[NoJsonEncode], to avoid ambiguity and preserve expected behavior for existing code. === 2.4 Reflection API === The attribute is visible and queryable via reflection: $rp = new ReflectionProperty(Example::class, 'connection'); var_dump($rp->getAttributes(NoSerialize::class)); // array(1) { ... } === 2.5 Invalid Targets & Compile-Time Diagnostics === Applying #[NoSerialize] to unsupported targets results in compile-time diagnostics. The engine validates the attribute’s target during class compilation and emits appropriate compile-time errors. ^ Target ^ Severity ^ Message ^ Behavior ^ | Static property | **E_COMPILE_ERROR** | `Cannot apply #[\NoSerialize] to static property %s::$%s` | Compilation aborted | | Virtual property | **E_COMPILE_ERROR** | `Cannot apply #[\NoSerialize] to virtual property %s::$%s` | Compilation aborted | | Interface | **E_COMPILE_ERROR** | `Cannot apply #[\NoSerialize] to interface %s` | Compilation aborted | | Trait | **E_COMPILE_ERROR** | `Cannot apply #[\NoSerialize] to trait %s` | Compilation aborted | Rationale: * Static properties are class-level and not part of instance serialization. * Virtual properties are engine-managed and not serialized by userland mechanisms. * Interfaces and traits cannot be serialized or instantiated, so the attribute is invalid in those contexts. ==== 3. Alternative names ==== ^ Proposed name ^ Notes ^ | NoSerialize | Chosen for its clarity and consistency. Short, imperative, and self-explanatory. | | SkipSerialize | Grammatically clear and intuitive; “skip” emphasizes runtime behavior rather than prohibition. Could be a valid alternative if “NoSerialize” is considered stylistically inconsistent. | | SerializeIgnore | Mirrors conventions used in other languages and frameworks (e.g., @JsonIgnore in Java). However, it feels less idiomatic in PHP, which favors simple verb prefixes (No*, Allow*, etc.) over noun-based ones. | | DoNotSerialize | Verbose but explicit. Deemed unnecessarily long for PHP attribute syntax. | ===== Backward Incompatible Changes ===== Defining a userland class named NoSerialize in the global namespace will no longer be possible, as this name becomes reserved for the new attribute. A GitHub search for "class NoSerialize " language:php returned **11 results**, all defined within namespaces. Therefore, this change would not affect any known public codebases in practice, and the impact on backward compatibility is expected to be negligible. ===== Proposed PHP Version(s) ===== Next version of PHP (PHP 8.6 or PHP 9.0) ===== RFC Impact ===== ==== To the Ecosystem ==== This RFC has no negative impact on existing code and benefits frameworks, static analyzers, and serializers that rely on native PHP serialization. ==== To Existing Extensions ==== None ==== To SAPIs ==== None ===== Open Issues ===== None currently. ===== Future Scope ===== * Allow __sleep() to return null or no value, signaling the engine to fall back to the default serialization logic, which would then automatically respect #[NoSerialize]. ===== Voting Choices ===== * Yes * No * Abstain ===== Patches and Tests ===== [[https://github.com/php/php-src/pull/20074]] ===== Implementation ===== After the RFC is implemented, this section should contain: - the version(s) it was merged into - a link to the git commit(s) - a link to the PHP manual entry for the feature ===== References ===== * Discussion: https://news-web.php.net/php.internals/128988 ===== Rejected Features ===== * Class-level #[NoSerialize] with replacing value by NULL ===== Changelog ===== * v0.7 — Class-level behavior aligned with @not-serializable (now throws instead of serializing as NULL) * v0.6 — Class-level #[NoSerialize] excluded from RFC * v0.5 — Clarified JSON scope * v0.4 — Restructured semantics into property/class sections, added proper compile-time diagnostics, and clarified deserialization behavior.