PHP - object context context - odd behavior - is this a PHP error?

I do not ask a typical question about why some code failed, but I ask why this worked. It worked with me during coding, and I needed it to crash.

Case

  • base abstract class with declared protected constructor
  • parent class extends abstract class with public constructor (Over ridding)
  • child class extends the same abstract class with a protected constructor

    abstract class BaseClass { abstract protected function __construct(); } class ChildClass extends BaseClass { protected function __construct(){ echo 'It works'; } } class ParentClass extends BaseClass { public function __construct() { new ChildClass(); } } // $obj = new ChildClass(); // Will result in fatal error. Expected! $obj = new ParentClass(); // that works!!WHY? 

Question

The parent class instantiates the child class object and it works! how did it happen? as far as I know, an object cannot be created if its constructor is declared protected, except only inside or from any subclass by inheritance.

The parent class is not a subclass of the child class, it does not inherit a dime from it (but both extend the same base abstract class), so how does the instantiation process not be interrupted?

EDIT

This case only happens with the abstract BaseClass, which also has an abstract constructor. If BaseClass is a concerete, or if its protected constructor is not abstract, instance creation fails, as expected. Is this a PHP bug? For my sanity, I really need an explanation of why PHP behaves this way in this particular case.

Thanks in advance

+6
source share
4 answers

Note: the following has been tested in PHP 5.3.8. Other versions may vary.

Since there is no formal specification for PHP, there is no way to answer this in terms of what is supposed to happen. The closest we can get is the protected statement from the PHP manual:

Declared protected members can only be accessed within the class itself and by inherited and parent classes.

Although a member can be overridden in ChildClass (preserving the "protected" specifier), it was originally declared in BaseClass , so it remains visible to descendants of BaseClass .

In direct contrast to this interpretation, compare the behavior for the protected property:

 <?php abstract class BaseClass { protected $_foo = 'foo'; abstract protected function __construct(); } class MommasBoy extends BaseClass { protected $_foo = 'foobar'; protected function __construct(){ echo __METHOD__, "\n"; } } class LatchkeyKid extends BaseClass { public function __construct() { echo 'In ', __CLASS__, ":\n"; $kid = new MommasBoy(); echo $kid->_foo, "\n"; } } $obj = new LatchkeyKid(); 

Conclusion:

  In LatchkeyKid:
 MommasBoy :: __ construct

 Fatal error: Cannot access protected property MommasBoy :: $ _ foo in - on line 18

Changing an abstract __construct to a specific function with an empty implementation gives the desired behavior.

 abstract class BaseClass { protected function __construct() {} } 

However, non-magical methods are visible to relatives, regardless of whether they are abstract (most magic methods should be publicly available).

 <?php abstract class BaseClass { abstract protected function abstract_protected(); protected function concrete() {} } class MommasBoy extends BaseClass { /* accessible in relatives */ protected function abstract_protected() { return __METHOD__; } protected function concrete() { return __METHOD__; } } class LatchkeyKid extends BaseClass { function abstract_protected() {} public function __construct() { echo 'In ', __CLASS__, ":\n"; $kid = new MommasBoy(); echo $kid->abstract_protected(), "\n", $kid->concrete(), "\n"; } } $obj = new LatchkeyKid(); 

Conclusion:

  In LatchkeyKid: 
 MommasBoy :: abstract_protected
 MommasBoy :: concrete

If you ignore warnings and declare magic methods (other than __construct , __destruct and __clone ) as protected , they appear to be available to relatives, as with non-magic methods.

Protected __clone and __destruct not available to relatives, regardless of whether they are abstract. This makes me think that the behavior of abstract __construct is a mistake.

 <?php abstract class BaseClass { abstract protected function __clone(); } class MommasBoy extends BaseClass { protected function __clone() { echo __METHOD__, "\n"; } } class LatchkeyKid extends BaseClass { public function __construct() { echo 'In ', __CLASS__, ": \n"; $kid = new MommasBoy(); $kid = clone $kid; } public function __clone() {} } $obj = new LatchkeyKid(); 

