Using __call in a clever way

hi guys

I have an interesting question for you guyz about magic methods in php. Although it’s quite easy to understand what they do I kinda have problem understanding why I would ever want to use them. I am especially interested in __call.

let’s outline some scenario: the client code call a method that doesnt belong to the public interface of the object we are dealing with and therefore __call is triggered.

And so what? I havent find a single article on the net that shows you a cool usage of this magic method. Can u guyz enlighten me please? Here’s some code we can start from:

class Foo {
   function __call($fun, $params) {
      
   }
}

$o = new Foo;
$o->not() // will trigger __call

Here’s a rather ridiculous, yet interesting way to use __call.

The original data-set could be an array of database entries-n-such.

Obviously not to be a practical object, for 2 reasons - it’s crafted wholly from my kitchen without testing and finally, it’s rubbish.


<?php
class storage
{
    protected $data;
    
    public function __construct($data)
    {
        $this->data = $data;
    }
    
    public function __call($method, $arguments)
    {
        if(true === array_key_exists($method, $this->data))
        {
            if(is_array($this->data[$method]) && count($arguments) > 0)
            {
                if(true === array_key_exists($arguments[0], $this->data[$method]))
                {
                    return new self($this->data[$method][$arguments[0]]);
                }
            }
            return $this->data[$method];
        }
    }
}

$data = array(
    'users'    =>    array(
        1    =>    array(
            'name'    =>    'sample',
            'age'    =>    21
        ),
        5    =>    array(
            'name'    =>    'sample',
            'age'    =>    25,
            'books'    =>    array(
                1    =>    array(
                    'title'    =>    'Moby Dick'
                )
            )
        )
    )
);

$storage = new storage($data);
echo $storage->users(1)->age(); #21
echo $storage->users(5)->age();    #25
echo $storage->users(5)->books(1)->title(); #Moby Dick
?>

You may want to proxy another object, instead of using inheritance.


class Foo {
   function __construct($o) {
       $this->otherObj = $o;
   }
   function __call($fun, $params) {
      return call_user_func_array(array($this->otherObj, $fun), $params);
   }
}

Using a ActiveRecord call() can become very handy in the creation of a generic count and find method.

$user = new User(4);
// how many blogs does the user have?
$total = $user->countBlogs();

The other is as a dynamic finder. Fetching a user then fetching their blogs.

$user = new User(4);
$blogs = $user->getBlogs();

All dynamic due to __call().

http://github.com/oddz/active_record/blob/2825ee437336e28ebd59ba1830442f9f2e5fcdc5/storage/active_record.php#LID246


<?php
  /**
   * This demo is partly based on a post by Anthony Sterling on the sitepoint forum.
   * It is partly based on a video by John Lebensold of killerphp.com
   * 
   * http://www.sitepoint.com/forums/showthread.php?t=636303
   * 
   * AnthonySterling
   * Twitter: @AnthonySterling
   * 
   * Here's a rather ridiculous, yet interesting way to use __call.
   * 
   * The original data-set could be an array of database entries-n-such.
   * 
   * Obviously not to be a practical object, for 2 reasons :
   * it's crafted wholly from my kitchen without testing and finally, it's rubbish.
   * 
   */

  /**
   * ===============================================
   *  USING __CALL INSTEAD OF __GET AND __SET
   * ===============================================
   * I would like to have a base class that provides inheritance, and which
   * uses __call for most generic method calls in order to get/set properties.
   * This is a small example of the concept. I have a working model that uses 
   * __set and __get that works very well, but...
   *
   * after watching a video by John Lebensold of killerphp.com
   * http://www.killerphp.com/tutorials/advanced-php/videos/advanced-php-6.php
   *
   * I realized that __call was a far better choice for what I wanted.
   * 
   * I have run into a small snag, and am hoping someone can help me solve it.
   *
   * In order to see the problem set $show_ugly_workaround to true at the end of 
   * the class, at the beginning of the demo portion.
   */

  class storage
  {
    protected $data;
    protected $aMessages = array();

    public function __construct($data)
    {
      $this->data = $data;
    }

    public function getBookArray($user_no, $book_no)
    {
      /**
       * This handles returning information about a book in the form of an array.
       * The __call method below handles getting and setting all properties, and can
       * be used set arrays as well, but for some reason I was not able to get
       * __call to return an array, in particular I could not make the next line work.
       *
       * $aObj = $storage->getUsers(5)->getBooks(1);
       *
       * As an ugly workaround I wrote this method which returns the books array, but
       * I would have preferred to have __call return the array.
       */
      return $this->data['users'][$user_no]['books'][$book_no];
    }
    
    public function __call($method, $arguments)
    {
      /**
      * __call as I have set it up below, can set variable primitive types, as well
      * as arrays, but can only return primitives (no arrays) as I currently have it 
      * setup. I did not want to use __set and __get, I wanted to use __call only.
      * 
      * looking at the colors property which contains a multi-dimensional array
      * 
      * Using __call overloading as defined here:
      * 
      * you can set a primitive using : $storage->setBr("<br>");
      * you can get a primitive using : echo $storage->getBr();
      * 
      * you can set an array such as $colors as shown on the next line:
      * $storage->setColors
      * (
      * array
      *   (
      *     'foreground' => array
      *     (
      *       "header"  => "black",
      *       "block"   => "orange",
      *       "body"    => "black",
      *       <snip> ... </snip>
      *     )
      *   )
      * );  // End of : $storage->setColors
      * 
      * you can get any of the elements in the multi-dimensional array as follows:
      * echo "$storage->getColors('foreground')->getBody();   // returns "black"
      * 
      * but  I cannot retrieve an entire array using __call, by calling:
      *
      * $aObj = $storage->getUsers(5)->getBooks(1);   // fails to return the array
      * $aArray = (array) $aObj;            // casting to array does not help
      * 
      * I had to create a specific method: public function getBookArray($user_no, $book_no)
      * in order to retrieve the entire array, something I did not want to do. I was hoping 
      * to be able to do both set/get using __call and only create specific methods whenever 
      * internal calculations, or changing other affected variables, or other actions need 
      * to be taken.
      * 
      * The $message_no's are temporary until I come up with a finished version.
      */
      
      $methodPrefix = substr($method, 0, 3);
      $methodProperty = strtolower($method[3]) . substr($method, 4);
      $number_of_arguments = count($arguments);
      $message = "$number_of_arguments arguments were provided for \\$methodProperty : $methodProperty";

      switch($methodPrefix)
      {
        case "get":
              if(true === array_key_exists($methodProperty, $this->data))
              {
                $message_no = "0001";
                $this->aMessages[ ] = "[" . $message_no . "] " . $message;
                if(is_array($this->data[$methodProperty]) && $number_of_arguments > 0)
                {
                  $message_no = "0002";
                  $this->aMessages[ ] = "[" . $message_no . "] " . $message;
                  if(true === array_key_exists($arguments[0], $this->data[$methodProperty]))
                  {
                    $message_no = "0003";
                    $this->aMessages[ ] = "[" . $message_no . "] " . $message;
                    return new self($this->data[$methodProperty][$arguments[0]]);
                  }
                }
                $message_no = "0004";
                $this->aMessages[ ] = "[" . $message_no . "] " . $message;
                return $this->data[$methodProperty];
              }
              break;
        case "set":
              if(true === array_key_exists($methodProperty, $this->data))
              {
                if(is_array($this->data[$methodProperty]) && $number_of_arguments > 0)
                {
                  $this->data[$methodProperty] = $arguments;
                  $message_no = "0005";
                }
                else
                {
                  $message_no = "0006";
                }
              }
              else
              {
                if($number_of_arguments > 1)
                {
                  $this->data[$methodProperty] = $arguments;
                  $message_no = "0007";
                }
                else if($number_of_arguments == 1)
                {

                  $this->data[$methodProperty] = $arguments[0];
                  $message_no = "0008";
                }
                else
                {
                  $message_no = "0009";
                  $this->aMessages[ ] = "[" . $message_no . "] " . $message;
                  throw new Exception($message);
                }
              }
              $this->aMessages[ ] = "[" . $message_no . "] " . $message;
              break;
        default:

      }

    }
  }   // End of class storage

  // ===============================================
  //  DEMO BEGIN
  // ===============================================
  $show_ugly_workaround = false;
  
  echo "<h3> The Results...</h3>";
  
  
  $data = array(
    'users' => array(
      1 => array(
        'name' => 'sample',
        'age' => 21
      ),
      5 => array(
        'name' => 'sample',
        'age' => 25,
        'books' => array(
          1 => array(
            'title' => 'Moby-Dick (Collector\\'s Library)',
            'isbn_10' => '0393972836',
            'isbn_13' => '978-0393972832',
            'price' => '$9.95'
          )
        )
      )
    )
  );

  /**
   * var_dump limits resursion depth to 3 levels, so print_r works better here
   * var_dump($data);
   */


  echo "<pre>";
  $s = print_r($data, true);
  $s = str_replace("  ", "  ", $s);
  echo "\\$data : " . substr($s,5);
  echo "</pre>";
  

  $storage = new storage($data);

  $storage->setBr("<br>");
  $storage->setBr2("<br><br>");

  $storage->setHr("<hr style=\\"width:50%;float:left;\\"><br>");

  echo $storage->getHr();

  $storage->setColors
  (
  array
    (
      'primary' => array
      (
        "red"     => "#FF0000",
        "green"   => "#00FF00",
        "blue"    => "#0066CC",
        "gray"    => "#999999"
      ),
      'foreground' => array
      (
        "header"  => "black",
        "block"   => "orange",
        "body"    => "black",
        "footer"  => "gray",
        "border"  => "#CCCCCC"
      ),
      'background' => array
      (
        "header"  => "#FFFFEE",
        "block"   => "#EEEEEE",
        "body"    => "#FFFFFF",
        "footer"  => "#DDEEFF"
      )
    )
  );  // End of : $storage->setColors

  echo "\\$storage->getColors('primary')->getBlue() : " . $storage->getColors("primary")->getBlue() . $storage->getBr();
  echo "\\$storage->getColors('foreground')->getBody() : " . $storage->getColors("foreground")->getBody() . $storage->getBr();       
  echo "\\$storage->getColors('background')->getBody() : " . $storage->getColors("background")->getBody() . $storage->getBr();       
  echo $storage->getHr();

  echo 'var_dump($storage->getColors());';
  var_dump($storage->getColors());
  echo $storage->getHr();

  echo "\\$storage->getUsers(1)->getAge() : " . $storage->getUsers(1)->getAge() . $storage->getBr();
  echo "\\$storage->getUsers(5)->getAge() : " . $storage->getUsers(5)->getAge() . $storage->getBr();

  echo $storage->getHr();

  /**
   * I can get the values individually, but I would rather get the key/values
   */

  echo "Title : " . $storage->getUsers(5)->getBooks(1)->getTitle() . $storage->getBr();
  echo "ISBN-10 : " . $storage->getUsers(5)->getBooks(1)->getIsbn_10() . $storage->getBr();
  echo "ISBN-13 : " . $storage->getUsers(5)->getBooks(1)->getIsbn_13() . $storage->getBr();
  echo "Price : " . $storage->getUsers(5)->getBooks(1)->getPrice() . $storage->getBr();

  echo "<h3> The Broken Part...</h3>";
  echo $storage->getHr();

  /**
   * EVERYTHING works great up to this point...
   * 
   * However, I am doing something terribly wrong here, and can't get this to work.
   * $aArray is a portion of the original object, and not an array, so I tried casting it
   * to an array, and it shows up in the debugger as an array containing two elements, 
   * each being an array, but when I click on the elements in the debug variables window 
   * they change to the null.   
   */

  echo '$aObj = $storage->getUsers(5)->getBooks(1);' . $storage->getBr();
  $aObj = $storage->getUsers(5)->getBooks(1);

  echo 'var_dump($aObj);';
  var_dump($aObj);

  echo $storage->getHr();
  /**
   * here I would like to get the books(1) array from the $storage object
   * I tried looping through the $aObj using foreach(...) below, and that 
   * got really screwed up
   */

  if ($show_ugly_workaround)
  {
    $user_no = 5;
    $book_no = 1;
    $aArray = $storage->getBookArray($user_no, $book_no);
  }
  else
  {
    echo'$aArray = (array) $aObj;' . $storage->getBr();
    $aArray = (array) $aObj;
    
    // $aArray = get_object_vars($aObj);
  }
  
  echo 'var_dump($aArray);';
  var_dump($aArray);

  echo "<h3> The Really Broken Part...</h3>";
  echo $storage->getHr();
  
  /**
   * does not work, obviously I don't have a grasp on OOP yet. I seem to be mixing
   * apples and carrots, and ending up with mush...
   */
  $i = 0;
  foreach($aArray as $key => $value)
  {
    echo "[" . $i++ . "] $key :  $value" . $storage->getBr();
  }

  /**
   * threw this next line in here in order to set a breakpoint in the debugger that 
   * lets me view the variables just prior to exit.
   */

  $x = 5; 
  // ===============================================
  //  DEMO END
  // ===============================================
