Jump to content

Php Inheritance, dynamic properties and new static() constructor


pepito

Recommended Posts

I come from a .NET world. Now entering these frigid php waters.

I found an example that got me a little confused. Of course, I am trying to apply OOP fundamentals to this php code but it doesn't make sense.

This is the class i am talking about.

<?php

namespace app\models;

class User extends \yii\base\Object implements \yii\web\IdentityInterface
{
public $id;
public $username;
public $password;
public $authKey;

private static $users = [
'100' => [
'id' => '100',
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
],
];

public static function findIdentity($id)
{
return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
}

public static function findByUsername($username)
{
foreach (self::$users as $user) {
if (strcasecmp($user['username'], $username) === 0) {
return new static($user);
}
}
return null;
}

public function getId()
{
return $this->id;
}

public function getAuthKey()
{
return $this->authKey;
}

public function validateAuthKey($authKey)
{
return $this->authKey === $authKey;
}

public function validatePassword($password)
{
return $this->password === $password;
}
}

Alright, it's obvious to me that in the method findByIdentity($id) all it's doing is creating a static new instance of User. This is the first thing that caught me off guard.

In .net you cannot create an instance of a static class.

Now, moving on. in that line

return isset(self::$users[$id])? new static(self::$users[$id]) : null;

the second thing that intrigues me is the following.

Since all you have in that array is a key/value collection....

private static $users = [
'100' => [
'id' => '100',
'username' => 'admin',
'password' => 'admin',
'authKey' => 'test100key',
],
'101' => [
'id' => '101',
'username' => 'demo',
'password' => 'demo',
'authKey' => 'test101key',
],
];

how does php determine that it has to create an User object? Reflection? Which leads me to the next question.... looking at the class that it inherits from, Object, in the constructor, there's in one parameter which is an array (one of the elements of the array above).

public function __construct($config = [])
{
if (!empty($config)) {
Yii::configure($this, $config);
}
$this->init();
}

BUT, this class in its constructor, is calling Yii::configure($this, $config) and in this method, the way I see it, Yii is adding to $this (the Object instance I am assuming, not the User one) the parameters that belong to User.

public static function configure($object, $properties)
{
foreach ($properties as $name => $value) {
$object->$name = $value;
}
return $object;
}

Seems to me like it's adding parameters dynamically to Object which will be accessed by User via the matching parameters.

Makes sense?

From my .net standpoint, $this in Object refers to Object instance itself, not to the User instance inheriting from it (like my friend says). I told him that's a violation of basic OOP principles and it's simply impossible.

Anyone that could make me understand about this?

Thank you.

Link to comment
Share on other sites

Alright, it's obvious to me that in the method findByIdentity($id) all it's doing is creating a static new instance of User. This is the first thing that caught me off guard.

In .net you cannot create an instance of a static class.

PHP does not have static classes. Rather, static is used for static class members, static variables in functions, and late static bindings (LSB).

 

It is the latter that is being used in this case. new static is a way of saying "create a new instance of whatever class was called when this method was invoked", which may make more sense if you consider that PHP does inheritance for static methods: if a parent class defines a static method then the child class inherits it too. Calling the static method on the child class will execute the one in the parent (if the child doesn't redefine it) and new static will then create an instance of the child class - where the method was called "from", in a sense. Compare that to new self which will always create an instance of the parent - where the method is defined.

 

the second thing that intrigues me is the following.

[...]

how does php determine that it has to create an User object? Reflection?

As mentioned above, LSB is the mechanism.

 

Which leads me to the next question.... looking at the class that it inherits from, Object, in the constructor, there's in one parameter which is an array (one of the elements of the array above).

[...]

BUT, this class in its constructor, is calling Yii::configure($this, $config) and in this method, the way I see it, Yii is adding to $this (the Object instance I am assuming, not the User one) the parameters that belong to User.

[...]

Seems to me like it's adding parameters dynamically to Object which will be accessed by User via the matching parameters.

Makes sense?

Yes. And yes, that is what it is doing. PHP lets you define object variables at runtime, but it's not a good practice to get into.

However it also lets you override property getting and setting when the properties don't exist, so more likely the yii\base\Object class does that and thus $object->property (which doesn't exist) will invoke the override and that function can do whatever it wants. (Often the value goes into an array of data.)

 

From my .net standpoint, $this in Object refers to Object instance itself, not to the User instance inheriting from it (like my friend says). I told him that's a violation of basic OOP principles and it's simply impossible.

Anyone that could make me understand about this?

Thank you.

You are correct in that $this behaves like it does in, like, every other OOP language: refers to the object instance of whatever class that may be. Meanwhile self refers to the class in which it is being used (regardless of inheritance), and of course there's also static with LSB.
Link to comment
Share on other sites

@requinix Thank you so much for your answer. Definitely helped me clear up some of my doubts.

 

Now, in plain English, let's see if I got this.

 

