-
Notifications
You must be signed in to change notification settings - Fork 7.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Wrong magic methods sequence with ??
operator
#12695
Comments
edit: Ah, you mean how the property gets created between the two calls. |
If you return |
Now seeing your edit @iluuu1994. But first of all yes the property should be fetched, but since __isset does set the property, the fetch shouldn't get through __get
Nothing prevents it and that's actually the only way to properly implement lazy object so yes, it can :) |
I don't think there's a good reason to add another check. Can't you just return the property you create in |
|
I must agree with @iluuu1994 here, |
I'm sorry but this doesn't make sense to me. The fact that __isset can be used to initialize properties is irrelevant to the issue. Actually, it's a critical capability that is leveraged since more than a decade to implement lazy objects. You might not be familiar with how those are implemented but the general idea is simple: all properties are unset when the object is created, and magic methods are added by inheritance to defer initialization to the moment when any property is accessed - that being via any magic methods, __isset included. There are critical piece of infrastructure that rely on lazy objects - many based on Doctrine entities and/or when using lazy services (eg Symfony DI container but not only it). The current behavior just breaks the semantics of magic methods. They are supposed to be called only when a property isn't accessible, and the call to __get is just missing the check to guard this behavior. Can you please expand on why you think things behave as expected? What's the argument to not fix this? |
Currently __isset and __get are called as a pair: Your proposal is to do This behaviour comes from the initial bugfix for PHP 7, it was probably done that way to just make it work, and maybe not the most fully thought out behaviour, but it's how it works today. I don't think you should argue it would break critical pieces of infrastructure, otherwise they'd be long broken, since PHP 7 that is. @nicolas-grekas I do however think, that from a theoretical perspective __get() should not be called, if it exists, period. It would be a break in long-standing behaviour though and as such only go into PHP 8.4, if this is changed. I also don't think performance is a major concern in this case, as the relative overhead of __isset() and __get() calls must be far bigger than a simple undef check (for declared props) or a hash_exists (for dynamic props). |
Patch
diff --git a/Zend/tests/coalesce_is_create_property.phpt b/Zend/tests/coalesce_is_create_property.phpt
new file mode 100644
index 0000000000..10927b6289
--- /dev/null
+++ b/Zend/tests/coalesce_is_create_property.phpt
@@ -0,0 +1,22 @@
+--TEST--
+Coalesce IS fetch should repeat property existence check after __isset
+--FILE--
+<?php
+#[AllowDynamicProperties]
+class A {
+ public function __isset($prop) {
+ echo __FUNCTION__, "\n";
+ $this->$prop = 123;
+ return true;
+ }
+ public function __get($prop) {
+ throw new Exception('Unreachable');
+ }
+}
+
+$a = new A;
+echo $a->foo ?? 234;
+?>
+--EXPECT--
+__isset
+123
diff --git a/Zend/zend_object_handlers.c b/Zend/zend_object_handlers.c
index 6a22156d18..d2b34ff9bf 100644
--- a/Zend/zend_object_handlers.c
+++ b/Zend/zend_object_handlers.c
@@ -589,6 +589,36 @@ ZEND_API uint32_t *zend_get_property_guard(zend_object *zobj, zend_string *membe
}
/* }}} */
+static zval *zend_std_read_dynamic_property(zend_object *zobj, zend_string *name, void **cache_slot, uintptr_t property_offset)
+{
+ if (EXPECTED(zobj->properties != NULL)) {
+ if (!IS_UNKNOWN_DYNAMIC_PROPERTY_OFFSET(property_offset)) {
+ uintptr_t idx = ZEND_DECODE_DYN_PROP_OFFSET(property_offset);
+
+ if (EXPECTED(idx < zobj->properties->nNumUsed * sizeof(Bucket))) {
+ Bucket *p = (Bucket*)((char*)zobj->properties->arData + idx);
+
+ if (EXPECTED(p->key == name) ||
+ (EXPECTED(p->h == ZSTR_H(name)) &&
+ EXPECTED(p->key != NULL) &&
+ EXPECTED(zend_string_equal_content(p->key, name)))) {
+ return &p->val;
+ }
+ }
+ CACHE_PTR_EX(cache_slot + 1, (void*)ZEND_DYNAMIC_PROPERTY_OFFSET);
+ }
+ zval *retval = zend_hash_find(zobj->properties, name);
+ if (EXPECTED(retval)) {
+ if (cache_slot) {
+ uintptr_t idx = (char*)retval - (char*)zobj->properties->arData;
+ CACHE_PTR_EX(cache_slot + 1, (void*)ZEND_ENCODE_DYN_PROP_OFFSET(idx));
+ }
+ return retval;
+ }
+ }
+ return NULL;
+}
+
ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int type, void **cache_slot, zval *rv) /* {{{ */
{
zval *retval;
@@ -638,31 +668,9 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
goto uninit_error;
}
} else if (EXPECTED(IS_DYNAMIC_PROPERTY_OFFSET(property_offset))) {
- if (EXPECTED(zobj->properties != NULL)) {
- if (!IS_UNKNOWN_DYNAMIC_PROPERTY_OFFSET(property_offset)) {
- uintptr_t idx = ZEND_DECODE_DYN_PROP_OFFSET(property_offset);
-
- if (EXPECTED(idx < zobj->properties->nNumUsed * sizeof(Bucket))) {
- Bucket *p = (Bucket*)((char*)zobj->properties->arData + idx);
-
- if (EXPECTED(p->key == name) ||
- (EXPECTED(p->h == ZSTR_H(name)) &&
- EXPECTED(p->key != NULL) &&
- EXPECTED(zend_string_equal_content(p->key, name)))) {
- retval = &p->val;
- goto exit;
- }
- }
- CACHE_PTR_EX(cache_slot + 1, (void*)ZEND_DYNAMIC_PROPERTY_OFFSET);
- }
- retval = zend_hash_find(zobj->properties, name);
- if (EXPECTED(retval)) {
- if (cache_slot) {
- uintptr_t idx = (char*)retval - (char*)zobj->properties->arData;
- CACHE_PTR_EX(cache_slot + 1, (void*)ZEND_ENCODE_DYN_PROP_OFFSET(idx));
- }
- goto exit;
- }
+ retval = zend_std_read_dynamic_property(zobj, name, cache_slot, property_offset);
+ if (retval) {
+ goto exit;
}
} else if (UNEXPECTED(EG(exception))) {
retval = &EG(uninitialized_zval);
@@ -693,6 +701,13 @@ ZEND_API zval *zend_std_read_property(zend_object *zobj, zend_string *name, int
}
zval_ptr_dtor(&tmp_result);
+
+ retval = zend_std_read_dynamic_property(zobj, name, cache_slot, property_offset);
+ if (retval) {
+ OBJ_RELEASE(zobj);
+ goto exit;
+ }
+
if (zobj->ce->__get && !((*guard) & IN_GET)) {
goto call_getter;
} I agree with @bwoebi in that this is more of a missing feature than a bug. The "property fetch" is currently considered an atomic operation. Anyway, returning |
I made this point to answer the objection that "isset should[n't] be used as a mechanism to create properties..." I agree that the reported behavior is an edge case. But it's one I stumbled upon while working on lazy objects so it's clearly a defect in how the engine behaves currently. To me it's an edge case that won't affect anyone negatively in practice and I would consider it as a bug fix. But I don't mind patching this in 8.4 if you think that's how it should be done.
Unless the value of the property is null! (note that the reported case is about __isset returning true) Thanks for the patch, I'm going to play with it right away. |
It turns out the patch doesn't work for declared, unset properties, as @bwoebi has pointed out. I think the might be soundness issues in that case too, because |
This can't be fixed without a behaviour change. In case the |
@dstogov I think the argument is that lazy objects cannot transparently handle this case. @nicolas-grekas It would be nice if you could provide a minimal example of what case you cannot solve without this fix by overriding |
Here is a simplified example : <?php
// This is the class that we want to make lazy.
// Note that this is an opaque class from the PoV of the proxy-generation logic:
// its implementation details are unknown.
class A
{
protected array $data = [];
public function __construct()
{
$this->data = ['foo' => 123];
}
public function __get($n) { return $this->data[$n] ?? null; } // line 15
public function __set($n, $v) { $this->data[$n] = $v; }
public function __isset($n) { return isset($this->data[$n]); }
public function __unset($n) { unset($this->data[$n]); }
}
// This is a simplified proxy class that would typically be generated automatically
// from the signature of the proxied class. The way this works is by unsetting all
// declared properties at instantiation time, then using the magic methods to lazily
// initialize them on demand. This is a very simple implementation that does not handle
// all edge cases, but it should be enough to illustrate the issue. The logic in each
// magic method is very repetitive: we checks if we're trying to access the uninitialized
// property. If yes, we initialize it, if not, we call the parent method.
class LazyA extends A
{
private const STATUS_UNINITIALIZED = 0;
private const STATUS_INITIALIZING = 1;
private const STATUS_INITIALIZED = 2;
private $lazyStatus = self::STATUS_UNINITIALIZED;
public function __construct()
{
unset($this->data);
}
public function __get($n)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
return parent::__get($n);
}
$this->initialize();
return $this->data;
}
public function __set($n, $v)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
parent::__set($n, $v);
}
$this->initialize();
$this->data = $v;
}
public function __isset($n)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
return parent::__isset($n);
}
$this->initialize();
return isset($this->data);
}
public function __unset($n)
{
if (self::STATUS_INITIALIZED === $this->lazyStatus || 'data' !== $n) {
parent::__unset($n);
}
$this->initialize();
unset($this->data);
}
private function initialize()
{
if (self::STATUS_INITIALIZING === $this->lazyStatus) {
return;
}
$this->lazyStatus = self::STATUS_INITIALIZING;
parent::__construct();
$this->lazyStatus = self::STATUS_INITIALIZED;
}
}
$a = new LazyA();
// Currently, this will trigger a TypeError:
// Cannot assign null to property A::$data of type array on line 15
// With Ilija's patch, this will work as expected and will display int(123)
var_dump($a->foo); |
BTW, I confirm that with @iluuu1994's patch the test cases I was working on are all green: I'm able to fix a buggy behavior that I had to implement as a workaround for the current behavior. |
The error message This may be fixed by modifying the line 15 public function __get($n) { return $this->data[$n] ?? ($n == "data" ? [] : null);} // line 15 |
We cannot do that because that would break encapsulation at two levels:
The issue is really the engine not behaving as it should - the rule is simple and not respected with the |
* 6.3: [Serializer] Fix normalization relying on allowed attributes only [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy [String] Fix Inflector for 'icon'
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 6.4: [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy fix typo Document BC break with $secret parameter introduction Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 Update CHANGELOG for 6.4.0-RC2 [String] Fix Inflector for 'icon' [Validator] Made tests forward-compatible with ICU 72.1
* 7.0: (23 commits) [Serializer] Fix anonymous test class [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy remove not needed method existance check fix typo fix typo Document BC break with $secret parameter introduction Bump Symfony version to 7.0.0 Update VERSION for 7.0.0-RC2 Update CHANGELOG for 7.0.0-RC2 Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 ...
* 7.0: (23 commits) [Serializer] Fix anonymous test class [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy remove not needed method existance check fix typo fix typo Document BC break with $secret parameter introduction Bump Symfony version to 7.0.0 Update VERSION for 7.0.0-RC2 Update CHANGELOG for 7.0.0-RC2 Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 ...
* 7.0: (23 commits) [Serializer] Fix anonymous test class [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy remove not needed method existance check fix typo fix typo Document BC break with $secret parameter introduction Bump Symfony version to 7.0.0 Update VERSION for 7.0.0-RC2 Update CHANGELOG for 7.0.0-RC2 Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 ...
* 7.0: (23 commits) [Serializer] Fix anonymous test class [Translation] Improve tests coverage [Routing] Add redirection.io as sponsor of versions 6.4/7.0/7.1 don't check parameter values if they are not set [HttpClient] Add Innovative Web AG (i-web) as sponsor of version 6.4/7.0 [Serializer] Fix normalization relying on allowed attributes only Minor @var doc update [Translation] Remove `@internal` from abstract testcases [VarExporter] Work around php/php-src#12695 for lazy objects, fixing nullsafe-related behavior [Validator] Add missing translations for Bulgarian #51931 [VarExporter] Fix serializing objects that implement __sleep() and that are made lazy remove not needed method existance check fix typo fix typo Document BC break with $secret parameter introduction Bump Symfony version to 7.0.0 Update VERSION for 7.0.0-RC2 Update CHANGELOG for 7.0.0-RC2 Bump Symfony version to 6.4.0 Update VERSION for 6.4.0-RC2 ...
Just to be sure we're on par: do we want to fix this as a bug or do we want to go through an RFC? |
I'm not sure. |
I will also note, that |
@Girgias you're right but this also spans to other operators, eg |
In my opinion |
@nicolas-grekas Are you planning on moving this discussion forward? Or did you find an acceptable solution for your use-case? |
Yes, I'd like this to be fixed so I'll raise the issue ASAP on internals, thanks for the reminder. |
@nicolas-grekas Is this still relevant with the introduction of lazy objects? |
Yes it is. But I'm also thinking about an RFC that'd add an I'd be fine to properly define the behavior of |
I don't really care about BC here, these are extreme edge cases. But object handlers are already very complex, things like this don't make them simpler.
Can you clarify why? |
Nothing fancy - I just mean the initial report: The sequence is incorrect currently. I'm not going to fall into it anymore with lazy-objects but that still remains incorrect behavior :) |
Thank you for the clarification. I'm going to close my PR in favor of lazy objects then. I'm not sure if the current behavior can be classified as "wrong", but with an available alternative now, it doesn't seem necessary to modify it. |
Description
When
??
is used to check a property using magic methods, PHP unconditionnaly calls both __isset and __get even when __isset did actually set the property.Reproducer: https://3v4l.org/cDlNW
This script:
echoes this:
while it should echo this:
PHP Version
All since 7.0.6
Operating System
No response
The text was updated successfully, but these errors were encountered: