PHP中的嵌套类或内部类

143

我正在为我的新网站构建一个用户类,不过这一次我想稍微有些不同...

C++Java 甚至Ruby(以及可能的其他编程语言)都允许在主类中使用嵌套/内部类,这使得我们可以使代码更面向对象和有组织。

在 PHP 中,我想要做如下操作:

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

    public class UserProfile {
      // some code here
    }

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

在PHP中是否可能实现?我该如何做到?


更新

如果不可能,未来的PHP版本是否会支持嵌套类?


8
PHP 中不可能实现。 - Eugene
你可以让它扩展User,例如:public class UserProfile extends Userpublic class UserHistory extends User - Dave Chen
你也可以从一个抽象的用户类开始,然后扩展它。http://php.net/manual/en/language.oop5.abstract.php - Matthew Blancarte
4
扩展并不等同于包含... 当你进行扩展时,用户类会被复制3次(作为User、UserProfile和UserHistory)。 - Tomer W
@MatthewBlancarte,那不是重点。 - Pacerier
显示剩余4条评论
11个回答

149

简介:

嵌套类与外部类的关系略有不同。以Java为例:

非静态嵌套类可以访问封闭类的其他成员,即使它们被声明为私有。此外,非静态嵌套类需要实例化父类的实例。

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

使用嵌套类有几个令人信服的理由:

  • 这是一种逻辑上将仅在一个位置中使用的类进行分组的方式。

如果一个类只对另一个类有用,则将其与该类相关联并嵌入该类中并将两者保持在一起是合乎逻辑的。

  • 它增加了封装性。

考虑两个顶级类A和B,其中B需要访问A的成员,否则这些成员将被声明为私有。通过将类B隐藏在类A中,可以声明A的成员为私有,并且B可以访问它们。此外,B本身也可以被隐藏在外部世界之外。

  • 嵌套类可以导致更可读和易于维护的代码。

嵌套类通常与其父类相关,并一起形成一个“包”。

在PHP中

即使没有嵌套类,您也可以在PHP中实现类似的行为。

如果你想要的只是结构/组织,例如Package.OuterClass.InnerClass,PHP命名空间可能足够使用。您甚至可以在同一文件中声明多个命名空间(尽管由于标准自动加载功能,这可能不可取)。

namespace;
class OuterClass {}

namespace OuterClass;
class InnerClass {}

如果您想要模拟其他特性,比如成员可见性,需要多付出一些努力。

定义“package”类

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()");
            }
        }
    }
}

使用案例

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();
        }
    }
}

测试

$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)

输出:

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

注意:

我真的不认为在PHP中尝试模拟内部类是一个好主意。我认为代码不够清晰易读。此外,可能有其他方法可以使用已经成熟的模式(如观察者、装饰器或组合)来实现类似的结果。有时,甚至简单的继承就足够了。


32

在2013年,PHP 5.6提出了具有public/protected/private可访问性的真正嵌套类作为RFC,但未能通过(截至2021/02/03,尚未进行投票,自2013年以来没有更新):

https://wiki.php.net/rfc/nested_classes

class foo {
    public class bar {
 
    }
}

至少,匿名类已经被引入到PHP 7中

https://www.php.net/manual/en/language.oop5.anonymous.php https://wiki.php.net/rfc/anonymous_classes

从这个RFC页面:

未来范围

此补丁所做的更改意味着命名嵌套类更容易实现(略微)。

因此,我们可能会在某个未来版本中获得嵌套类,但尚未决定。


最近的PHP 8怎么样? - T.Todua
1
@T.Todua 没有,什么也没发生。 - Fabian Schmengler
对我不起作用。Parse error: syntax error, unexpected 'class' (T_CLASS), expecting function (T_FUNCTION) or const (T_CONST) in C:\xampp\htdocs\fastAuth\Fast-Auth\class.FastAuth.php on line 5 - Shubham Gupta
是的,我正在使用 PHP 版本 7.3.16。 - Shubham Gupta
4
这就是我写的。这是一份RFC,尚未实现甚至未经批准。它可能永远不会到来。 - Fabian Schmengler

11

是的,不幸的是这是传统的方式。 - Lior Elrom
2
流畅接口与声明“嵌套”或“内部”类不同。 - Michael Niño