User creates an instance of itself by using new static... (since the method getIdentity($id) was invoked in User? or new static was invoked?). By the way, I forgot to mention that getIdentity($id) actually belongs to an interface and according to the documentation returns an IdentityInterface? Confusing...

 

Moving on.

 

if I say.... 

 

$user = User::getIdentity($id)... it returns an instance of User... or IdentityInterface?

 

Assuming it's User (for what I understood from your response....)

 

Since User doesn't have a constructor, but it's parent class does, it executes the code inside Object's constructor. 

This constructor receives an array of key-values that dynamically are added to Object. 

 

Then User, since it has public properties that "matches" the same ones added to Object, it sort of "channels" those values up to User.

 

Am I correct?

 

Now, a couple of questions that pop up while i was writing the paragraph above...

 

the reason they use new static and not new User is because findIdentity($id) is a static method that belongs to an interface? if I wanted to use new User I would've need to have a static factory method that returns an instance of User. So, basically new static is a "static factory method" for the class where new static was invoked on. Correct?

 

Here are the three classes i am talking about and the interface.

 

https://github.com/yiisoft/yii2/blob/master/framework/web/IdentityInterface.php

https://github.com/yiisoft/yii2/blob/master/framework/base/Object.php

http://www.yiiframework.com/wiki/490/creating-a-simple-crud-app-with-yii2-revised-12-20-2013/#hh11 (User class)

Link to comment
Share on other sites

I assume that "findIdentity" and "getIdentity" are the same method, just inconsistently named in these posts?

 

User creates an instance of itself by using new static... (since the method getIdentity($id) was invoked in User? or new static was invoked?).

If you call User::getIdentity() then yes. If you create a subclass of User, say "ForumUser", and call ForumUser::getIdentity(), then it will return an instance of ForumUser - even though the code executing resides in (and was inherited from) the User class.

 

By the way, I forgot to mention that getIdentity($id) actually belongs to an interface and according to the documentation returns an IdentityInterface? Confusing...

Yes. Not only can static methods be inherited, they can also be defined in interfaces. The documentation doesn't mean that the method has to return a new IdentityInterface, which isn't possible anyways because it's an interface, but that whatever object it does return has to be an object of a class that implements, or has a parent class that implements, IdentityInterface.

 

$user = User::getIdentity($id)... it returns an instance of User... or IdentityInterface?

Yes to both.

Because the method uses "new static" there are two possibilities for what it can return:

a) A new User, which is fine because User implements IdentityInterface, or

b) An object of a class that inherits from User, which is also fine because it inherits the implementation of IdentityInterface

 

The term instance of can be a bit vague at times so I've tried to be more precise. If you say "an instance of User" meaning that get_class($object) == "User" then the answer is "not necessarily" because you could subclass User and then getIdentity() would return something of that class. However if you said the alternative "an instanceof User", which is what's happening here, then the answer would be yes.

 

Assuming it's User (for what I understood from your response....)

In this case it is because there is no subclass of User.

 

Since User doesn't have a constructor, but it's parent class does, it executes the code inside Object's constructor.

This constructor receives an array of key-values that dynamically are added to Object. 

 

Then User, since it has public properties that "matches" the same ones added to Object, it sort of "channels" those values up to User.

