Вкладений або внутрішній клас на PHP


111

Я будую клас користувача для свого нового веб-сайту, однак цього разу я думав будувати його трохи інакше ...

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
Це неможливо в PHP
Євген

Ви можете розширити User, наприклад: public class UserProfile extends Userі public class UserHestory extends User.
Дейв Чен

Ви також можете почати з абстрактного класу користувача, а потім розширити його. php.net/manual/en/language.oop5.abrief.php
Метью Бланкарт

@DaveChen Я знайомий із розширенням уроків, проте шукаю кращого рішення OOP :( Thx.
Lior Elrom

4
розширення - це не те саме, що і утримування ... коли ви продовжуєте, ви отримуєте дублювання класу User 3 рази (як User, як UserProfile та як UserHistory)
Tomer W

Відповіді:


136

Вступ:

Вкладені класи відносяться до інших класів трохи інакше, ніж до зовнішніх класів. Візьмемо Java як приклад:

Нестатичні вкладені класи мають доступ до інших членів класу, що додається, навіть якщо вони оголошені приватними. Крім того, для нестатичних вкладених класів необхідний екземпляр батьківського класу.

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

Існує кілька вагомих причин їх використання:

  • Це спосіб логічного групування класів, які використовуються лише в одному місці.

Якщо клас корисний лише для одного іншого класу, то логічно пов'язати його та вбудувати його в цей клас та тримати їх разом.

  • Це збільшує інкапсуляцію.

Розглянемо два класи вищого рівня, A і B, де B потрібен доступ до членів A, які в іншому випадку будуть оголошені приватними. Приховуючи клас B в класі A, члени A можуть бути оголошені приватними, і 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

ПРИМІТКА:

Я дійсно не думаю, що спроба наслідувати InternalClasses в PHP - це така гарна ідея. Я думаю, що код менш чистий і читабельний. Також, ймовірно, існують інші способи досягнення подібних результатів, використовуючи чітко встановлений зразок, такий як Шаблон спостереження, Декоратор або СО2. Іноді навіть простого успадкування достатньо.


2
Це приголомшливо @Tivie! Я так буду реалізовувати це рішення в моїй програмі розширення OOP! (див. мій github: github.com/SparK-Cruz)
SparK

21

Реальні вкладені класи з public/ protected/ privateдоступності були запропоновані в 2013 році для PHP 5.6 як RFC, але не внесли її (ще немає голосування, жодного оновлення з 2013 року - станом на 2016/12/29 ):

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

class foo {
    public class bar {
 
    }
}

Принаймні, анонімні класи перетворилися на PHP 7

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

З цієї сторінки RFC:

Майбутня сфера застосування

Зміни, внесені цим патчем, означають вкладені класи, які легше здійснити (невеликим шматочком).

Отже, ми можемо отримати вкладені класи в якійсь майбутній версії, але це ще не вирішено.


12

Ви не можете цього зробити в PHP. Однак є функціональні способи цього досягти.

Для отримання більш детальної інформації, будь ласка, перегляньте цю публікацію: Як зробити PHP вкладений клас або вкладені методи?

Такий спосіб реалізації називається вільним інтерфейсом: http://en.wikipedia.org/wiki/Fluent_interface


Так, на жаль, це звичайний спосіб
Ліор Елром

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";

4

Відповідно до коментаря Ксенона до відповіді Аніла Езсельгіна, анонімні класи були реалізовані в 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
            }
        }
    }
?>

Це, однак, має дуже неприємний застереження. Якщо ви використовуєте IDE, такий як PHPStorm або NetBeans, а потім додайте такий Userклас до класу:

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

... до побачення автоматичне завершення. Це так, навіть якщо ви кодуєте інтерфейси (I у SOLID), використовуючи такий зразок:

<?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 для автоматичного завершення, нюху коду і рефакторингу.


3

Ви не можете зробити це в PHP. PHP підтримує "включати", але ви навіть не можете цього робити у визначенні класу. Тут не так багато чудових варіантів.

Це не відповідає на ваше запитання безпосередньо, але вас можуть зацікавити "Простори імен", жахливо потворний \ синтаксис \ хакер \ в \ верх \ PHP OOP: http://www.php.net/manual/en/language .namespaces.rationale.php


Простори імен, безумовно, можуть краще організувати код, але він не такий потужний, як вкладені класи. Дякую за відповідь!
Ліор Елром

чому ви називаєте це "жахливим"? я думаю, що це добре і добре відокремлений від інших синтаксичних контекстів.
емфі

2

Він чекає голосування як RFC https://wiki.php.net/rfc/anonymous_classes


1
Я не вірю, що анонімний клас запропонує функціонал вкладеного класу.
Ерік Г

1
На сторінці RFC, якщо ви шукаєте "вкладені", ви можете бачити, що на ньому є підтримка. Не дуже точно з Java способом, але він підтримує.
Anıl Özselgin

3
Реалізовано в PHP 7.
Електра

2

Я думаю, що я написав елегантне рішення цієї проблеми, використовуючи простори імен. У моєму випадку внутрішньому класу не потрібно знати його батьківський клас (як, наприклад, статичний внутрішній клас на Java). Як приклад, я створив клас під назвою "Користувач" та підклас під назвою "Тип", який використовується у моєму прикладі як посилання на типи користувачів (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;
?>

2

Ви можете, як це зробити, у 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)();

-6

Покладіть кожен клас в окремі файли і "вимагайте" їх.

User.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();

        }            

    }

?>

UserProfile.php

<?php

    class UserProfile 
    {
        // Some code here
    }

?>

UserHistory.php

<?php

    class UserHistory 
    {
        // Some code here
    }

?>
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.