phpclassoopnestedinner-classes

Nested or Inner Class in PHP


I'd like to create a User Class for my new website. However, this time, I was thinking of creating it a little bit differently...

Java, C++, and even Ruby (and probably other programming languages) allow the creation of nested/inner classes inside the main class, which allows us to make the code more object-oriented and organized.

In PHP, I would like to do something like so:

<?php
  public class User {
    public $userid;
    public $username;
    private $password;

    public class UserProfile {
      // some code here
    }

    private class UserHistory {
      // some code here
    }
  }
?>

Is that possible in PHP? How can I achieve it?


UPDATE
If it's impossible, will future PHP versions support nested classes?


Solution

  • Intro:

    Nested classes relate to other classes a little differently than outer classes. Taking Java as an example:

    Non-static nested classes have access to other members of the enclosing class, even if they are declared private. Also, non-static nested classes require an instance of the parent class to be instantiated.

    OuterClass outerObj = new OuterClass(arguments);
    outerObj.InnerClass innerObj = outerObj.new InnerClass(arguments);
    

    There are several compelling reasons for using them:

    If a class is useful to only one other class, then it is logical to relate and embed it in that class and keep the two together.

    Consider two top-level classes, A and B, where B needs access to members of A that would otherwise be declared private. By hiding class B within class A, A's members can be declared private and B can access them. In addition, B itself can be hidden from the outside world.

    A nested class usually relates to it's parent class and together form a "package"

    In PHP

    You can have similar behavior in PHP without nested classes.

    If all you want to achieve is structure/organization, as Package.OuterClass.InnerClass, PHP namespaces might sufice. You can even declare more than one namespace in the same file (although, due to standard autoloading features, that might not be advisable).

    namespace;
    class OuterClass {}
    
    namespace OuterClass;
    class InnerClass {}
    

    If you desire to emulate other characteristics, such as member visibility, it takes a little more effort.

    Defining the "package" class

    namespace {
    
        class Package {
    
            /* protect constructor so that objects can't be instantiated from outside
             * Since all classes inherit from Package class, they can instantiate eachother
             * simulating protected InnerClasses
             */
            protected function __construct() {}
            
            /* This magic method is called everytime an inaccessible method is called 
             * (either by visibility contrains or it doesn't exist)
             * Here we are simulating shared protected methods across "package" classes
             * This method is inherited by all child classes of Package 
             */
            public function __call($method, $args) {
    
                //class name
                $class = get_class($this);
    
                /* we check if a method exists, if not we throw an exception 
                 * similar to the default error
                 */
                if (method_exists($this, $method)) {
    
                    /* The method exists so now we want to know if the 
                     * caller is a child of our Package class. If not we throw an exception
                     * Note: This is a kind of a dirty way of finding out who's
                     * calling the method by using debug_backtrace and reflection 
                     */
                    $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS, 3);
                    if (isset($trace[2])) {
                        $ref = new ReflectionClass($trace[2]['class']);
                        if ($ref->isSubclassOf(__CLASS__)) {
                            return $this->$method($args);
                        }
                    }
                    throw new \Exception("Call to private method $class::$method()");
                } else {
                    throw new \Exception("Call to undefined method $class::$method()");
                }
            }
        }
    }
    

    Use case

    namespace Package {
        class MyParent extends \Package {
            public $publicChild;
            protected $protectedChild;
            
            public function __construct() {
                //instantiate public child inside parent
                $this->publicChild = new \Package\MyParent\PublicChild();
                //instantiate protected child inside parent
                $this->protectedChild = new \Package\MyParent\ProtectedChild();
            }
            
            public function test() {
                echo "Call from parent -> ";
                $this->publicChild->protectedMethod();
                $this->protectedChild->protectedMethod();
                
                echo "<br>Siblings<br>";
                $this->publicChild->callSibling($this->protectedChild);
            }
        }
    }
    
    namespace Package\MyParent
    {
        class PublicChild extends \Package {
            //Makes the constructor public, hence callable from outside 
            public function __construct() {}
            protected function protectedMethod() {
                echo "I'm ".get_class($this)." protected method<br>";
            }
            
            protected function callSibling($sibling) {
                echo "Call from " . get_class($this) . " -> ";
                $sibling->protectedMethod();
            }
        }
        class ProtectedChild extends \Package { 
            protected function protectedMethod() {
                echo "I'm ".get_class($this)." protected method<br>";
            }
            
            protected function callSibling($sibling) {
                echo "Call from " . get_class($this) . " -> ";
                $sibling->protectedMethod();
            }
        }
    }
    

    Testing

    $parent = new Package\MyParent();
    $parent->test();
    $pubChild = new Package\MyParent\PublicChild();//create new public child (possible)
    $protChild = new Package\MyParent\ProtectedChild(); //create new protected child (ERROR)
    

    Output:

    Call from parent -> I'm Package protected method
    I'm Package protected method
    
    Siblings
    Call from Package -> I'm Package protected method
    Fatal error: Call to protected Package::__construct() from invalid context
    

    NOTE:

    I really don't think trying to emulate innerClasses in PHP is such a good idea. I think the code is less clean and readable. Also, there are probably other ways to achieve similar results using a well established pattern such as the Observer, Decorator or composition. Sometimes, even simple inheritance is sufficient.