Вложенный или внутренний класс в 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.abstract.php
Мэтью Бланкарт,
@DaveChen Я знаком с расширением классов, но я ищу лучшее решение ООП :( Спасибо.
Лиор Элром,
4
расширение - это не то же самое, что сдерживание ... когда вы расширяете, вы получаете дублирование класса User 3 раза (как User, как UserProfile и как UserHistory)
Tomer W

Ответы:

137

Вступление:

Вложенные классы относятся к другим классам немного иначе, чем внешние классы. Возьмем 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

НОТА:

Я действительно не думаю, что попытка эмуляции innerClasses в PHP - такая хорошая идея. Я думаю, что код менее чистый и читаемый. Кроме того, вероятно, есть другие способы достижения аналогичных результатов с использованием хорошо зарекомендовавшего себя шаблона, такого как шаблон Observer, Decorator или COmposition. Иногда достаточно даже простого наследования.

Тиви
источник
2
Классно @Tivie! Я собираюсь внедрить это решение в свою структуру расширений ООП! (см. мой 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";
Pascal9x
источник
4

Согласно комментарию Xenon к ответу Анила Озелгина, анонимные классы были реализованы в 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 для автозаполнения, обнаружения запаха кода и рефакторинга.

e_i_pi
источник
3

Вы не можете сделать это на PHP. PHP поддерживает "include", но вы не можете сделать это даже внутри определения класса. Здесь не так много хороших вариантов.

Это не дает прямого ответа на ваш вопрос, но вас могут заинтересовать «Пространства имен», ужасно уродливый \ синтаксис \ взломанный \ на \ вершине \ PHP ООП: http://www.php.net/manual/en/language .namespaces.rationale.php

дкамина
источник
Пространства имен, безусловно, могут лучше организовать код, но они не так эффективны, как вложенные классы. Спасибо за ответ!
Лиор Элром
почему вы называете это «ужасным»? Я думаю, что это нормально и хорошо отделено от других синтаксических контекстов.
emfi
2

Ожидает голосования как RFC https://wiki.php.net/rfc/anonymous_classes

Анил Озельгин
источник
1
Я не верю, что анонимный класс будет предлагать функциональность вложенного класса.
Эрик Джи
1
На странице RFC, если вы выполните поиск по запросу «вложенный», вы увидите, что он поддерживает. Не совсем то же самое с способом Java, но он поддерживает.
Анил Озельгин
3
Реализовано в PHP 7.
Élektra
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
    }

?>
Приябагус
источник