?>


I would be grateful for any help…

Bill Hernandez
Plano, Texas

I guess you could use call if you suspect that others could be using a class incorrectly, chuck an error or throw and exception, log it etc.

class Animal{

function walk(){
echo 'walks';

}

function __call( $method, $args ){

echo 'throw error because the method ' . $method . ' does not exist' ;

}

};

$horse = new Animal ;
$horse->neigh();

What does anyone think, is this a good thing to do?

My primary reason for using call is to avoid creating, deleting, modifying a myriad a of get/set methods whose only purpose in life is to return, or set a primitive property.

If you take a peek at the Jon Levensold videos (the end of video # 5, and beginning of video # 6) this precisely what he does.

http://www.killerphp.com/tutorials/advanced-php/videos/advanced-php-5.php
http://www.killerphp.com/tutorials/advanced-php/videos/advanced-php-6.php

This is part of the comment that I posted with the code (I’ve made some slight changes to try to try to better explain what I would like to accomplish.


  /**
  * __call as I have set it up below, can set variable primitive types, as well
  * as arrays, but can only return primitives (no arrays). Like I said before, 
  * as I currently have it setup.  
  * I do not want to use __set and __get, I wanted to use __call only.
  * 
  * looking at the colors property which contains a multi-dimensional array
  * 
  * Using __call overloading as defined here:
  * 
  * you can set a primitive using : $storage->setBr("<br>");
  * creates if required, and sets the br property.
  * you can get the value of a primitive using : echo $storage->getBr();
  * 
  * you can set an array such as $colors as shown on the next line:
  * $storage->setColors
  * (
  * array
  *   (
  *     'foreground' => array
  *     (
  *       "header"  => "black",
  *       "block"   => "orange",
  *       "body"    => "black",
  *       <snip> ... </snip>
  *     )
  *   )
  * );  // End of : $storage->setColors
  * 
  * you can get any of the elements in the multi-dimensional array as follows:
  * echo "$storage->getColors('foreground')->getBody();   // returns "black"
  * 
  * I can retrieve the value of a book title, an element of the array books
  * which basically was the example first written by AnthonySterling
  *
  * by calling : echo $storage->getUsers(5)->getBooks(1)->getTitle();
  * 
  * but I cannot retrieve an entire array using __call, by calling:
  *
  * $aObj = $storage->getUsers(5)->getBooks(1);   // fails to return the array
  * $aArray = (array) $aObj;            // casting to array does not help
  * 
  * I had to create a specific method: public function getBookArray($user_no, $book_no)
  * in order to retrieve the entire array, something I did not want to do. I was hoping 
  * to be able to do both set/get using __call and only create specific methods whenever 
  * internal calculations, or changing other affected variables, or other actions need 
  * to be taken.
  */

This actually shows it better:

http://www.journey-of-flight.com/bh_php/get_set_call/index.php