www.voj-tech.net


Friend Classes in vtCompose

When writing a larger piece of software we might face the problem of separating the solution into smaller pieces (components). Additionally we might want to restrict the accessibility of the components' internals from other components. As of PHP 5.3 we can at least structure our codebase into namespaces. However we still do not have the ability to group our classes, interfaces and other items into components marking individual items as internal within the components which define them or visible outside these components. In PHP we can only define visibility of class members with respect to the class in which they are declared.

This is very limiting, particularly when developing a distributable class library such as vtCompose. Generally speaking we have two options. The first option is to design all of our classes properly, separating the functionality appropriately (applying the SOLID principles if you like buzzwords) and hope that users will only access the items designated as the library interface. The second option is to cram all of the internal logic into private members of our classes. As you can guess neither option is ideal.

In C#, types and members can have the internal access modifier which makes them accessible only within the same assembly. In C++ we can declare functions and classes as friends of a particular class granting them access to private and protected members of the class. Until something like the Class Friendship PHP RFC is implemented no such functionality is available in PHP but there are ways to emulate it. In this article we explain how vtCompose solves this problem by exposing private methods to selected objects using handshakes and closures.

Rather than describing the theory we look at a real use case as it exists in the framework. Let us have two classes called DataRow and DataTable. Any instance of the DataRow class shall be friend with a single instance of the DataTable class for the whole duration of the DataRow object lifetime. We are effectively modelling a one-to-many relationship. Being friend in this context means that the DataRow object is somehow able to call private methods on the associated DataTable object and vice versa, the DataTable object is able to call private methods on any of the associated DataRow objects. In vtCompose we can find these classes in the VTCompose\Data namespace (to learn more about the namespace see the Data Access page) and it should be noted that the actual situation in the framework is a little bit more complicated as there is a DataRowCollection object associated with each data table and it is only these collections which maintain the data rows. We have simplified the situation for the purposes of this article.

The Boilerplate

We start with including the vtCompose class loader, importing a few classes which we will use in the script and registering the class loader as well as the vtCompose error handler. It is optional to register the error handler but it might be considered a good practice. Should a PHP error occur the handler will throw a VTCompose\ErrorHandling\ErrorException while respecting the error_reporting php.ini directive (more on error handling on the Error Handling page).

require_once 'VTCompose/Autoloading/Autoloader.php';

use VTCompose\Autoloading\Autoloader;
use VTCompose\Collection\Dictionary;
use VTCompose\ErrorHandling\ErrorHandler;
use VTCompose\Exception\InvalidOperationException;

(new Autoloader())->register();
(new ErrorHandler())->register();

The DataRow class

First we define our cut down version of the VTCompose\Data\DataRow class. Any of our DataRow instances shall carry a boolean value specifying whether the instance is important or not, whatever it means. This is a made up property which does not exist in the actual VTCompose\Data\DataRow class. We merely use this fictional value later in this article to demonstate the friend relationship between objects.

A DataRow instance has a private property which holds the DataTable object associated with the row. We also define a plain public getter to retrieve the value of the property from outside of the object.

Let us step back a bit and define an important term. An accessor shall be an instance of the Closure PHP class representing an anonymous function declared in the context of an object and taking a method name as the first parameter and arguments to call the method with as the rest of the parameters. An accessor shall simply call the method on $this which is available inside of the function's scope. Whichever object gets hold of the accessor can effectively call private and protected methods on the object associated with the accessor.

Back to our DataRow class—let us define a private property called $tableAccessor to hold the accessor of the associated data table and also a plain private setter to set the value of the property.

The constructor requires an instance of the DataTable class and the first thing we do in the body of the constructor is that we call the DataTable::establishFriendshipWithRow() method on the provided data table passing $this and the accessor of $this. Soon we will see what this method does and why it is important to call it sooner rather than later in the constructor. To keep the code tidy we define a private getter to retrieve the data row accessor, which is even cached in a private property, but this is not essential and we could just declare the closure directly in the constructor in our case. Following that, the constructor initialises the value of the property holding the associated data table.

