hackartist
hackartist

Reputation: 5264

Class variables holding a function in PHP

PHP allows for variables to hold functions like so:

$f = function($a,$b) {
   print "$a $b";
};
$f("Hello","World!"); //prints 'Hello World!'

This works just fine for me. I'm trying to pass a function into a class and set an instance variable to hold that function but with little luck:

class Clusterer {
    private $distanceFunc;
    public function __construct($f) {
        $this->distanceFunc = $f;
        print $f(1,7); //works
        print $this->distanceFunc(1,7); //exceptions and errors abound
    }
}
$func = function($a,$b) {
    return abs($a-$b);
}
$c = new Clusterer($func);

Am I doing something wrong here? The error is that the function doesn't exist so my guess currently is that it looks for a class function with that name (which there isn't one) and then gives up rather than looking for variables as well... how can I make it view the $this->distanceFunc as a variable?

EDIT: So after the advice from the answers below, I found a solution which was the make a function to wrap the invocation. For example my class is now:

class Clusterer {
    private $distanceFunc;
    public function __construct($f) {
        $this->distanceFunc = $f;
        print $f(1,7); //works
        print $this->distanceFunc(1,7); //exceptions and errors abound
    }
    private function distanceFunc($a,$b) {
        $holder = $this->distanceFunc;
        return $holder($a,$b);
    }
}
$func = function($a,$b) {
    return abs($a-$b);
}
$c = new Clusterer($func);

and this works great. Php looks for functions first and can only tell if it is a variable by context I guess is the moral of this story.

Upvotes: 9

Views: 2348

Answers (6)

IMSoP
IMSoP

Reputation: 97688

In PHP, methods and properties of an object occupy separate namespaces. This is different from JavaScript, for example, where foo.bar = function() {} is a perfectly valid way of defining a method.

Consequently, $this->distanceFunc(1,7); looks for a method named distanceFunc on the current class, and the classes it inherits from, but never looks for the property which you happen to have given the same name.

One solution is to force PHP to look up a property, then execute it, e.g. $foo = $this->distanceFunc; $foo(1,7) or call_user_func($this->distanceFunc, 1, 7). Using parentheses around the property lookup also does the same: ($this->distanceFunc)(1, 7);

Another would be to define the magic method __call on your class, which gets run whenever a non-existent method is referenced. Something like this ought to work (I don't have an easy way to testright now):

function __call($func, $args) {
     if ( property_exists($this, $func)  && is_callable($this->$func) ) {
           return call_user_func_array($this->$func, $args);
      }
 }

Note that this still isn't the same as a real method, for instance in terms of access to private properties.

Upvotes: 4

Ryan
Ryan

Reputation: 14649

PHP doesn't have first class functions. In JavaScript if you returned a function you could do this: myFunctionThatReturnsAFunction()(1,2), but not in PHP.

<?php

class Clusterer {

    private $distanceFunc;

    public function __construct(Closure $f) {
        $this->distanceFunc = $f;
    }

    public function getDistFunc()
    {

      return $this->distanceFunc;
    }

}


$func = function($a,$b) {
    return abs($a-$b);
};
$c = new Clusterer($func);

$a = $c->getDistFunc();
echo $a(1,2);

Upvotes: 1

Matthew
Matthew

Reputation: 48284

In HHVM, you can do this:

<?php
class Foo
{
  public function __construct()
  {
    $this->bar = function() { echo "Here\n"; };

    ($this->bar)();
  }
}
new Foo();

But it's not yet supported in PHP. But, it will be in PHP 7 (there will be no release named PHP 6).

Upvotes: 2

Christian Gollhardt
Christian Gollhardt

Reputation: 17004

Take a look at call_user_func

class Clusterer {
    private $distanceFunc;
    public function __construct($f) {
        $this->distanceFunc = $f;
        print $f(1,7); //works
        print call_user_func($this->distanceFunc, 1, 7); //works too ;)
    }
}

$func = function($a,$b) {
    return abs($a-$b);
};

$c = new Clusterer($func);

Don't ask me what is the difference, but it works the way you want (One of the reasons i hate this language)

Upvotes: 1

pdizz
pdizz

Reputation: 4240

It looks like you're going for a strategy pattern here. IE you want to be able to inject different methods for calculating distance? If so there is a more "sane" way to do it.

You can define an interface to the classes you will use to store the strategy method ensuring that the class will always have the method calculate() for example which would be your distance calculation function. Then in the constructor of your Clusterer class, type check against the interface in the parameter and call calculate() on the object passed in.

Looks like this:

interface Calculateable
{
    public function calculate();
}

class MyDistanceCalculator implements Calculateable
{
    public function calculate()
    {
        // Your function here
    }

}

class Clusterer
{
    protected $calc;
    public function __construct(Calculateable $calc)
    {
        $this->calc = $calc;
        $this->calc->calculate();
    }
}

$myClusterer = new Clusterer(new MyDistanceCalculator());

Because you defined an interface, any object you pass in will have the calculate() function

Upvotes: 2

spacebiker
spacebiker

Reputation: 3865

Your code doesn't work because PHP interprets $this->distanceFunc(1,7) as a class method, but you can do the following:

class Clusterer {

    private $distanceFunc;

    public function __construct($f) {
        $this->distanceFunc = $f;
        print $f(1,7); //works
        print call_user_func_array($this->distanceFunc, array(1, 7));
//      print $this->distanceFunc(1,7); //exceptions and errors abound
    }
}
$func = function($a,$b) {
    return abs($a-$b);
};
$c = new Clusterer($func);

http://sandbox.onlinephpfunctions.com/code/cdc1bd6bd50f62d5c88631387ac9543368069310

Upvotes: 4

Related Questions