Skip to content

[PoC] PHPC-2379: Add properties to ReadPreference class#1973

Closed
alcaeus wants to merge 1 commit intomongodb:v2.xfrom
alcaeus:poc/phpc-2379-typed-properties
Closed

[PoC] PHPC-2379: Add properties to ReadPreference class#1973
alcaeus wants to merge 1 commit intomongodb:v2.xfrom
alcaeus:poc/phpc-2379-typed-properties

Conversation

@alcaeus
Copy link
Copy Markdown
Member

@alcaeus alcaeus commented Apr 7, 2026

PHPC-2379

I revisited the topic of proper properties for value objects based on recent work on object comparisons and the get_properties_for handler. For the time being, I've only done the work for the MongoDB\Driver\ReadPreference class, but the concept applies to all other value objects (see list below).

I've left comments in the code itself to elaborate on some of the changes, but the TL;DR is that we can do away with the static get_properties_hash function (which serves no other purpose than to build a list of properties for the various methods that need them) and some of the other handlers. In fact, we no longer need to implement the __serialize method and the get_debug_info object handler as these dump public properties by default. There are multiple test changes associated with this as our own handlers often would not serialise properties that were "unset" or contained default values.

Here is the list of classes this concept would apply to:

  • MongoDB\Driver\ReadConcern
  • MongoDB\Driver\WriteConcern
  • MongoDB\Driver\WriteError
  • MongoDB\Driver\WriteResult
  • MongoDB\Driver\Exception\CommandException
  • MongoDB\Driver\Exception\WriteException
  • MongoDB\Driver\Monitoring\CommandFailedEvent
  • MongoDB\Driver\Monitoring\CommandStartedEvent
  • MongoDB\Driver\Monitoring\CommandSucceededEvent
  • MongoDB\Driver\Monitoring\ServerChangedEvent
  • MongoDB\Driver\Monitoring\ServerClosedEvent
  • MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent
  • MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent
  • MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent
  • MongoDB\Driver\Monitoring\ServerOpeningEvent
  • MongoDB\Driver\Monitoring\TopologyChangedEvent
  • MongoDB\Driver\Monitoring\TopologyClosedEvent
  • MongoDB\Driver\Monitoring\TopologyOpeningEvent

These changes would also apply to all BSON classes, although they would not be as useful as the corresponding methods (which should eventually be deprecated) are mandated by the interfaces, which can't contain properties until PHP 8.4. For some of the other classes (e.g. MongoDB\Driver\Server), the value of a property may change over time (e.g. the server description or latency) so I'm not sure whether we would want to use hooked properties for those. I would suggest discussing that separately and splitting PHPC-2379 if we decide to go this route.


zend_class_entry* phongo_readpreference_ce;

static const char* phongo_readpreference_get_mode_string(const mongoc_read_prefs_t* read_prefs)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function isn't new, I merely had to pull it up as it's used in update_properties.

mongoc_read_prefs_set_tags(intern->read_preference, tags);
bson_destroy(tags);
} else {
} else if (Z_TYPE_P(tagSets) != IS_NULL) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is necessary as we now serialise null values for empty tag sets, while previously we would omit the tags element entirely.

}
}

static HashTable* phongo_readpreference_get_properties_hash(zend_object* object, bool is_temp)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

get_properties_hash becomes entirely useless.

Comment on lines +448 to +453
ZEND_HASH_FOREACH_STR_KEY_VAL_IND(HASH_OF(getThis()), string_key, val)
{
if (
(Z_TYPE_P(val) == IS_NULL) || (!strcmp(ZSTR_VAL(string_key), "maxStalenessSeconds") && Z_TYPE_P(val) == IS_LONG && Z_LVAL_P(val) == MONGOC_NO_MAX_STALENESS)) {
continue;
}
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bsonSerialize gets a bit of special logic to avoid adding default values to the output.
HASH_OF(getThis()) gets us the property table, which we can then iterate over to skip null values, as well as the default value for maxStalenessSeconds.

Note the IND suffix on the foreach macro above. Without this, the type of each zval is IS_INDIRECT, while the _IND set of foreach macros gives us access to the actual zval so we can do type checking.

Comment on lines +457 to +458
Z_TRY_ADDREF_P(val);
add_assoc_zval(return_value, ZSTR_VAL(string_key), val);
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add_assoc_zval assumes ownership of the zval. Since we're passing the zval from the internal properties table, we have to use Z_TRY_ADDREF_P to add to the refcount of each refcounted zval to avoid the internal property value being freed once the return value goes out of scope. Finding this took me longer than I care to admit.

return &intern->std;
}

