11

I have searched many a page of Google results as well as here on stackoverflow but cannot find a solution that seems to fit my situation. I appear to have but one last snag in the function I am trying to build, which uses call_user_func_array to dynamically create objects.

The catchable fatal error I am getting is Object of class Product could not be converted to string. When the error occurs, in the log I get five of these (one for each argument): PHP Warning: Missing argument 1 for Product::__construct(), before the catchable fatal error.

This is the code of the function:

public static function SelectAll($class, $table, $sort_field, $sort_order = "ASC")
{  
/* First, the function performs a MySQL query using the provided arguments. */

$query = "SELECT * FROM " .$table. " ORDER BY " .$sort_field. " " .$sort_order;
$result = mysql_query($query);

/* Next, the function dynamically gathers the appropriate number and names of properties. */

$num_fields = mysql_num_fields($result);
for($i=0; $i < ($num_fields); $i++)
{
  $fetch = mysql_fetch_field($result, $i);
  $properties[$i] = $fetch->name;
}

/* Finally, the function produces and returns an array of constructed objects.*/

while($row = mysql_fetch_assoc($result))
{
  for($i=0; $i < ($num_fields); $i++)
  {
    $args[$i] = $row[$properties[$i]];
  }
  $array[] = call_user_func_array (new $class, $args);
}

return $array;
}

Now, if I comment out the call_user_func_array line and replace it with this:

$array[] = new $class($args[0],$args[1],$args[2],$args[3],$args[4]);

The page loads as it should, and populates the table I am building. So everything is absolutely functional until I try to actually use my $args array within call_user_func_array.

Is there some subtle detail about calling that array that I am missing? I read the PHP manual for call_user_func_array once, and then some, and examples on that page seemed to show people just building an array and calling it for the second argument. What could I be doing wrong?

hakre
  • 193,403
  • 52
  • 435
  • 836
tuespetre
  • 1,827
  • 2
  • 18
  • 30

3 Answers3

20

You can't call the constructor of $class like this:

call_user_func_array (new $class, $args);

That's no valid callback as first parameter. Let's pick this apart:

call_user_func_array (new $class, $args);

Is the same as

$obj = new $class;
call_user_func_array ($obj, $args);

As you can see, the constructor of $class has been already called before call_user_func_array comes into action. As it has no parameters, you see this error message:

Missing argument 1 for Product::__construct()