We define the DataRow::__clone() and DataRow::__wakeup() methods as private methods with an empty implementation. That way we prevent cloning and reconstructing our DataRow objects which would violate the integrity of our object model.

Finally, we define the business logic of our DataRow class, i.e. the DataRow::makeImportant() method. In this method we set the value of the aforementioned property to true but we also use the data table accessor to notify the associated DataTable object that this row has become important. As we will see below the DataTable::makeRowImportant() method is defined as private so that it is not possible to call it from outside of the DataTable class unless the caller has got the data table accessor. Note that we have to explicitly call the Closure::__invoke() method as opposed to just using parentheses with arguments after the property name. This is because the PHP grammar dictates such notation would be a DataRow method call instead.

class DataRow {

    private $important;
    private $table;

    private $accessor;
    private $tableAccessor;

    public function getTable() {
        return $this->table;
    }

    private function getAccessor() {
        if (!isset($this->accessor)) {
            $this->accessor = function($methodName, ...$parameters) {
                return $this->$methodName(...$parameters);
            };
        }

        return $this->accessor;
    }

    private function setTableAccessor(Closure $tableAccessor) {
        $this->tableAccessor = $tableAccessor;
    }

    public function __construct(DataTable $table) {
        $table->establishFriendshipWithRow($this, $this->getAccessor());

        // the rest of the constructor logic follows once friendship with data table is established
        $this->table = $table;
    }

    private function __clone() {
    }

    private function __wakeup() {
    }

    public function makeImportant() {
        $this->important = true;
        $this->tableAccessor->__invoke('makeRowImportant', $this);
    }

}

The DataTable class

In our DataTable class we define the DataTable::makeRowImportant() method taking an instance of the DataRow class as a parameter. As mentioned above the method is private. For illustration purposes the method does not have to actually do anything and we can get away with printing a simple message.

As was the case with the DataRow class we define a private property called $accessor and a private getter to retrieve the object accessor. It is worth noting that the property along with the getter would be exactly the same in any class and so they are good candidates to be defined in a Trait for reuse.

The constructor initialises another private property of the DataTable class which holds a dictionary mapping the associated data rows to their respective accessors. Although we actually do not make use of the dictionary in our example we would do in case we wanted to call private methods on the associated DataRow objects from the DataTable class.

Again, the definition of the private DataTable::__clone() and DataTable::__wakeup() methods with an empty implementation is necessary to ensure the integrity of our object model.

Lastly, the DataTable class contains the definition of the DataTable::establishFriendshipWithRow() method. Remember that this method is called from within the constructor of the DataRow class as the first thing. This is important because the method establishes a friendship between a newly created DataRow object and an existing DataTable object and this should be done as soon as the DataRow object is created if possible. The method needs to check that the provided data row is not friend with any data table yet, in other words, it is a newly created instance of the DataRow class. The method does this by trying to retrieve the associated instance of the DataTable class and testing whether the returned value is NULL or not. The method trusts the constructor of the DataRow class initialises objects properly so that when trying to retrieve the associated data table afterwards we always receive the correct associated object. Technically we just need to make sure that the constructor of the DataRow class first calls the DataTable::establishFriendshipWithRow() method and only after that it initialises the $table property. It might be considered a good practice though to always establish any friendships as the first thing in a constructor and only then follow with the rest of the constructor logic. After the provided DataRow object is successfully verified as not a friend with any data table the DataTable::establishFriendshipWithRow() method adds the provided data row accessor to the aforementioned dictionary held in a private property of the DataTable class. More importantly for our example the method also uses the accessor to call the private $tableAccessor property setter of the DataRow class, passing the data table accessor as an argument, and thus interlinks the DataRow and DataTable objects together. Because the provided data row is not friend with any data table yet we assume that the caller is the constructor of the DataRow class and therfore the provided data row accessor is not a malicious function. (We cannot be absolutely sure that the caller is the constructor. The mechanism is not entirely bulletproof. See below for discussion.)

The DataTable::establishFriendshipWithRow() method needs to be public to allow calling it from within the constructor of the DataRow class. (Note that at the same time it is the only non-private class member required for our mechanism of friend classes.) This is not a problem though because any user call would throw an exception signifying that the provided data row is already a friend with a data table. As explained above the DataRow::getTable() method would always return an object. Still, we mark the DataTable::establishFriendshipWithRow() method as internal to vtCompose.

