A JS Class implementaion for PHP
note - I've improved the inheritance API a bit.
After installing PHP5.3 on my server, I decided to finally start playing with Closures. JS fan as I truly am, I thought it would be nice to implement a Mootools-Class-like constructor in PHP. This is a spin-off, as originally, Class was intended to introduce OOP mechanism PHP already has into JS. But, JS has some cool stuff that PHP doesn't - mostly the ability to Monkey-Patch classes. So, Here comes a potentially useless-yet-cool JSClass implementation in PHP.
The basics
Mootool's Class is a factory for creating on-the-fly Classes. As such, It should not be used as a Class by itself. And so, borrowing from the Singelton pattern, we will make our __construct private, but allow generate to call it.
The constructor will register the variables so they could be used. I will also use PHP's getters and setters to use them. First of all, here is the complete Class:
<?php
class JSClass{
/**
* @var array a container for all of the Class's members and methods.
*/
private $params = array();
/**
* @param array $params a list of members and methods to assign to the Class
* @access private
* @return JSClass instance
*/
private function __construct(array $params){
$this->params = $params;
return $this;
}
/**
* replaces the private constructor to make it more clear that this is a factory.
*
* @param array|JSClass $params a list of members and methods to assign to the Class
* @param array $ext a list of members and methods to use for overriding original methods (used when extending a class)
*
* @access public
* @return JSClass instance
*/
static public function generate($params,$ext=array()){
if (is_array($params))
return new JSClass($params);
if ($params instanceof JSClass){
$params = array_merge($params->params,$ext);
return new JSClass();
}
}
/**
* calls a lambda, passing it $self as 1st parameter. Similar to JS's apply
* @param Closure $callback a method to run
* @param array $params an array of parameters to pass to the function
* @access public
* @return mixed whatever the method returns
*/
public function apply($callback,$params = array()){
//$self = &$this;
array_unshift($params,$this);
return call_user_func_array($callback,$params);
}
/**
* creates a new instance of the class
*
*/
public function create(){
$params = func_get_args();
$obj = new JSClass($this->params);
if (array_key_exists('init',$this->params) && is_callable($this->params['init'])){
$res = $obj->apply($this->params['init'],$params);
if ($res) return $res;//if initializer actualy returned a value
}
return $obj;
}
/**
* calls a method if exists in the parameter stack, using apply
*/
public function __call($name,$params){
$params = func_get_args(); //get all paramaters;
if (array_key_exists($name,$this->params) && is_callable($this->params[$name])){
$this->apply($this->params[$name],$params);
}
}
public function __get($name){
if (array_key_exists($name,$this->params)) return $this->params[$name];
}
public function __set($name,$value){
$this->params[$name]=$value;
}
public function __isset($name){
return array_key_exists($name,$this->params);
}
}
Things to notice -
- To create a new class, we call the
generatestatic method. This will return to us a working factory. - To instantiate it, we call the
createmethod. It will create a new instance of that class, and attempt to call aninitmethod if one exists. Any parameters passed to it will be passed along. - All methods called by this class have to pass through the apply method. This method will pass as a first argument a reference to
$this. - If the
generatemethod is passed another class, it will create a copy of it, thus allowing you to extend it without overriding the parent's methods.
How to use
First - a simple use case:
$Class1 = JSClass::generate(array(
'a' => 'aaa'
, 'init' => function($self){
echo 'starting';
$self->b = 'b';
}
, 'doSomething' => function($sef){
echo $self->a . $self->b;
}
));
$ins1 = $Class1->create(); //will echo starting
$ins1->doSomething(); //aabb
Extending a class is a bit more complex. We will need to use the use keyword to point to it's parent:
//assuming above Class1
$parent = &$Class1;
$Class2 = JSClass::generate($Class1, array(
'init' => function($self,$suffix) use($parent){
$self->a='bbb';
$self->apply($parent->init);
echo $suffix;
}
));
$ins2 = $Class2->create('ccc'); //will echo bbbccc
Lastly, there is a very important thing being sacrificed in this pattern - private methods. But, we can actually use scopes to get a similar result, as we do in JS:
$Class3 = call_user_func(function() use ($Class1){
//closest thing we have to self-invoking until 5.4
$private = function($self){
$self->a = 'ccc';
};
$parent = &$Class1;
return = JSClass::generate($Class1, array(
'init' => function($self) use ($parent,$private){
$self->apply($private);
$self->apply($parent->init);
};
));
});
$Class3->create(); //ccc
OK, so this is quite an overhead I agree. So is this entire exercise. The point is - PHP5.3 is cool, and we just created a viable, semi-usable alternative to PHP's class which allows Live class creation and monkey-patching. Have fun.
If you want, you can download the source from github.
Future addons?
One last thing that is missing here is a instanceof replacement. It isn't really that hard to implement, but the thing is that the above implementation isn't really all that clear about hierarchy, as any parent can be passed to the use keyword. This can actually prove to be an advantage on some points, but missing out on interfaces is a big problem, so I think I will add this (and a way to implement Interfaces).