Not quite the right explanation but the end result is correct. (I actually missed that those properties were defined when I said that the __get/set methods were "more likely" being used - they're not.)

 

$object in configure(), which was the same $this as passed from the constructor, is an instance of the User class and thus $this->id will attempt to get or set the "id" property on that User instance. Directly.

 

As for the $object->$name syntax, that touches on a more advanced subject, but the short explanation is that the code will attempt to get or set the property on the instance. So with $name = "id" then it will behave just like writing $object->id.

 

the reason they use new static and not new User is because findIdentity($id) is a static method that belongs to an interface?

The interface only requires that there (1) exist a method named "findIdentity" and (2) it has one parameter, and PHP will enforce it. Any class implementing that interface need only meets those two requirements - there is no requirement about how the method behaves or what, if anything, it returns (though the method's documentation gives clear indications to the developer as to what they should do).

 

The reason is that using "static" keeps the door open regarding subclassing User (such as with ForumUser). If they just wrote "new User" then subclasses would have to reimplement getIdentity() since the parent's would always return a User object.

 

if I wanted to use new User I would've need to have a static factory method that returns an instance of User. So, basically new static is a "static factory method" for the class where new static was invoked on. Correct?

Starting to lose me there.

 

new static is kinda a factory already. It saves you from having to write out and manage

class User ... {

	// now protected
	protected static $users = [...];

	public static function findIdentity($id) {
		return isset(self::$users[$id]) ? new self(self::$users[$id]) : null;
	}

}

class ForumUser extends User {

	public static function findIdentity($id) {
		return isset(User::$users[$id]) ? new self(User::$users[$id]) : null;
	}

}

class AdvancedForumUser extends ForumUser {

	public static function findIdentity($id) {
		return isset(User::$users[$id]) ? new self(User::$users[$id]) : null;
	}

}
See how ForumUser and AdvancedForumUser have the exact same implementation of findIdentity()? The only difference between them is the sole reason why they have to exist separately: which class the method returns a new instance of. Without LSB and new static, User doesn't know about the ForumUser or AdvancedForumUser classes and so it can't create instances of them, thus it falls on the subclasses to figure that out. With LSB the entire thing can be reduced to

class User ... {

	// back to private
	private static $users = [...];

	public static function findIdentity($id) {
		return isset(self::$users[$id]) ? new static(self::$users[$id]) : null;
	}

}

class ForumUser extends User { }

class AdvancedForumUser extends ForumUser { }
Link to comment
Share on other sites

@requinix.... Once again, thank you so much for your lengthy answers.

 

NOW i got it... the static part. Perfect.

 

One thing you got me confused about... let me quote.

 

$object in configure(), which was the same $this as passed from the constructor, is an instance of the User class and thus $this->id will attempt to get or set the "id" property on that User instance. Directly.

 

 

In this example I am presenting to you, $this inside of the constructor of the class Object, refers to Object, not to User. That's where I got confused.

 

In .net in order to have an instance of the child in the parent you have to pass it in the constructor...

public class Parent
{

private Child _child;

public Parent(Child child)
{

this._child = child; // see? This here means Parent. Not Child.
}
}


public class Child
{

public Child():base(this)
{
// See? this here means Child. That's how you pass a ref to Parent in .net
}
}

So, when you say that $this in the Object constructor refers to User, that part confuses me. 

 

Do you see my point?

Link to comment
Share on other sites

Maybe my use of the term "instance" is throwing you off.

 

What I'm talking about is

public class Parent {

	public Parent() {
		Console.WriteLine(this.GetType().Name);
	}

}

public class Child : Parent {

	public Child() : base() { }

}

Child c = new Child(); // Child, not Parent
PHP behaves the same way.

class ParentClass {

	public function __construct() {
		echo get_class($this);
	}

}

class ChildClass extends ParentClass { }

$c = new ChildClass(); // ChildClass
Continuing with that,

class Config {

	public static function set($object, array $data) {
		echo get_class($object);
		foreach ($data as $key => $value) {
			$object->$key = $value;
		}
	}

}

class ParentClass {

	public function __construct(array $data) {
		Config::set($this, $data);
	}

}

class ChildClass extends ParentClass {

	public $name;

}

$c = new ChildClass(["name" => "pepito"]); // ChildClass
echo $c->name; // pepito
Link to comment
Share on other sites

@requinix.... you're absolutely RIGHT and so I was :)... not really.

 

Here was my confusion...

 

from a .NET perspective, yes, during runtime, "this" in the Parent becomes the child when you create an instance of the child. Your example is perfect. As a matter of fact, you don't even need :base(). As long as there is a constructor in the parent class, it will step in.

 

What threw me off was this.

 

At "design" time, in .NET, "this" in the parent class, really responds to methods and variables that are in the class scope. In other words, you would get a compilation error. So, if I have a method in my Child, or properties, I wouldn't be able to reach them from my parent class while writing code. So, when I saw that Configure was taking $this as the first parameter, AND assigning values to some "properties", that's when I got confused and hit by the bus. I was like..."how can this method pass values to this class (Object = $this) when it doesn't have any of these members???? Dynamic assigning??? 

 

And that's what it was.

 

Pretty much if I create a method in my class Config and use reflection, I could've achieved the same in .NET. But of course, php already comes equipped with its own "reflection".

 

Now it all makes sense. 

 

Once again, THANK YOU TONS for your attention to this doubts. 

Link to comment
Share on other sites

Pretty much if I create a method in my class Config and use reflection, I could've achieved the same in .NET. But of course, php already comes equipped with its own "reflection".

PHP doesn't really have it's own built-in reflection, it is just lazy about validating things and does so at run time rather than compile time.

 

From a design perspective, your parent class should be completely un-aware of what methods or properties a child would define, so it makes sense that from within the parent's code, this should only ever reference an item defined on that parent (or one of it's parents). If you want the parent to be able to call methods defined in a child, then that parent should be abstract with those methods defined, but not implemented, eg:

abstract class Sortable {
   //define, but don't implement
   abstract protected function compare($a,$b);
  
   public function sort(){
       //Somewhere in here we call $this->compare($a,$b);
   }
}
Assuming the statement about .NET is true (I'm not that familiar with it) then .NET would enforce that design pattern at compile time by verifying anything you try and access exists. Since PHP doesn't do that you can get away with referencing things on a child and just run the risk of a fatal/notice error at runtime if you ever happen to try it with a child that doesn't define the method/property being referenced.
Link to comment
Share on other sites

Archived

This topic is now archived and is closed to further replies.

×
×
  • Create New...

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.