7
根据Xenon对Anıl Özselgin答案的评论,PHP 7.0已经实现了匿名类,这是目前最接近嵌套类的方式。以下是相关的RFC: 嵌套类(状态:已撤回) 匿名类RFC(状态:在PHP 7.0中实现) 匿名类文档 作为原始帖子的示例,您的代码将如下所示:
<?php
    public class User {
        public $userid;
        public $username;
        private $password;
        
        public $profile;
        public $history;

        public function __construct() {
            $this->profile = new class {
                // Some code here for user profile
            }
            
            $this->history = new class {
                // Some code here for user history
            }
        }
    }
?>

然而,这也带来了一个非常严重的警告。如果您使用像PHPStorm或NetBeans这样的IDE,并且将像这样的方法添加到User类中:

public function foo() {
  $this->profile->...
}

再见自动完成。即使您使用接口编码(SOLID中的I),也是如此,使用类似这样的模式:

<?php
    public class User {
        public $profile;
        
        public function __construct() {
            $this->profile = new class implements UserProfileInterface {
                // Some code here for user profile
            }
        }
    }
?>

除非您唯一调用 $this->profile 的方法是来自 __construct()方法(或任何定义 $this->profile 的方法),否则您将不会得到任何类型提示。 您的属性本质上对您的IDE“隐藏”,如果您依赖IDE进行自动完成、代码嗅探和重构,则会让生活变得非常困难。

如果你花时间编写界面,你可以输入你的变量(例如 public UserProfileInterface $profile;)来恢复自动补全。 - Shayan Toqraee
@ShayanToqraee 我在 PHP 版本为 7.1 时写下了这个答案,根据你提供的语法,看起来 PHP 7.4 现在已经支持了类型属性。 - e_i_pi

6
自从PHP 5.4版本,你可以通过反射强制创建私有构造函数的对象。这可用于模拟Java嵌套类。示例代码如下:
class OuterClass {
  private $name;

  public function __construct($name) {
    $this->name = $name;
  }

  public function getName() {
    return $this->name;
  }

  public function forkInnerObject($name) {
    $class = new ReflectionClass('InnerClass');
    $constructor = $class->getConstructor();
    $constructor->setAccessible(true);
    $innerObject = $class->newInstanceWithoutConstructor(); // This method appeared in PHP 5.4
    $constructor->invoke($innerObject, $this, $name);
    return $innerObject;
  }
}

class InnerClass {
  private $parentObject;
  private $name;

  private function __construct(OuterClass $parentObject, $name) {
    $this->parentObject = $parentObject;
    $this->name = $name;
  }

  public function getName() {
    return $this->name;
  }

  public function getParent() {
    return $this->parentObject;
  }
}

$outerObject = new OuterClass('This is an outer object');
//$innerObject = new InnerClass($outerObject, 'You cannot do it');
$innerObject = $outerObject->forkInnerObject('This is an inner object');
echo $innerObject->getName() . "\n";
echo $innerObject->getParent()->getName() . "\n";

3

我认为使用命名空间解决这个问题是一种优雅的方法。在我的情况下,内部类不需要知道它的父类(就像Java中的静态内部类)。例如,我创建了一个名为“User”的类和一个名为“Type”的子类,用作示例中用户类型(ADMIN、OTHERS)的引用。祝好。

User.php(User类文件)

<?php
namespace
{   
    class User
    {
        private $type;

        public function getType(){ return $this->type;}
        public function setType($type){ $this->type = $type;}
    }
}

namespace User
{
    class Type
    {
        const ADMIN = 0;
        const OTHERS = 1;
    }
}
?>

Using.php(调用“子类”的示例)

<?php
    require_once("User.php");

    //calling a subclass reference:
    echo "Value of user type Admin: ".User\Type::ADMIN;
?>

3
这一页在我的互联网搜索结果中一直出现,所以我认为即使这是一个8年前的帖子,我也应该加入进来。PHP5文档证明匿名类可以在类方法内定义。创建的对象可以扩展、实现甚至使用其他类、接口和特性。考虑以下工厂对象生产的面向对象编程范例。就像@e-i-pi指出的那样...
class Factory {
    /**
     *  Method to manufacture an inner-class object.
     *
     *  @param  string  $args   Arguments to be passed to
     *                          the inner-class constructor.
     */
    static function manufacture_object($args) {
        /**
         *  Here's the definition of the inner-class.
         */
        return new class($args) {
            static $remembers = 'Nothing';
            private $args;
            function __construct($args) {
                $this->$args = $args;
            }
            function says() {
                return $this->args;
            }
        };
    }
}