As already mentioned any DataTable object holds a collection of all the associated data row accessors despite the fact that they are not used anywhere in the example. Let us say DataRow::verySpecialMethod() is a private or protected method taking a single parameter and we want to call it from within the DataTable class. Assuming we want to call the method on the $row object, passing $parameter as an argument, all we have to do is to reach for the right accessor in the dictionary and use it as we have seen before; $this->rowAccessors[$row]('verySpecialMethod', $parameter).

class DataTable {

    private $accessor;
    private $rowAccessors;

    private function makeRowImportant(DataRow $row) {
        echo "Making a row important...\n";
    }

    private function getAccessor() {
        if (!isset($this->accessor)) {
            $this->accessor = function($methodName, ...$parameters) {
                return $this->$methodName(...$parameters);
            };
        }

        return $this->accessor;
    }

    public function __construct() {
        $this->rowAccessors = new Dictionary();
    }

    private function __clone() {
    }

    private function __wakeup() {
    }

    /**
     * @internal
     */
    public function establishFriendshipWithRow(DataRow $row, Closure $rowAccessor) {
        if ($row->getTable() != NULL) {
            throw new InvalidOperationException('The DataRow is already friend with a DataTable.');
        }

        $this->rowAccessors->addAt($row, $rowAccessor);
        $rowAccessor('setTableAccessor', $this->getAccessor());
    }

}

Putting It into Action

Here is a piece of code which uses the classes we have just defined. It creates a data table and a data row associated with it. The code then demonstrates that when we call the DataRow::makeImportant() method on the data row the method calls the private DataTable::makeRowImportant() method on the associated data table.

$table = new DataTable();
$row = new DataRow($table);
$row->makeImportant();

As expected the output is:

Making a row important...

Further Discussion

First we should emphasise that it is not required for both sides of the relationship to exchange their accessors. In many cases a one-way friendship is perfectly fine. If we did not want to call private methods of the DataTable class from within methods of the DataRow class but only the other way around we would only have to provide any data table with the associated data row accessors.

In our example we have given receivers of accessors the ability to call any private or protected method on the associated objects. You might want to restrict the set of methods allowed to be called by having a clever logic in the body of the accessor anonymous function. Of course the accessor can take into account not only the method name but also any of the arguments. In fact you might want to implement an accessor which allows getting and/or setting properties rather than calling methods or an accessor which exposes both methods and properties. In any case it might be a good idea to keep the accessor logic as simple as possible.

In the example we have shown how to couple objects together allowing them to call each other's private and protected methods. Sometimes you might want to be able to access private and protected members of any object of a particular class. Such scenario requires a static dictionary of accessors available in the calling scope. A disadvantage of such setup is that in order to keep the amount of used memory to a minimum we need to remember to remove unused objects and accessors from the static dictionary (i.e. unregister them) along the way.

We have mentioned that the mechanism is not bulletproof. The DataTable::establishFriendshipWithRow() method takes an instance of the DataRow class as a parameter and the type is actually enforced by a type hint in the method argument list. This is important because we can safely call a getTable() method on the $row object and be sure that we are calling the DataRow::getTable() method. But what if we create a subclass of the DataRow class and override the method in it so that it always returns NULL? The type hint will happily allow an instance of the subclass and the system will fall apart. Long story short, there are countless ways of breaking the system by extending either of the DataRow and DataTable classes. One can define the classes as final making it impossible to extend them however that is quite a radical solution which may not be always acceptable. You might have to simply trust the user not to circumvent the mechanism by creating malicious subclasses of your library classes.

Finally, we should note that there is another way of tackling the lack of internal accessibility level and friend classes in PHP. One can use the debug_backtrace() PHP function in a public method to inspect the call stack and decide whether the method call is legitimate or not. Given the function name such usage of the function in production code is questionable and probably no one can guarantee that the function behaviour will stay the same in future versions of PHP. It would be also interesting to see a performance comparison of a solution similar to the one described in this article and a solution based on the debug_backtrace() PHP function.