RSS
 

PHP 5.3 Class Friendship Support

23 Apr

One of the useful features of OOP languages like C++ is class friendship via the friend keyword. What this allows you to do is define a class that permits other explicitly named classes to touch its private parts.

For an overly simple example, let’s say you have a User class that carries its user info privately, but you also have a Logger class for writing error and info message to a log. If you tried the following, you’d hit errors telling you that the User properties “id” and “nick” are private:

class User {
  private $id;
  private $nick;
  // ...
}

class Logger {
  // ...
  public static function write($str) {
    $user = User::get_current_user();
    fwrite(self::$handle, "User: {$user->nick} ({$user->id}): {$str}\n");
  }
}

In C++ you could add a simple line to class User like:

friend class Logger;

This would permit the Logger class to access any of its private or protected properties and methods. Despite there being numerous people who demand that you should redesign your code instead of using class friendship, there are many cases where friendship is not only useful but necessary. This is all good and fine except for the fact that the PHP language does not have support for friend classes, nor do its developers have any intention of adding it (the last I heard from the PHP team is they long ago decided against such functionality).

This led me to develop a workaround, which wouldn’t have been possible in the past, but with the flexibility of PHP5.3 and late-static binding, it is now possible. So, without further ado, here’s my implementation of class Friendship.

class Friendship {
  private static function friend_error($i_type, $i_name, $i_sep = '->') {
    $bt = debug_backtrace();
    $caller = $bt[1];

    $msg = array();
    !empty($caller['file']) && $msg[] = $caller['file'];
    !empty($caller['line']) && $msg[] = "(Line {$caller['line']})";
    $msg[] = "attempted to access inaccessible {$i_type}:";
    $msg[] = get_called_class() . "{$i_sep}{$i_name}";
    $msg = implode(' ', $msg);

    trigger_error($msg, E_USER_ERROR);
  }

  private static function backtrace_test($i_callee = null) {
    $db = debug_backtrace();
    $i = 2;
    do {
      $bt = @$db[$i++];

      if (empty($bt['file']))
        $bt = null;
      else if (preg_match('#^(' . __NAMESPACE__ . ')?\\\\?(call_user_func|call_user_func_array)$#i', @$bt['function']))
        $bt = null;
      else if (@$bt['file'] == __FILE__ || __CLASS__ == @$bt['class'])
        $bt = null;
      else if ($i_callee) {
        if (0 == strcasecmp(get_called_class(), @$bt['class']) &&
            0 == strcasecmp($i_callee, @$bt['function']))
        {
          $bt = @$db[$i];
          break;
        }
      }
    } while ($i < count($db) && empty($bt));

    $class = @$bt['class'];
    $func = @$bt['function'];

    if (is_subclass_of(get_called_class(), $class))
      return true;

    $prep = function($s) {
		$ns = function($i_name) {
		  if (!is_scalar($i_name))
			return $i_name;
		  return strpos($i_name, '\\') !== false ? $i_name : (__NAMESPACE__ . '\\' . $i_name);
		};
		return strtolower($ns($s));
	};

    $friends = array_map($prep, static::get_friend_classes());

    if (strpos($class, '\\') === false)
      $class = '\\' . $class;
    return in_array(strtolower($class), $friends);
  }

  protected static function get_friend_classes() {
    return array();
  }
  
  public function __call($i_name, $i_args) {
    if (!method_exists($this, $i_name)) {
      $bt = debug_backtrace();
      $bt = $bt[1];
      trigger_error($bt['file'] . ' (Line ' . $bt['line'] . ') ' .
        'call to non-existent method ' . get_called_class() . '->' .
        $i_name . '()', E_USER_WARNING);
      return null;
    }

    !$this->backtrace_test($i_name) && self::friend_error('method', $i_name . '()');

    $rm = new \ReflectionMethod($this, $i_name);
    $accessible = $rm->isPrivate() || $rm->isProtected();
    $rm->setAccessible(true);
    $ret = $rm->invokeArgs($this, $i_args);
    $rm->setAccessible($accessible);
    return $ret;
  }

  public static function __callStatic($i_name, $i_args) {
    if (!method_exists(get_called_class(), $i_name)) {
      $bt = debug_backtrace();
      $bt = $bt[1];
      trigger_error($bt['file'] . ' (Line ' . $bt['line'] . ') ' .
        'call to non-existent static method ' . get_called_class() . '::' .
        $i_name . '()', E_USER_WARNING);
      return null;
    }

    !self::backtrace_test($i_name) && self::friend_error('static method', $i_name . '()', '::');

    $rm = new \ReflectionMethod(get_called_class(), $i_name);
    $accessible = $rm->isPrivate() || $rm->isProtected();
    $rm->setAccessible(true);
    $ret = $rm->invokeArgs(null, $i_args);
    $rm->setAccessible($accessible);
    return $ret;
  }

