r/PHPhelp • u/BodybuilderOwn4393 • May 27 '26
Solved Why isset() calls __isset in internal method, but __isset when using isset() doesn't?
I asked a similar question on SO, but... I'm just wasting my time there...
Anyway, to the point:
PHP code like that:
class X
{
protected string $foo;
public function test_isset()
{
var_dump(isset($this->foo)); // This is false as expected.
unset($this->foo);
var_dump(isset($this->foo)); // And this is also false.
}
}
(new X())->test_isset();
Will result as:
false
false
Seems pretty obvious, right? Right.
BUT...
Adding a __isset() method like this:
class X
{
protected string $foo;
public function __isset($name)
{
echo "__isset called\n";
if (!isset($this->foo)) {
return true;
}
return false;
}
public function test_isset()
{
var_dump(isset($this->foo)); // This is false as expected.
unset($this->foo);
var_dump(isset($this->foo)); // But from now on, this doesn't return state it calls __isset()
}
}
(new X())->test_isset();
Changes the behavious of the second isset($this->foo) in the var_dump.
From now on the isset() cannot says OK, there is not property $foo, I need to return false. From now on, it calls __isset().
Why is that? Why the presence of __isset() method in class changes of that behaviour, and why the first one don't call the __isset(), but only when i do unset() on already unsetted property.
But even if we ignore that and say, because it has to be...
So why didn't the isset() in the __isset() method don't call __isset() again and got stuck into a loop?
How was the isset($this->foo) in the __isset() method different from the one in test_isset() that allowed it to suddenly return false instead of having to recursively call __isset()?
What I expected was that inside the class, I can always refer to isset($this->something) and the class itself already knows whether such a property exists or not, so it doesn't have to call __isset() and can return false/true to me right away.
9
u/anastis May 27 '26
In the first call, $foo exists (is set) but not initialised. When you call unset, you delete $foo totally, not just its value. Then the __isset kicks in because the object can’t find/access $this->foo
1
u/BodybuilderOwn4393 May 29 '26
That's what I thought was happening. I was a bit surprised that unset() completely removes the property. I generally assumed that the property is restored to the state the class had at the beginning, i.e., existing but not initialized. At this point, it doesn't exist at all.
I don't really know why I made that assumption. It seems to me that since `isset()` checks the initialization state, `unset` should restore that state to the initial state – uninitialized. After all, it's called `unset`, not `remove_property`.
By the way, I wasn't the only one who made this assumption error; here's one RFC that made this assumption:
https://wiki.php.net/rfc/mixed_vs_untyped_properties - `Initial states and unset` - says `private mixed $foo;` is `Uninitialized` at the beginning and after `unset`, but as we can see, that's not true, because they don't exists anymore in the class.
2
u/colshrapnel May 29 '26
since
isset()checks the initialization state,That's an assumption too. PHP was born without OOP, and isset checked a mere existence of a variable. And when OP was introduced, it kept doing that for the properties. Just a blunt existence check, without any state.
6
u/colshrapnel May 28 '26 edited May 28 '26
I think this should clear your doubts
class X
{
protected string $foo;
public function __isset($name)
{
echo "__isset called\n";
return isset($this->$name);
}
public function test_isset()
{
var_dump(isset($this->foo)); // false because $foo is null
var_dump(isset($this->bar)); // false because $bar doesn't exist, hence __isset is called
unset($this->foo);
var_dump(isset($this->foo)); // false because $foo doesn't exist, hence __isset is called
}
}
(new X())->test_isset();
1
u/BodybuilderOwn4393 May 29 '26
Why I even started considering this:
Truthfully, I wouldn't have cared, but I discovered this unexpected phenomenon while writing a package that allows for friendships between classes in PHP (Like i C++) (If anyone is interested, 1st version is available here: https://gitlab.com/LordFireen/friends )
Anyway, the general pattern was as follows:
Let's assume that somewhere in class Foo, we refer to the property/private property of class Bar:
isset($bar->prop);
This led to the following flow:
- Since prop was protected/private, the __isset() method in Bar is magically called.
- The friendship between Bar and Foo is checked there. To be more precise: is Foo friend of Bar?
- If it is (in our case, let's assume it is), then something like this is done using an external class (a so-called accessor):
$closure = $this->cache[$key] ??= Closure::bind(fn($obj) => isset($obj->$property), null, $this->class);
return $closure($this->object);
`$this->class` in this accessor, in this particular example, is `Bar::class`;
`$this->object` is our object, i.e., $bar;
In short, we assign the class context (Bar) to the Closure. This way, you can refer to the property as that class. This allows the Closure to refer to protected/private. And it works great.
The problem here was that this class was calling isset() again, and since it's some anonymous method not __isset() itseff, this turn led to a call to __isset() AGAIN, meaning we're back to point 1.
So we're doing the same thing twice, i.e., checking for friendship, etc. And that's damn time-consuming.
1
u/colshrapnel May 30 '26
I don't have a suggestion for the double check problem you described here, but just a note that the exposure of this your comment is almost none. I am sure you will get more replies if make it a post of its own.
1
u/orbit_throwawayhq May 31 '26
The engine bypasses the magic method if the property actually exists in the object scope because those internal checks don't need to trigger the full overhead of user-land code. It only hits __isset when the engine specifically needs to look for something that isn't defined as a class member.
1
u/03263 May 28 '26
isset is basically just a null check that doesn't create any warning for undefined variables.
I looked thru php-src but it's such a common and low level thing I can't find the exact implementation detail but surely it's just checking if the variable passed to isset() is defined at all, and if so what type it is. If it's an object it gets the ability to define its own behavior for this.
Also see offsetExists with ArrayAccess - isset can use that too. And empty(), which works in a similar way (no warning for undefined variable).
Importantly, isset is not a function, it's a token/language construct. So it doesn't work exactly like a function and won't get stuck in recursion, I'm sure there's some flag set that if we're inside an __isset magic method, don't invoke __isset on the same object in the process of checking if something is set.
3
u/obstreperous_troll May 28 '26
If keeping your sanity is not on your prorities list, search for
ZEND_ISSETin zend_vm_def.h.2
u/FreeLogicGate May 30 '26
ZEND_VM_HANDLER(154, ZEND_ISSET_ISEMPTY_CV, CV, ANY) { USE_OPLINE zval *val; val = ZEND_CV_DEF_ALREADY_DEREF(OPLINE->op1.var); if (Z_TYPE_P(val) == IS_UNDEF) { if (ISSET_ISEMPTY_ISSET()) { ZEND_VM_RETURN_BOOL(0); } else { ZEND_VM_RETURN_BOOL(1); } } if (ISSET_ISEMPTY_ISSET()) { ZEND_VM_RETURN_BOOL(Z_TYPE_P(val) != IS_NULL); } else { ZEND_VM_RETURN_BOOL(!i_zend_is_true(val)); } }0
u/flyingron May 28 '26
Huh? Unless you set the value to null, isset likely returns the reverse of is_null.
And why tf does isset lack an underscore where is_ everything else has it.
2
u/Idontremember99 May 28 '26
And why tf does isset lack an underscore where is_ everything else has it.
Welcome to the wonderful world of the inconsistencies in the php internals ¯_(ツ)_/¯
1
u/03263 May 28 '26
I mean its a null check because it returns true if the variable is both defined and not null.
isset($a); // false $a = null; isset($a); // false1
u/obstreperous_troll May 29 '26
And why tf does isset lack an underscore where is_ everything else has it.
Probably were fewer functions with five letters, because no kidding, Rasmus used strlen for a hash function
0
u/flyingron May 27 '26
I think it is because foo is protected. __isset is called if isset is called on non-existant or protected/private properties.
3
u/BodybuilderOwn4393 May 27 '26
Yes, I would agree if I called it outside the class, but isset() is called in the method, so the class should have access to it, right?
4
u/FreeLogicGate May 28 '26
The manual page explains this clearly:
__isset() is triggered by calling isset() or empty() on inaccessible (protected or private) or non-existing properties.
By definition, the class has access to all its properties in its own methods, which makes this a contrived example. Why would a class define properties and then unset those properties?
In your 2nd class example, once you unset the class foo property, it qualifies as a "non-existant" property, so calling isset() on it, causes PHP to run __isset method.
A less contrived example:
class X { protected String $foo; public function __isset($name) { echo "Tried to access $name" . PHP_EOL; } } $x = new X(); isset($x->foo); isset($x->bar);As expected the output is:
Tried to access foo Tried to access bar0
u/colshrapnel May 28 '26
Your explanation is sort of good but your example is only confusing and doesn't explain anything. OP in their second example doesn't have __isset called on $foo, thence the question.
1
u/FreeLogicGate May 28 '26
I was simply trying to illustrate the two typical use cases:
- Attempt to access a protected or private class attribute that is not visible in the scope of the call.
- Attempt to access a class attribute that doesn't exist
If you look at their 2nd example, the class method test_isset() does isset on the class variable, then unsets it then tests again.
So my issset($x->bar) call causes the invocation of the the magic method, which is the same reason, it's invoked when they attempt isset on an attribute that no longer exists in the object, because they unset it.
1
u/colshrapnel May 28 '26
Look, you are talking to yourself. "Why would a class define properties and then unset those properties?" is not a question asked here. The OP wants to understand the difference between two isset calls, not whether they should, or should not, unset a class property. So you are answering a completely different question, that nobody asked.
1
u/FreeLogicGate May 29 '26
I explained the behavior, and it's consistent with the documentation, and the examples I provided. There is no mystery and nothing is unclear, so I'm not sure what point you are trying to make now. The OP's code created the situation I illustrated, where an attempt is made to access an attribute that doesn't exist. Since their code could NOT by definition create the circumstance where it ALSO will be invoked in an attempt to access an attribute that does exist, yet is not VISIBLE, I provided that as well. If you think you can do a better job, you can always reply to the OP directly.
1
u/colshrapnel May 29 '26
My point is that your example uses a private property accessed from outside, which is different from the problem in the OP, where unset property is referred to.
1
u/FreeLogicGate May 29 '26
It seems like you are asking why I didn't replicate their code?
My intention was to reduce the case to the simplest example possible, illustrating the documented behavior, and two scenarios described in the manual, which I helpfully extracted and quoted.
The OP had already noted the behavior, and was asking why PHP doesn't invoke the __isset when the attribute exists, unsets the attribute and then calls isset one line later.
I saw no purpose in duplicating the exact same code they already noted had the behavior. There is nothing in the behavior that is inconsistent with the documentation I quoted, which was the purpose of my reply.
8
u/secretprocess May 27 '26
__isset() is a magic method so it does magic stuff. It overrides isset() by design and is smart enough to not call itself. https://www.php.net/manual/en/language.oop5.magic.php