Repository navigation
Set properties without throwing for unknown names on dynamic classes - #37
Merged
Merged
Conversation
properties_set threw and caught an UnreflectableException for every name a dynamic class does not declare, so hydrating a stdClass cost one exception per property. ReflectedClass::findProperty() walks the same parent chain as property() and returns null instead, and property() is built on top of it.
object_data always builds a new object, so for a plain stdClass with the default accessor the cast gives the same result without reflection.
This was referenced Oct 14, 2026
This was referenced Oct 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
properties_setlooks up every passed name withReflectedClass::property(), which throwsUnreflectableExceptionwhen the class and its parents do not declare it. For a dynamic class that exception is caught and the value is assigned directly, so hydrating astdClasscosts one thrown and caught exception per property. php-soap/encoding decodes every untyped complex type into astdClassthis way, which came up in the review of php-soap/encoding#70.Approach
ReflectedClass::findProperty()walks the same parent chain asproperty()and returnsnullinstead of throwing.property()is now built on it, andproperties_setuses it directly: a known property is set through reflection, an unknown one is assigned on a dynamic class and still throwsUnreflectableExceptionon any other class.findProperty()is public so php-soap/encoding can use it for a non-throwing read later.property_getis unchanged.object_data(stdClass::class)now casts the values with(object) $valuesinstead of hydrating throughproperties_set. It only does that for exactlystdClasswith the default accessor, becausefromalways builds a fresh object there. A custom accessor and other#[AllowDynamicProperties]classes keep the normal path.For a class with parents, the
unknownPropertymessage now names the class you asked for. It used to name the topmost ancestor, because the exception came from the last recursive call.I added phpbench as a dev dependency with benchmarks for both functions (
composer bench). They run withxdebug.mode=off.Benchmarks
Mode per call in microseconds, 500 revs, 10 iterations, PHP 8.4.13. All variants ran in the same phpbench run, because separate runs on this machine drifted by more than 50%.
$class->properties()once and does a key lookup per value.findProperty(). I measured it returning aPsl\Optionand returning a nullable.I went with C returning a nullable. B is fastest for a
stdClassbut up to 20x slower than today when one property of many is set, because it reflects every property up front. Returning anOptionadded 35 to 50% on typed classes with all properties set, which is the php-soap/encoding case. The nullable version stays within a few percent of the current code on typed classes and is faster on dynamic classes and on private properties in a parent.object_data(stdClass::class)->from($values), through the newproperties_setand with the cast:On main the same call took 3.8, 14.0 and 66.6 microseconds in a separate run.
Throws
Counted with Xdebug
xdebug.show_exception_tracein develop mode, 20 properties per scenario:The error path counted 2 before because the exception was caught and rethrown.
Release
This is meant for a 1.x minor release (1.1.0). The public API only gains
findProperty(), and the exception message above is the only other visible difference.