Conclusion:

  In LatchkeyKid:

 Fatal error: Call to protected MommasBoy :: __ clone () from context 'LatchkeyKid' in - on line 16 

Access to __clone is done in zend_vm_def.h (in particular, the ZEND_CLONE opcode handler). This is in addition to method access checks, so there may be other behavior. However, I do not see a special appeal for access to __destruct , so there is clearly more.

Stas Malyshev (hi, Stas!), One of the PHP developers, looked over __construct , __clone and __destruct and said the following:

In general, a function defined in a base class should be available to all [descendants] of this class. The rationale for this is that if you define (even abstractly) in your base class, you say that it will be available for any instance (including extended) of this class. So any descendant of this class can use it.

[...] I checked why ctor behaves differently, and this is because the parent ctor is considered the prototype for the ctor child (signed with enforce, etc.) only if it declared an abstract or an interface. So, by declaring ctor abstract or by making it part of the interface, you make it part of the contract and, therefore, accessible to all hierarchies. If you do not, ctors is completely unrelated to each other (this is different for all other non-static methods) and thus, if the parent ctor does not say anything about the ctor child, therefore the parent ctor visibility is not portable. So for ctor is not a mistake. [Note: this is similar to J. Bruni's answer.]

I still think this is most likely an error for __clone and __destruct.

[...]

I posted bug # 61782 to track issues with __clone and __destruct.

+3
source

Why does it work?

Because, from within ParentClass you provided access to the abstract method from BaseClass . This is the same abstract method that is called from ChildClass , despite the fact that its implementation is defined by itself.

Everything relies on the difference between a concrete and an abstract method.

You can think this way: an abstract method is the only method with several implementations. On the other hand, each particular method is a unique method. If it has the same name as its parent, it overrides the parent (it does not implement ).

So, when abstract declared, it is always called a base class method.

Think of the declared abstract method: Why can't the signatures of different implementations differ? Why can't child classes declare a method with less visibility?

Anyway, you just found a very interesting feature. Or, if my understanding of the above is wrong, and your expected behavior is really the expected behavior, then you have found an error.

+3
source

EDIT: the constructors act differenlty ... It is expected that it will work even without abstract classes, but I found this test that checks the same case and it looks like it's a technical limitation - the material described below does not work with constructors directly now.

There is no mistake . You need to understand that access attributes work with the context of objects. When you extend a class, your class will be able to see methods in the context of BaseClass. ChildClass and ParentClass in the context of BaseClass so that they can see all BaseClass methods. Why do you need it? For polymorphism:

  class BaseClass { protected function a(){} } class ChildClass extends BaseClass { protected function a(){ echo 'It works'; } } class ParentClass extends BaseClass { public function b(BaseClass $a) { $a->a(); } public function a() { } } 

No matter which child you pass to the ParentClass :: b () method, you will be able to access BaseClass methods (including protected ones, since ParentClass is a child of BaseClass, and children can see the protected methods of their parents). The same behavior applies to constructors. and abstract classes.

+2
source

I wonder if there is anything bad with the abstract implementation under the hood, or if there is a subtle problem that we do not see. Changing BaseClass from abstract to concrete creates a fatal error, after which you are later (classes are renamed for my convenience)

EDIT: I agree that @deceze says in his comments that this is a brief case of an abstract implementation and potentially a mistake. This is at least a workaround that provides the expected behavior of albiet with some ugly technique (fake abstract base class).

 class BaseClass { protected function __construct() { die('Psuedo Abstract function; override in sub-class!'); } } class ChildClassComposed extends BaseClass { protected function __construct() { echo 'It works'; } } // Child of BaseClass, Composes ChildClassComposed class ChildClassComposer extends BaseClass { public function __construct() { new ChildClassComposed(); } } 

PHP Fatal error: calling protected ChildClassComposed :: __ construct () from the context "ChildClassComposer" in / Users / quickshiftin / junk -php / change-private-of-another-class.php on line 46

+1
source

Source: https://habr.com/ru/post/909251/


All Articles