tags:

views:

241

answers:

10

Despite PHP being a pretty poor language and ad-hoc set of libraries ... of which the mix of functions and objects, random argument orders and generally ill-thought out semantics mean constant WTF moments....

... I will admit, it is quite fun to program in and is fairly ubiquitous. (waiting for Server-side JavaScript to flesh out though)

question: Given a class class RandomName extends CommonAppBase {} is there any way to automatically create an instance of any class extending CommonAppBase without explicitly using new?

As a rule there will only be one class definition per PHP file. And appending new RandomName() to the end of all files is something I would like to eliminate. The extending class has no constructor; only CommonAppBase's constructor is called. CommonAppBase->__construct() kickstarts the rest of the apps execution.

Strange question, but would be nice if anyone knows a solution.

Edit

Further to the below comment. The code that does the instantiation won't be in the class file. The class file will be just that, I want some other code to include('random.class.php') and instantiate whatever class extending CommonAppBase is in there.

For anyone unsure what I am after my hackish answer does what I want, but not in the sanest way.

Thanks in advance, Aiden

(btw, my PHP version is 5.3.2) Please state version restrictions with any answer.

Answers

The following can all be appended to a file (through php.ini or with Apache) to auto launch a class of a specific parent class.

First (thanks dnagirl)

$ca = get_declared_classes();
foreach($ca as $c){
    if(is_subclass_of($c, 'MyBaseClass')){
        $inst = new $c();
    }
}

and (the accepted answer, as closest answer)

auto_loader();
function auto_loader() {
    // Get classes with parent MyBaseClass
    $classes = array_filter(get_declared_classes(), function($class){
        return get_parent_class($class) === 'MyBaseClass';
    });
    // Instantiate the first one
    if (isset($classes[0])) {
        $inst = new $classes[0];
    }
}
+1  A: 

I think you will always need new.

If you want to load a class file and instantiate whatever class is in there, I think the only really reliable way is by using get_defined_classes() before and after the inclusion. Any difference (array_diff()) between those two lists you would instantiate. I have done this in past projects but wouldn't do it today any more, out of concerns for elegance and performance.

The best way IMO is to be really strict with naming. So that when you load random.class.php you define the convention that there is one class named random in there, and instanciate that automatically:

$classname = "random";

require "$classname.class.php";
$$classname = new $classname();  // Produces an object instance `$random`
Pekka
+1 for sound advice. These were my initial thoughts. I see your point about degrading the elegance. Your naming route is my next option.
Aiden Bell
+1  A: 

call_user_func_array(array($className,'__construct'),$args);

Mark Baker
+1 You get what I mean, but: `call_user_func_array() expects parameter 1 to be a valid callback, non-static method BaseApplication::__construct() cannot be called statically`
Aiden Bell
`call_user_func_array()` cannot call a constructor. A "expects parameter 1 to be a valid callback, non-static method MyClass::__construct() cannot be called statically" error is thrown. For details see http://blog.esfex.com/How-to-call-a-PHP-constructor-like-call_user_func_array.html
dnagirl
@dnagirl - It was close though.
Aiden Bell
@Aiden Bell: true, it was. In fact the only reason I knew it wouldn't work is because I was trying last week.
dnagirl
A: 

Just as a geek experiment, you could use the APD function override_function to override the include function.

In the new function, you could tokenise the included file (to get class definitions) and instantiate when a class is found.

adam
@adam - I was thinking this, along with other things like grepping out the `ClassFoo extends Base` bit with a regexp. If I can cache it, it may not be too bad. A bit of a lock-in though ;)
Aiden Bell
@Aiden no need to grep: http://www.php.net/manual/en/function.token-get-all.php
Pekka
Thanks Pekka - I should have put that link in my answer, that's just what I meant!
adam
@Pekka - See my attempted answer using `token_get_all()`, thanks!
Aiden Bell
+1  A: 

does this do what you want?

class CommonAppBase {}
class RandomName extends CommonAppBase {}
$klass = 'RandomName';
$instance = new $klass();
SorcyCat
@SorcyCat - The code performing `new` won't know the value of `RandomName` only that it extends `CommonAppBase`
Aiden Bell
How will it know that it extends CommonAppBase?
Zak
+2  A: 

I may be misinterpreting your problem but if you're looking for a single instantiation of a class to use throughout the application (like a database class) I would suggest you use a factory pattern. Then, anytime you need the class you can do something like....

$obj = &MyFactory::getClass('mysql_database_class');

I would definitely rule out instantiating your classes at the end of your class files. If you're trying to follow standard OOP principles then you should avoid this at all costs, as it can cause collisions elsewhere in your code.

Jarrod
Returning objects by references hasn't done anything for performance since PHP4, and it's deprecated in 5.3.
ryeguy
Isn't it just assigning the return value of "new" OBJ by reference that is deprecated? Apologies if I was unclear in my post. Store the object in a static variable and then on the factory call check to see if your static variable has been instantiated yet. If yes - return the object out of the static variable, if not start a new one.
Jarrod
+1  A: 

Assuming you have an __autoload function, what is the problem with new?

But if want a more indirect instantiation method, you can hide new inside a static method. So if your base class had these two methods:

static function make(array $args=NULL){
  $class=static::who(); //note: static, NOT self
  $obj=new $class($args);

  //some test for whether or not $obj is acceptable
  return ($test) ? $obj : false;  
}

abstract static function who(){
  return __CLASS__;
}

Then your RandomClass object can attempt instantiation this way:

$classname= 'RandomClass';
$someargs=array(1,2,3);

if(!$newobj= $classname::make($someargs)) die('cannot make new object');
dnagirl
@dnagirl, I don't know the value 'RandomClass' ahead of time.
Aiden Bell
@Aiden Bell: so when do you determine your 'RandomClass'? What is sending the request to make an object and why can't it send the class' name?
dnagirl
@dnagirl a script get's told simply to include a file. Short of naming conventions mentioned in other posts ... the loading script doesn't know the name of the class in the file, but has to instantiate it. I don't want to instantiate it in the class file itself.
Aiden Bell
@dnagirl - See my self-answer - gives a good idea of what I mean.
Aiden Bell
+1 for the other answer's comments.
Aiden Bell
A: 

Did you consider using Reflection to dynamically instantiate the classes?

$cls = new ReflectionClass('ClassName');
$obj = $cls->newInstance();

You can also retrieve its parent class.

nuqqsa
@nuqqsa - I don't know 'ClassName' at that point in the script's execution. I only know the base class.
Aiden Bell
Yet you know where your class files are or at least where to look for them. Using a meaningful directory/file naming should let you map file names to class names.
nuqqsa
+1  A: 

My Attempt, which does what i'm after (dubiously)

function auto_loader()
{
    $file = $_SERVER['SCRIPT_FILENAME']; // Some class file
    $cont = file_get_contents($file);
    $tokens = token_get_all($cont);
    for($i=0; $i<sizeof($tokens); $i++) {
        // Token = $token[$i][0]
        // Lexeme is $token[$i][1]
        if($tokens[$i][0]==T_EXTENDS && $tokens[$i+2][1]=="MyBaseClass"){
            // Get the lexeme of the class
            $class = $tokens[$i-2][1];
            break;
        }
    }
    $inst = new $class();
}
auto_loader();

Which is auto appended through Apache or whatever. Seems to work, but doesn't take into account no/varying whitespace ($i-2) between extends and BaseAppClass.

As a hack it seems to work, a bit more code and linking it to a cache and it might be a contender. Slightly over engineered.

So when http://www.foo.com/some_class_file.php is requested, the above is appended by Apache/php.ini and can instantiate the class in some_class_file.php and start it executing regardless of class name. This is because in my case, URL doesn't relate to the class. It could be a class MyDogBenjiClass being instantiated.

Attempt 2, thanks dnagirl The following is auto appended like the above to launch the app.

$ca = get_declared_classes();
foreach($ca as $c){
    if(is_subclass_of($c, 'MyBaseClass')){
        $inst = new $c();
    }
}
Aiden Bell
Looks nice, but running the tokenizer on dozens of files on every request...? I mean, PHP has to do that anyway when it parses the file, but still. Doesn't feel right to me, I would prefer a naming convention.
Pekka
hmm, now I understand what you want (probably!). What if you tried `get_declared_classes()` after each `include` and took the last entry in the array to get your `$classname`. You can check parentage with `is_subclass_of('MyBaseClass)` and then instantiate with `new $classname`.
dnagirl
@Pekka - It doesn't feel right to me either. I was hoping my question would throw up some more efficient methods. @ndagirl - I think that method would work ... (and be more efficient) i'll try it out.
Aiden Bell
@dnagirl - I have a solution at the end of this answer based on your method.
Aiden Bell
A: 
  1. map every URL to index.php
  2. find out the file from $_SERVER['REQUEST_URI'] (or whatever works for you)
  3. use variable classname to init ($main = new $file())

MVC frameworks typically do something like this, but in a more structured way:

  1. the request is redirected via mod_rewrite to index.php, which loads and runs some sort of dispatcher
  2. the dispatcher parses the url: http://example.com/foo/bar/baz is turned into $controller = 'foo', $action = 'bar', $param = 'baz'
  3. the dispatcher includes controllers/foo.php
  4. then instantiates it: $controllerInstance = new $controller()
  5. then calls the action and passes the parameter to it: call_user_func_array(array($controllerInstance, 'do'.ucfirst($action)), array($param))
Tgr
A good description of URL routing. But not what I am after ;)
Aiden Bell
+2  A: 

I'm not sure if the following is precisely what you are looking for, but it's an idea. The code should be pretty self-explanatory.

auto_loader();
function auto_loader() {
    // Get classes with parent MyBaseClass
    $classes = array_filter(get_declared_classes(), function($class){
        return get_parent_class($class) === 'MyBaseClass';
    });
    // Instantiate the first one
    if (isset($classes[0])) {
        $inst = new $classes[0];
    }
}

Note: the functions are available since early in the life of PHP 4 but the anonymous function syntax used with array_filter was only introduced in PHP 5.3.0.

salathe
+1 and accepted. Missed your answer hiding there, but me and dnagirl reached the same conclusion in a different way. Yours is closest and a full, complete answer.
Aiden Bell