Changelog - Version 7.0
Status: in development. This document tracks changes landing on the
7.0branch. Nothing here is released yet, and the contents may still change.
Overview
Version 7.0 starts as a performance-focused release. ObjectCopy gains a direct copy path
for its most common usage, and the property handlers were simplified. Backed enums are now supported,
and are serialized as their value (the one breaking change). The PHP requirement is now >=8.3 <8.7 (see Requirements below).
New Features
Backed enums
A property typed with a backed enum (public ?Status $status) works in both directions:
Serializewrites a backed enum as its->value, asjson_encode()does, at every level, so also withwithStopAtFirstLevel(). It used to become{"name": ..., "value": ...}, or stay an enum object at the first level (which byjg/micro-orm passes to the database).ObjectCopybuilds the enum from anintorstringwithfrom()when the target property, or its setter's parameter, is typed with the enum (Statusor?Status). Writing'PAID'into that property used to be aTypeError. An unknown value throws aValueError; a type that also accepts a scalar (string|Status) receives the value as it is.
A pure enum is unchanged. See docs/serialize.md and docs/objectcopy.md.
Performance
ObjectCopy direct copy path
ObjectCopy::copy() previously routed every copy through the full Serialize pipeline.
When the source is an array and no property handler is given, that pipeline does no
work beyond iterating the array: withStopAtFirstLevel() makes every value pass through
untouched, no property filters are configured, and null values are copied by default.
That case now uses direct iteration instead. The resulting assignments are identical — the
behaviour is pinned by ObjectCopyTest::testFastPathMatchesPipeline(), which runs the same
inputs through both paths and asserts the targets match.
Copies made with a property handler, or from an object source, are unaffected and still use the pipeline.
Cached property-name resolution
The case-insensitive property lookup used when a target has no setter and no exactly matching
property is now cached statically, keyed by class name, rather than rebuilt on every call to
copy(). Class properties do not change at runtime, so the cache is both correct and bounded
by the number of classes copied into.
Measured impact
200,000 copies of a five-property model, PHP 8.5, no opcache JIT, median of three runs:
| Scenario | 6.0 | 7.0 | |
|---|---|---|---|
| Array source, no property handler | 3.67 s | 1.26 s | ~2.9x faster |
| Array source, with property handler | 5.09 s | 4.90 s | ~4% faster |
The second row only benefits from the cached property-name resolution; it still goes through the pipeline, since a property handler may map or transform every name and value.
Internal Changes
ObjectCopy::applyAttribute()no longer threads a property-name cache through its arguments; target assignment moved into a dedicatedapplyToTarget()helper shared by both copy paths.CamelToSnakeCaseandSnakeToCamelCaseuse arrow functions for theirpreg_replace_callbackbodies, dropped an unuseduse Closure;import, and had trailing whitespace cleaned up.
These are all private/internal to final class ObjectCopy, so they are not part of the public API.
Bug Fixes
Serialize::withMethodPattern()now rejects a search pattern shorter than 2 characters, as its message ("cannot be empty or less than 2 characters") already said. The check used&&(empty($search) && strlen($search) <= 2), so only''and'0'were rejected and a one-character pattern such as'/'was stored, failing later inpreg_replace()with "No ending delimiter". A 2-character pattern ('//') is still accepted.ObjectCopy::copy()now copies a backed enum the same way on every path. An array source without a property handler (the fast path) copied the enum object as it was, while every other path (an object source, or any source with a handler) goes throughSerializeand copied its->value. A target typed with the enum got the same result either way, but astring|Enumproperty, astdClassor an array target got the enum object from one path and its value from the other. The fast path now copies the value too.
Breaking Changes
A backed enum is serialized as its value
Serialize::from($model)->toArray() (and toJson(), toYaml(), ...) gave a backed enum as
['name' => 'Paid', 'value' => 'PAID']; it now gives 'PAID'. With withStopAtFirstLevel()
it gave the enum object; it now gives 'PAID'. Code that read ['value'] from the output
reads the value directly.
ObjectCopy copies a backed enum from an array as its value
ObjectCopy::copy(['status' => Status::Paid], $target) (array source, no property handler)
put the enum object into a stdClass or array target, or into a property that also takes a
scalar (string|Status); it now puts 'PAID', as an object source or a property handler
already did. A property or setter typed with the enum (Status, ?Status) still gets
Status::Paid.
Path to Upgrade from 6.x to 7.x
If you read a backed enum from Serialize output as ['value'] (or as the enum object at the
first level), read the value directly. If you copy an array holding a backed enum into a
stdClass, an array or a string|Enum property and expect the enum object, call
Status::from() on the copied value. Otherwise no action is required. Existing code using ObjectCopy, ObjectCopyTrait,
BaseModel, Serialize, and the property handlers continues to work unchanged.
For the 6.x migration guide, see CHANGELOG-6.0.md.
Requirements
- PHP 8.3, 8.4, 8.5 and 8.6 are now supported:
"php": ">=8.3 <8.7". The previous<8.6upper bound excluded PHP 8.6, since<8.6is exclusive.
Toolchain
-
PHPUnit updated to
^12.5. -
Psalm is installed as
psalm/phar(^6.16) instead ofvimeo/psalm.vimeo/psalmlists the PHP versions it supports and no published release includes 8.6, so as a dev dependency it madecomposer installfail on the 8.6 build job before any test ran.psalm/pharrequires onlyphp ^8.2and bundles its own dependencies, so it installs on every PHP version in the matrix and cannot conflict with the project's. Psalm itself still refuses to run on 8.6, which is why the Psalm job uses 8.5.composer psalmruns it.PHPUnit 13 is deliberately not used. It requires PHP
>=8.4.1, which would break the 8.3 floor.
Continuous Integration
- The build matrix now includes PHP 8.6.
- The Psalm job now runs on PHP 8.5. Psalm declares
~8.1.31 || ~8.2.27 || ~8.3.16 || ~8.4.3 || ~8.5.0and therefore does not run on PHP 8.6.
Housekeeping
phpunit.xml.distrenamed tophpunit.xml.