  public function __get($i_var) {
    if (!property_exists($this, $i_var)) {
      $bt = debug_backtrace();
      $bt = $bt[1];
      trigger_error($bt['file'] . ' (Line ' . $bt['line'] . ') ' .
        'attempted to access to non-existent property ' . get_called_class() . '->' .
        $i_var, E_USER_NOTICE);
      return null;
    }

    !self::backtrace_test() && self::friend_error('property', '$' . $i_var);
    return $this->$i_var;
  }

  public function __set($i_var, $i_value) {
    if (property_exists($this, $i_var))
      !self::backtrace_test() && self::friend_error('property', '$' . $i_var);

    $rp = new \ReflectionProperty($this, $i_var);
    $accessible = $rp->isPrivate() || $rp->isProtected();
    $rp->setAccessible(true);
    $rp->setValue($this, $i_value);
    $rp->setAccessible($accessible);
    return $i_value;
  }

  public function __isset($i_var) {
    if (!property_exists($this, $i_var))
      return false;

    !self::backtrace_test() && self::friend_error('property', '$' . $i_var);

    $rp = new \ReflectionProperty($this, $i_var);
    $accessible = $rp->isPrivate() || $rp->isProtected();
    $rp->setAccessible(true);
    $value = $rp->getValue($this);
    $rp->setAccessible($accessible);
    return $value !== null;
  }

  public function __unset($i_var) {
    if (property_exists($this, $i_var))
      !self::backtrace_test() && self::friend_error('property', '$' . $i_var);

    // we are not able to truly unset private properties, so we'll just make them null
    $rp = new \ReflectionProperty($this, $i_var);
    $accessible = $rp->isPrivate() || $rp->isProtected();
    $rp->setAccessible(true);
    $rp->setValue($this, null);
    unset($this->$i_var);
    $rp->setAccessible($accessible);
  }

  //
  // the functions below are for a future version of PHP
  //
  /*
  public static function __getStatic($i_var) {
  }

  public static function __setStatic($i_var, $i_value) {
  }

  public static function __issetStatic($i_var) {
  }

  public static function __unsetStatic($i_var) {
  }
  */
}


Just derive from Friendship and override the get_friend_classes() method. The latter is for global functions, not class methods. I designed it to take into consideration the current namespace and will adjust the specified friends accordingly.

Here’s an example of how you can use class Friendship:

class Restricted extends Friendship {
  private $priv_var;
  protected $prot_var;
  public $pub_var;

  protected static function get_friend_classes() {
    return array('Buddy');
  }

  private static function private_static_func() {
    echo __CLASS__ . '::' . __FUNCTION__ . '()<br>';
  }

  private function private_func() {
    echo __CLASS__ . '::' . __FUNCTION__ . '()<br>';
    echo 'I am a ';
    print_r($this);
  }
}

class Buddy {
  public static function touch_privates() {
    Restricted::private_static_func();

    $r = new Restricted();
    $r->priv_var = 'I changed this private property';
    $r->prot_var = 'Now a protected one';
    $r->pub_var = 'Public... not so special';

    $r->private_func();

    unset($r->priv_var);
    unset($r->prot_var);
    unset($r->pub_var);

    $r->private_func();

    echo "After unset()'ing, the public property is gone, but private and protected still exist as nulls.<br>";
  }
}

class Stranger {
  public static function deny_privates() {
    // none of these statements will work
    Restricted::private_static_func();

    $r = new Restricted();
    $r->priv_var = 'I cannot change this private property';
    $r->prot_var = 'Nor a protected one';
    $r->private_func();
  }
}

Buddy::touch_privates();
echo '<hr/>';
Stranger::deny_privates();

The output of the above example is:

MyNS\Restricted::private_static_func()
MyNS\Restricted::private_func()

I am a MyNS\Restricted Object
(
[priv_var:MyNS\Restricted:private] => I changed this private property
[prot_var:protected] => Now a protected one
[pub_var] => Public... not so special
)

MyNS\Restricted::private_func()

I am a MyNS\Restricted Object
(
[priv_var:MyNS\Restricted:private] =>
)

After unset()'ing, the public property is gone, but private and protected still exist as nulls.

2011-04-23 17:32:28 -0700 - *E_USER_ERROR*
/var/www/index.php (Line 52) attempted to access inaccessible static method: MyNS\Restricted::private_static_func()
/var/www/friendship.php (Line 17)

Some limitations are:

  • setting/getting static properties is not possible due to PHP’s lack of __getStatic(), __setStatic(), __issetStatic(), and __unsetStatic() magic methods
  • private properties cannot be unset(), so they’re just made null


I haven’t fully tested this in a great variety of examples, but at the very least, it does work as desired in all obvious, simple cases. I’m not pleased that I had to resort to using a lot of Reflection functionality, but I did just want some way to make any kind of friendship possible in PHP and I believe I’ve accomplished that. If you notice any issues or have any suggestions for improvement, please do let me know. Thanks for reading!

 
No Comments

Posted in PHP

 

78,481 views

Leave a Reply