127

我正在为我的新网站构建一个用户类,但是这次我想构建它有点不同......

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 版本会支持嵌套类吗?

4

11 回答 11

145

介绍:

嵌套类与其他类的关系与外部类略有不同。以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 {}

如果您希望模仿其他特征,例如成员可见性,则需要更多的努力。

定义“包”类

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 中模拟 innerClasses 是一个好主意。我认为代码不太干净和可读。此外,可能还有其他方法可以使用成熟的模式(例如观察者、装饰器或组合模式)来实现类似的结果。有时,即使是简单的继承也足够了。

于 2013-05-07T17:17:11.347 回答
25

public具有//可访问protected性的真正嵌套类private在 2013 年作为 RFC 提出,用于 ​​PHP 5.6,但没有成功(尚未投票,自 2013 年以来没有更新 -截至 2021/02/03):

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

class foo {
    public class bar {
 
    }
}

至少,匿名类进入 PHP 7

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

从此 RFC 页面:

未来范围

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

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

于 2015-07-16T12:39:02.327 回答
11

不能在 PHP 中执行此操作。但是,有一些功能性的方法可以实现这一点。

有关更多详细信息,请查看这篇文章: 如何做 PHP 嵌套类或嵌套方法?

这种实现方式称为fluent interface:http ://en.wikipedia.org/wiki/Fluent_interface

于 2013-05-07T16:46:04.707 回答
6

根据 Xenon 对 Anıl Özselgin 的回答的评论,匿名类已在 PHP 7.0 中实现,它与您现在将获得的嵌套类一样接近。以下是相关的 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 进行自动完成、代码气味嗅探和重构,那么您的生活将变得非常艰难。

于 2017-01-31T23:14:22.350 回答
5

从 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";
于 2016-01-04T20:08:37.767 回答
4

你可以像这样在 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->name=$name;
    $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)();
于 2019-02-09T21:51:19.707 回答
3

你不能在 PHP 中做到这一点。PHP 支持“包含”,但您甚至不能在类定义中这样做。这里没有很多很棒的选择。

这并不能直接回答您的问题,但您可能对“命名空间”感兴趣,这是一种非常丑陋的\语法\黑客\on\top\of PHP OOP: http ://www.php.net/manual/en/language .namespaces.rationale.php

于 2013-05-07T16:43:56.257 回答
2

它正在等待投票作为 RFC https://wiki.php.net/rfc/anonymous_classes

于 2015-03-19T11:53:24.297 回答
2

我想我通过使用命名空间为这个问题写了一个优雅的解决方案。在我的例子中,内部类不需要知道他的父类(就像Java中的静态内部类)。作为示例,我创建了一个名为“User”的类和一个名为“Type”的子类,在我的示例中用作用户类型(ADMIN、OTHERS)的参考。问候。

User.php(用户类文件)

<?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;
?>
于 2016-08-22T16:20:24.457 回答
1

从 PHP 8.1 开始,它甚至更好。结合Anonymous 类New in initializers特性,你会得到这样的结果:

class User
{
    public $profile = new class {
        // Some code here
    };
    private $history = new class {
        // Some code here
    };

    public function __construct()
    {}
}

好处是,与 PHP7 天不同(由 e_i_pi 回答),您不需要在构造函数中定义类。这样,(1)构造函数不会被污染,并且(2)在许多嵌套类(甚至一个)的情况下,您不会过度缩进(即,当您定义了一个新的嵌套类)。很酷。

等等……但是如果你想多次实例化一个类怎么办?好吧,让我们结合更多的东西:您可以将箭头函数带入操作中(嗯,包括其他新功能作为其酱汁,如Constructor 属性提升EnumerationsReadonly 属性):

class UserHistory
{
    public $newAction = fn(...$args) => new class(...$args) {
        public function __construct(
            // Think ActionType a defined enumeration
            public readonly ActionType $type,
            public readonly string $description = '',
        ) {}
    };

    private array $actions = [];

    public function add($action)
    {
        $this->actions[] = $action;
    }
}

// Test
$userHistory = new UserHistory();
$userHistory->add(
    // Again, suppose ActionType enumeration is defined
    $userHistory->newAction(ActionType::CREATE_ACCOUNT)
);

令人印象深刻!是的,但它有一个主要缺点:您不能使用匿名类的类型(例如,查看UserHistory::add())。可能还有其他问题,例如编辑器提供的自动完成功能无法正常工作。但是,忍受它;因为当你获得其他东西时,你经常会失去一些东西。

于 2021-08-21T20:49:55.220 回答
-7

将每个类放入单独的文件中并“要求”它们。

用户.php

<?php

    class User {

        public $userid;
        public $username;
        private $password;
        public $profile;
        public $history;            

        public function __construct() {

            require_once('UserProfile.php');
            require_once('UserHistory.php');

            $this->profile = new UserProfile();
            $this->history = new UserHistory();

        }            

    }

?>

用户配置文件.php

<?php

    class UserProfile 
    {
        // Some code here
    }

?>

用户历史.php

<?php

    class UserHistory 
    {
        // Some code here
    }

?>
于 2017-11-24T22:59:28.480 回答