Using PHP Anonymous Classes as Package Private Classes

I’ve written before about the benefits of using PHP’s Anonymous Classes for test doubles; but Anonymous Classes also have potential usecases within production code as well. In this article I’m going to describe one such usecase that can be particularly useful within libraries, and that is replicating the access of Package Private (in Java), or Protected Internal Classes (as per C#).

In Java, a Package Private class will be accessible to other classes in the same package but will be inaccessible to classes outside the package, meaning that it isn’t possible to create an object of that class or to declare a variable of that class type. In C#, Internal or Protected Internal Classes enable a group of components to cooperate in a private manner without being exposed to the rest of the application code. Access to Internal Classes is limited to the assembly in which they exist, or can be granted to specific “friend” assemblies.

This class visibility simply in’t available in PHP, but it could be useful for library writers to have that control over visibility of the classes inside their packages that are only intended for internal use.

My own usecase (with the PHPExcel/PHPSpreadsheet libraries as an example) for Package Private classes is based around the fact that a worksheet object contains a collection of cell objects, and a cell object should only be created using the appropriate getCell() method in the worksheet object, because this ensures that the connection between the two is correctly established. While most users of the library do build their spreadsheets correctly using the getCell() method as per the documentation, the existence of a Cell class within the library means that some users of the library mistakenly believe that they can instantiate a cell from within their own code, which has no connection to any worksheet object, and wonder why things don’t work as they expected… and there is no mechanism built into PHP itself that I can use to prevent them from doing so.
So what I want to achieve in PHP is a class with all its public methods still accessable from within userland code, so that the developer using the library can still read and set values and styling using the cell object’s public methods; but that cannot be instantiated from outside the library. It doesn’t exactly match Java’s Protected Private or C#’s Protected Internal; but in the absence of any class visibility within PHP, it’s the closest approximation that I’ve managed to achieve.

Let’s start with some example code, cell and worksheet class definitions.


class cell {
    protected $address;
    protected $value;
    public function __construct($address, $value = null) {
        $this->address = $address;
        $this->value = $value;
    }
    public function setValue($value = null) {
        $this->value = $value;
    }
    public function getValue() {
        return $this->value;
    }
}

class worksheet {
    protected $collection = [];
    public function getCell($address, $value = null)  {
        if (isset($this->collection[$address])) {
            return $this->collection[$address];
        }
        $cell = new cell($address, $value);
        $this->collection[$address] = $cell;
        return $cell;
    }
}

And the documented process to create a new cell is through the call to the worksheet’s getCell() method using


$worksheet = new worksheet();
$cellA1 = $worksheet->getCell('A1', 10);

But there is nothing to prevent somebody using


$cellA2 = new cell('A2', 20);

to instantiate a new cell object, but without that entry in the worksheet’s collection.

So let’s rewrite this with cell as an abstract class, so it cannot be instantiated directly, and then modify the getCell() method to create an anonymous class extending that abstract instead.


abstract class cell {
    protected $address;
    protected $value;
    public function __construct($address, $value = null) {
        $this->address = $address;
        $this->value = $value;
    }
    public function setValue($value = null) {
        $this->value = $value;
    }
    public function getValue() {
        return $this->value;
    }
}

class worksheet {
    protected $collection = [];
    public function getCell($address, $value = null)  {
        if (isset($this->collection[$address])) {
            return $this->collection[$address];
        }
        $cell = new class($address, $value) extends cell {
        };
        $this->collection[$address] = $cell;
        return $cell;
    }
}

Externally, we create a new cell in the worksheet in exactly the same way. From code outside the library, the only way to instantiate a cell object is by calling the getCell() method of the worksheet object — we can no longer use $cellA1 = new cell('A1', 10); because this will give a Fatal Error — or by explicitly writing a new concrete class (or another anonymous class) that extends the cell class and instantiating that. Neither of these could be considered a mistake; but rather it requires a conscious decision on the part of the developer using the library.

The difference now is that the cell object is of an anonymous class, but one that extends cell, so all the public methods defined in the abstract class are still accessible in userland code, and we can still typehint as an object of type cell.


Of course, there are limitations to using Anonymous Classes, and these can cause complications. For example, PHPExcel needs to be able to serialize cell objects when it uses cell caching, but it isn’t possible to serialize an instance of an anonymous class, even if it implements Serializable or has the magic __sleep()/__wakeup() methods defined. Attempting to serialize() an anonymous class will always throw an Exception.

But that doesn’t prevent us from writing our own serialize()/unserialize() methods; although we have to call them manually. For this we’ll use a Trait:

trait AnonymousSerialize {
    public function serialize() {
        $properties = [];
        foreach(get_object_vars($this) as $name => $value) {
            $properties[$name] = $value;
        }
        return serialize($properties);
    }
    public function unserialize($data) {
        foreach(unserialize($data) as $name => $value) {
            $this->{$name} = $value;
        }
    }
}

And apply it directly against our Anonymous Class rather than the abstract cell class, modifying the worksheet’s getCell() method to apply the Trait:

 
$cell = new class($address, $value) extends cell {
    use AnonymousSerialize;
};

Applying the serialize()/unserialize() methods to the Anonymous Class means that they’re not part of the public documentation for the cell, so library users shouldn’t be tempted to call them from their own code.

Of course, we do need to create a new cell through the worksheet getCell() method before we can unserialize the data to it, so internally we need to keep track of the details so that we match up the correct address reference; but as we’re only utilising serialize()/unserialize() from within our library code, we should be able to do so. We would also need to provide a custom magic __sleep()/__wakeup() in the worksheet class if we wanted to allow that to be serializable as well.

I’ve kept the class properties in this example simple too, so they are all protected rather than private, and all of them are instance properties rather than static properties, purely so I can provide a simple demonstration of the serialize()/unserialize() methods. If there were private cell properties or statics involved, then I’d need to provide a more complex mapper for those methods.

While I can resolve the issue of serialization, another drawback is that IDEs like PHPStorm no longer hint at the methods and properties of a cell object because it’s no longer an instance of class cell, but of the Anonymous Class, and even though this extends cell, the IDE doesn’t recognise this. If you’re working purely with an internal library class, where there should be no external access to the properties or methods, this may be less of a problem (though it is still awkward when working with an IDE); but if those methods need to be publicly available, and it’s only class instantiation that you want to control, then this approach probably isn’t recommended as a solution.


This is simply a proof of concept rather than a planned change to the PHPExcel or PHPSpreadsheet libraries; nor is it a definitive solution to implementing private classes within a library (core PHP functionality would be a far better option were it available). Rather, it is a demonstration of how anonymous classes could be used to create internal library classes, and some of the limitations with doing so.

This entry was posted in PHP and tagged , , , , . Bookmark the permalink.

6 Responses to Using PHP Anonymous Classes as Package Private Classes

  1. Pingback: Приватные классы. Сокрытие в php – CHEPA website

  2. Pingback: Mark Baker: Using PHP Anonymous Classes as Package Private Classes - webdev.am

  3. Clever, but an abstract protected class is also part of your public API – it’s literally an invitation to extend the class, at which point protected methods are part of your public API as well.

    To my knowledge, the closest thing we have in PHP today, is the @internal doc-block – I guess, in cases like this one, you might also get away with completely hiding an anonymous class inside a private static method. But it does start to look a bit like mangling the code to try to get something that PHP just doesn’t really offer at this time.

    Like

    • Iltar van der Berg says:

      You could always add a final annotation with “use at own risk”. Not perfect, but it’s something.

      /** @final do not extend, use at own risk */
      abstract class Foo {}

      Like

    • Vasiliy Pyatykh says:

      to avoid this “abstract class invitation” at all, maybe it is possible to just create the whole Cell class as an anonymous class, without extending?

      Like

Leave a comment