Next to that, $obj is of type object. A valid callback must be either a string or an array (or exceptionally a very special object: Closure, but that's out of discussion here, I only name it for completeness).

As $obj is an object and not a valid callback, so you see the PHP error message:

Object of class Product could not be converted to string.

PHP tries to convert the object to string, which it does not allow.

So as you can see, you can't easily create a callback for a constructor, as the object yet not exists. Perhaps that's why you were not able to look it up in the manual easily.

Constructors need some special dealing here: If you need to pass variable arguments to a class constructor of a not-yet initialize object, you can use the ReflectionClass to do this:

  $ref = new ReflectionClass($class);
  $new = $ref->newInstanceArgs($args);

See ReflectionClass::newInstanceArgs

hakre
  • 193,403
  • 52
  • 435
  • 836
  • Thank you for your answer, however, I am not getting a 'no valid callback' error, and the error log seemingly indicates that the `call_user_func_array` does successfully call the construct, because it gives me a `PHP Warning: Missing argument 1 for Product::__construct()` for each of the five arguments the particular constructor is expecting but for some reason not receiving. Of course, it may be something I do not yet understand, but that is the knowledge I possess so far. – tuespetre Nov 05 '11 at 21:09
  • You first instantiate the object with `new $class`. That means the constructor has been already called before `call_user_func_array` get's into action. You see? First comes `new` because you use it as an expression in the parameter, then the function call happens. But because of new being executed, the constructor of `$class` is already called (with no arguments). And then PHP tries to convert the object into a string because the first parameter expects a string or array. Because your object does not support casting to strings, you get the error/warning. – hakre Nov 05 '11 at 21:13
  • I see! Thank you. I will look into other ways of dynamically passing arguments to a constructor. – tuespetre Nov 05 '11 at 21:14
  • @tuespetre: See the code example in the answer (the two lines of code at the end), that is the way you can do that. – hakre Nov 05 '11 at 21:20
  • I understand this ReflectionClass thing now. I want to thank you very much for answering. I can now do what i need! – tuespetre Nov 05 '11 at 23:21
2

Not possible using call_user_func_array(), because (as the name suggest) it calls functions/methods, but is not intended to create objects, Use ReflectionClass

$refClass = new ReflectionClass($class);
$object = $refClass->newInstanceArgs($args);

Another (more design-based) solution is a static factory method

class MyClass () {
  public static function create ($args) {
    return new self($args[0],$args[1],$args[2],$args[3],$args[4]);
  }
}

and then just

$object = $class::create($args);

In my eyes it's cleaner, because less magic and more control

KingCrunch
  • 128,817
  • 21
  • 151
  • 173
  • You mean you can call, for instance `Product::__construct($args)` where `$args` is an array of arguments? Can all functions be called using an argument array like that? – tuespetre Nov 05 '11 at 21:21
  • Where did I say something like that?!? Of course you can instanciate objects with arrays, but then you'll receive that array as single argument in the constructor. It doesn't look like thats what you want – KingCrunch Nov 05 '11 at 22:03
  • Sorry, I misread your post. :) – tuespetre Nov 05 '11 at 23:11
  • The ReflectionClass way is cleaner, as your method doesn't scale to more than 4 parameters. Your method will however be faster in execution time. – demonkoryu May 15 '15 at 14:32
  • @demonkoryu One may argue, that using reflection is a hack by itself, but of course you are right, that you need special methods every time the signature changes. However, this special methods are so common, that they even have a name: Factories :) On the other hand the answer is from 2011 and nowadays with variadics it's trivial to create a general purpose factory "thing", if you think it's a good idea to do so. – KingCrunch May 16 '15 at 17:53
-1

I use this for singleton factory pattern, becouse the ReflectionClass brokes the dependence tree, I hate the use of eval but its the only way to i find to simplificate the use of singleton pattern to inject mockObjects whith PHPUnit whitout open the class methods to that injection, BE CAREFULL WHITH THE DATA WHAT YOU PASS TO eval FUNCTION!!!!!!!! YOU MUST BE SURE THAT IS CLEANED AND FILTERED!!!

abstract class Singleton{
   private static $instance=array();//collection of singleton objects instances
   protected function __construct(){}//to allow call to extended constructor only from dependence tree
   private function __clone(){}//to disallow duplicate
   private function __wakeup(){}//comment this if you want to mock the object whith php unit jejeje

   //AND HERE WE GO!!!
   public static function getInstance(){        
    $a=get_called_class();
    if(!array_key_exists($a, self::$instance)){ 
        if(func_num_args()){
          /**HERE IS THE CODE **//
            $args=func_get_args();
            $str='self::$instance[$a]=new $a(';
            for($i=0;$i<count($args);$i++){
                $str.=(($i)?",":"").'$args['.$i.']';
            }
            eval($str.");");//DANGER, BE CAREFULLY...we only use this code to inject MockObjects in testing...to another use you will use a normal method to configure the SingletonObject
          /*--------------------------*/
        }else{
            self::$instance[$a]=new $a();
        }

    }
    return self::$instance[$a];     
}


}

And to use that:

class MyClass extends Singleton{
    protected function __construct(MyDependInjection $injection){
      //here i use the args like a normal class but the method IS PROTECTED!!! 
  }
}

to instanciate the object:

$myVar= MyClass::getInstance($objetFromClassMyDependInjection);

it calls the constructor whith the args I pased. i know that i can get the same result extending the static method getInstance but to teamworking its more easy to use this way

Diego
  • 1
  • 2