Ga direct naar


Friend classes in PHP

Monday 30 May 2011 15:30

Have you ever found yourself writing comments like this: "Do not use this function!! Used by class X only!!!"? Or even exposed some of a class' internal structures just to speed up your application? Then friend classes may be what you need.

By Patrick van Bergen

Intro

Encapsulation is one of the cornerstones of Object Oriented Programming. It means that the data structure of an object should not be exposed to the outside world, and this is because

Hiding the internals of the object protects its integrity by preventing users from setting the internal data of the component into an invalid or inconsistent state. A benefit of encapsulation is that it can reduce system complexity, and thus increases robustness, by allowing the developer to limit the interdependencies between software components.

However, there are some very good reasons why you might give access to the internal data to some external objects:

  • Processing speed
  • Unit testing
  • Design patterns

Given by Bjarne Stroustrup (in "The C++ Programming Language") as an example for the reason of why friend functions and classes were introduced to C++, consider a function that multiplies a vector by a matrix. If that function needed to access each element of the vector and matrix via a separate method call, it would be many times slower than needed.

In a unit test it is often necessary to determine the exact state of an object. It can often be deduced by calling the public getters, but these may not cover the entire state. What's more, these getters likely modify the state before outputting it. It is a valuable addition for white box testing.

Some designs are distributed over many objects. For example, take the repository pattern. The repository creates objects. At some point the user may tell the object to save (persist) itself. Since the actual saving is done by the repository, either the object tells the repository about its internal state, or the repository queries the object's state directly. Both ways, the internal state will need to be exposed.

So, why does PHP not have friend classes? I could not find a definate reason, but I'm sensing that it is considered 'bad design' and 'too complex'. [view]

Basic Implementation

For this PHP 5.3+ implementation you will need a base class. A class that should be extended by the classes that need to have friends. This base class provides the friend-architecture, so that it will need to be coded only once. The original code was posted by T. Steiner on php.net [view].

class MyBaseClass
{
    protected static $friendClasses = array();

    public function __get($name)
    {
        if (
            // check if the caller's class is one of the friend classes
            ($trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)) &&
             (isset($trace[1]['class']) && in_array($trace[1]['class'], static::$friendClasses))
        ) {
            return $this->$name;
        } else {
            trigger_error('Member not available: ' . $name, E_USER_ERROR);
        }
    }
}


Then all you need to do do make add a friend to your class is declare it.

class Pooh extends MyBaseClass
{
    protected static $friendClasses = array('Piglet', 'Tigger');

    protected $feelings = 'sad';
}

class Piglet
{
    public function beSensitive(MyBaseClass $Character)
    {
        echo 'You are so ' . $Character->feelings . '!';
    }
}


Now a Piglet can access a Pooh's feelings.

$Pooh = new Pooh();
$Piglet = new Piglet();
$Piglet->beSensitive($Pooh);


How does that work? When $Piglet accesses $Pooh's $feelings, PHP detects that $Piglet does not have access, because the member is not public. So PHP calls the magic function __get() instead. This function is overloaded and calls debug_backtrace() to determine the accessor of the member via the call stack. If the class of the accessor is one of the allowed classes, the member will be returned. If not, an error is triggered.

Note that the actual 'private' members still cannot be reached this way. The members need to be protected.

Extensions

Since 'debug_backtrace' is a relatively heavyweight function, you may want to circumvent it. For example, use the check only in you developing environment, but leave it out in your production code, by adding a simple 'production' test:

        if (
            MySettings::$inProduction ||
            (
                ($trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)) &&
                 (isset($trace[2]['class']) && in_array($trace[2]['class'], static::$friendClasses))
            )
        ) {


You may want to provide access to the member functions and static functions as well. Here is a full implementation.

class MyBaseClass
{
    protected static $friendClasses = array();

    public function __call($name, $args)
    {
        if (
            MySettings::$inProduction ||
            (
                ($trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)) &&
                 (isset($trace[2]['class']) && in_array($trace[2]['class'], static::$friendClasses))
            )
        ) {
            return call_user_func_array(array($this, $name), $args);
        } else {
            trigger_error('Function not available: ' . $name, E_USER_ERROR);
        }
    }

    public static function __callStatic($name, $args)
    {
        if (
            MySettings::$inProduction ||
            (
                ($trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)) &&
                 (isset($trace[2]['class']) && in_array($trace[2]['class'], static::$friendClasses))
            )
        ) {
            return call_user_func_array(array(get_called_class(), $name), $args);
        } else {
            trigger_error('Function not available: ' . $name, E_USER_ERROR);
        }
    }

    public function __get($name)
    {
        if (
            MySettings::$inProduction ||
            (
                ($trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)) &&
                 (isset($trace[1]['class']) && in_array($trace[1]['class'], static::$friendClasses))
            )
        ) {
            return $this->$name;
        } else {
            trigger_error('Member not available: ' . $name, E_USER_ERROR);
        }
    }
}

Final remarks

This technique may be a valuable addition to your OOP toolkit. If used sparingly and wisely, it may bring speed, improved encapsulation(!) and more profound tests. Used badly, it is just a lame excuse for making all members public.

This code can be extended to created more complicated access mechanisms. You may want to grant friend access to classes in the same package (as Java does with its keyword protected). Or you may grant access to classes in the same file. And you may limit access only to certain methods of certain classes (friend functions).

« Back

Reactions on "Friend classes in PHP"

No posts found

Log in to comment on news articles.

Procurios zoekt PHP webdevelopers. Werk aan het Procurios Webplatform en klantprojecten! Zie http://www.slimmerwerkenbijprocurios.nl/.


Hello!

We are employees at Procurios, a full-service webdevelopment company located in the Netherlands. We are experts at building portals, websites, intranets and extranets, based on an in-house developed framework. You can find out more about Procurios and our products, might you be interested.

This weblog is built and maintained by us. We love to share our ideas, thoughts and interests with you through our weblog. If you want to contact us, please feel free to use the contact form!


Showcase

  • Klantcase: Bestseller
  • Klantcase: de ChristenUnie
  • Klantcase: Evangelische Omroep
  • Klantcase: de Keurslager
  • Klantcase: New York Pizza
  • Klantcase: Verhage

Snelkoppelingen