/**
 *  Create an inner-class object and have it do its thing.
 */
$mort = Factory::manufacture_object("Hello World!");
echo $mort->says();         // Echoes "Hello World!"

这段文字的意思是:这些对象只有一个,因此我们预计从一个实例到另一个实例返回的对象的静态值不会绑定。毕竟,匿名类在一个对象与另一个对象之间是唯一的。然而,晚期静态绑定的工作方式与嵌套类的预期相同。
$mort = Factory::manufacture_object("I can remember that.");
$mort2 = Factory::manufacture_object("I'll live vicariously through you.");
$mort::$remembers = 'Something';
echo $mort2::$remembers;    // Echoes "Something"

所以,这就是:自从2013年9月22日(大约在提出这个问题的时候)开始,内部/嵌套类和使用静态功能创建它们的对象已经成为可能。

3
您无法在PHP中实现此功能。PHP支持“include”,但您甚至不能在类定义内部使用它。这里没有很好的选择。
虽然这并没有直接回答您的问题,但是您可能会对“命名空间”感兴趣,这是PHP OOP的一个非常丑陋的语法: http://www.php.net/manual/en/language.namespaces.rationale.php

命名空间可以更好地组织代码,但它不像嵌套类那样强大。感谢您的回答! - Lior Elrom
为什么你称它为“可怕”?我认为它还好,并且与其他语法上下文分离得很好。 - emfi

3

在 PHP 7 中,您可以这样做:

class User{
  public $id;
  public $name;
  public $password;
  public $Profile;
  public $History;  /*  (optional declaration, if it isn't public)  */
  public function __construct($id,$name,$password){
    $this->id=$id;
    $this->name=$name;
    $this->password=$password;
    $this->Profile=(object)[
      'get'=>function(){
        return 'Name: '.$this->name.''.(($this->History->get)());
      }
    ];
    $this->History=(object)[
      'get'=>function(){
        return ' History: '.(($this->History->track)());
      }
      ,'track'=>function(){
        return (lcg_value()>0.5?'good':'bad');
      }
    ];
  }
}
echo ((new User(0,'Lior','nyh'))->Profile->get)();

这是此页面上唯一嵌套在外部类代码中并实际起作用的解决方案。上面的解决方案看起来很相似,但使用的是 new class{} 而不是我在这里写的方式 (object)[];,对我来说实际上无法正常工作,尤其在 PHP 版本 7.1.33 中更是如此。如果我尝试该解决方案,并放入任何方法并调用它,则出错输出为:
'PHP parse error: syntax error, unexpected "[function name (get)]" ..expecting function (T_FUNCTION) or const (T_CONSTANT) in (path) on line (line #)

即使我将函数更改为function get(){...},错误仍然存在。
'PHP Fatal Error: Uncaught Error: function name must be a string' in (path):(Line #)

对于最顶层的解决方案,虽然在引用'inner'类时像嵌套一样,但代码实际上并没有将内部类嵌套在外部类中。 enter image description here

此外,您可以采用这种范例并进行扩展,创建任意深度的内部类层次结构:

class Inner_Inner_Inner_Inner_class_demonstrator{
  public function __construct(){
    $this->Writing=(object)[
      'Files'=>(object)[
        'Third_Level_Inner_Class'=>(object)[
          'Fourth_Level_Inner_Class'=>(object)[
            'write'=>function($_what,$_where,$_append){
              $Handle;
              if(!$_append)$Handle = fopen($_where, 'w');
              else $Handle = fopen($_where, 'a');
              fwrite($Handle, $_what);
              fclose($Handle);
            }
          ]
        ]
      ]
    ];
  }
}
((new Inner_Inner_Inner_Inner_class_demonstrator())->Writing->Files->Third_Level_Inner_Class->Fourth_Level_Inner_Class->write)('four levels of inner classes!','tester.html',true);

2

1
我不相信匿名类能提供嵌套类的功能。 - Eric G
1
在RFC页面中,如果您搜索“nested”,则可以看到它有支持。虽然不完全与Java方式相同,但它是有支持的。 - Anıl Özselgin
4
使用PHP 7实现。 - Lux

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接