static HashTable* phongo_readpreference_get_debug_info(zend_object* object, int* is_temp)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No longer need to implement a get_debug_info handler as this now corresponds to the default behaviour.

}

static HashTable* phongo_readpreference_get_debug_info(zend_object* object, int* is_temp)
static zval* phongo_readpreference_read_property(zend_object* zobj, zend_string* name, int type, void** cache_slot, zval* rv)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Implementing the read_property handler is necessary to emit a deprecation notice for the hedge property (which is deprecated like the getHedge method). Since PHP does not support marking properties as deprecated internally (the mechanism only exists for functions/methods), we have to work around this and emit a deprecation notice before deferring to zend_std_read_property.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there an opportunity for a PHP RFC to enable property deprecation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been previously discussed, and at least for properties it doesn't seem very straightforward: https://externals.io/message/127305. I would definitely appreciate being able to formally deprecate all kinds of symbols in PHP.


typedef struct {
mongoc_read_prefs_t* read_preference;
HashTable* properties;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This struct member was only needed to store our own properties table assembled by get_properties_hash. The internal properties table itself is stored in zend_object.

Comment on lines +54 to +57
["maxStalenessSeconds"]=>
int(-1)
["hedge"]=>
NULL
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All test changes account for the extra output generated by var_dump which now always includes all properties.

@@ -0,0 +1,45 @@
--TEST--
MongoDB\Driver\ReadPreference equality comparison
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test was lifted from #1971. I changed the way we print the output for easier debugging and moved the deprecation notices to the top of the output as we run all comparisons before printing output.

Other than that, none of the results needed changing. Most importantly though, since we now defer to the standard object comparator, we can rely on a well-defined order between different ReadPreference instances based on the order and values of their internal properties.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see any tests using the > and < operators, so I assume they aren't supported natively with object properties. The value-equal == operator seems perfectly adequate to me as it is.

Copy link
Copy Markdown
Member Author

@alcaeus alcaeus Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They are supported -- I merely wanted to make sure that the tests added in #1971 pass as expected with this change. I would actually change the tests to var_dump($rp1 <=> $rp2); so we can assert against the result.

For context, when PHP compares objects of the same type that don't have a compare handler implemented, the engine goes through all properties of the objects comparing their values until a difference has been found, in which case the -1 or 1 value is returned. This follows PHP's standard semantics for value comparisons.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using spaceship operator in the test would be more specific; that's a good idea if we are confident PHP default sorting behavior will not change.
That would also prevent us from changing properties declaration order.

@alcaeus alcaeus force-pushed the poc/phpc-2379-typed-properties branch from 43c95d2 to d7330eb Compare April 7, 2026 07:11
@alcaeus alcaeus force-pushed the poc/phpc-2379-typed-properties branch from d7330eb to 32861f7 Compare April 7, 2026 11:22
Comment on lines +54 to +59
public readonly string $mode;
public readonly array|null $tags;
public readonly int $maxStalenessSeconds;

/** @deprecated */
public readonly object|null $hedge;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The reason all these properties weren't declared was because we couldn't make them read-only. But that's been possible since PHP 8.1. That explains why we can do it now, and it seems like a cleaner design.
However, as discussed, PHP does not allow modifications to a read-only property. Instead, we would use private(set) for properties whose values may change over time.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's correct -- however, private(set) requires us to bump our version requirement to PHP 8.4, while for value objects readonly achieves the same thing on PHP 8.1 already. Since all these classes are final, I don't see a concern to switching to private(set) once we bump the version requirement.

@alcaeus
Copy link
Copy Markdown
Member Author

alcaeus commented Apr 8, 2026

Continued in #1979.

@alcaeus alcaeus closed this Apr 8, 2026
@alcaeus alcaeus deleted the poc/phpc-2379-typed-properties branch April 8, 2026 13:26
Sign up for free to 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