Zagnieżdżona lub wewnętrzna klasa w PHP


111

Buduję klasy użytkownika dla mojej nowej strony internetowej, jednak tym razem myślałem o zbudowaniu jej trochę inaczej ...

C ++ , Java, a nawet Ruby (i prawdopodobnie inne języki programowania) pozwalają na użycie zagnieżdżonych / wewnętrznych klas wewnątrz głównej klasy, co pozwala nam uczynić kod bardziej zorientowanym obiektowo i zorganizowanym.

W PHP chciałbym zrobić coś takiego:

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

    public class UserProfile {
      // some code here
    }

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

Czy to możliwe w PHP? Jak mogę to osiągnąć?


AKTUALIZACJA

Jeśli to niemożliwe, czy przyszłe wersje PHP będą obsługiwać klasy zagnieżdżone?


4
To niemożliwe w PHP
Eugene

Możesz go rozszerzyć User, na przykład: public class UserProfile extends Useri public class UserHestory extends User.
Dave Chen

Możesz także zacząć od abstrakcyjnej klasy użytkownika, a następnie ją rozszerzyć. php.net/manual/en/language.oop5.abstract.php
Matthew Blancarte

@DaveChen Jestem zaznajomiony z rozszerzaniem zajęć, ale szukam lepszego rozwiązania OOP :( Thx.
Lior Elrom

4
rozszerzanie to nie to samo co zawieranie ... kiedy rozszerzasz, uzyskujesz duplikację klasy User 3 razy (jako User, jako UserProfile i jako UserHistory)
Tomer W

Odpowiedzi:


137

Wstęp:

Klasy zagnieżdżone odnoszą się do innych klas nieco inaczej niż klasy zewnętrzne. Biorąc Javę jako przykład:

Zagnieżdżone klasy niestatyczne mają dostęp do innych członków otaczającej klasy, nawet jeśli zostały zadeklarowane jako prywatne. Ponadto niestatyczne klasy zagnieżdżone wymagają utworzenia instancji klasy nadrzędnej.

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

Istnieje kilka istotnych powodów, dla których warto ich używać:

  • Jest to sposób na logiczne grupowanie klas, które są używane tylko w jednym miejscu.

Jeśli klasa jest przydatna tylko dla jednej innej klasy, logiczne jest powiązanie i osadzenie jej w tej klasie oraz utrzymanie tych dwóch razem.

  • Zwiększa hermetyzację.

Rozważ dwie klasy najwyższego poziomu, A i B, w których B potrzebuje dostępu do elementów członkowskich A, które w przeciwnym razie zostałyby zadeklarowane jako prywatne. Ukrywając klasę B w klasie A, członkowie A mogą zostać uznani za prywatnych i B może uzyskać do nich dostęp. Ponadto samo B można ukryć przed światem zewnętrznym.

  • Zagnieżdżone klasy mogą prowadzić do bardziej czytelnego i łatwiejszego w utrzymaniu kodu.

Zagnieżdżona klasa zwykle odnosi się do swojej klasy nadrzędnej i razem tworzą „pakiet”

W PHP

Możesz mieć podobne zachowanie w PHP bez zagnieżdżonych klas.

Jeśli wszystko, co chcesz osiągnąć, to struktura / organizacja, jak Package.OuterClass.InnerClass, przestrzenie nazw PHP mogą wystarczyć. Możesz nawet zadeklarować więcej niż jedną przestrzeń nazw w tym samym pliku (chociaż ze względu na standardowe funkcje automatycznego ładowania, może to nie być zalecane).

namespace;
class OuterClass {}

namespace OuterClass;
class InnerClass {}

Jeśli chcesz naśladować inne cechy, takie jak widoczność członków, wymaga to trochę więcej wysiłku.

Definiowanie klasy „pakietu”

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

Przypadek użycia

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

Testowanie

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

Wynik:

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

UWAGA:

Naprawdę nie sądzę, aby próba emulacji innerClasses w PHP była tak dobrym pomysłem. Myślę, że kod jest mniej czysty i czytelny. Prawdopodobnie istnieją również inne sposoby osiągnięcia podobnych wyników przy użyciu dobrze ugruntowanego wzorca, takiego jak Wzorzec obserwatora, dekoratora lub układu współrzędnych. Czasami nawet proste dziedziczenie jest wystarczające.


2
To niesamowite @Tivie! Mam zamiar zaimplementować to rozwiązanie w moim frameworku rozszerzeń OOP! (patrz mój github: github.com/SparK-Cruz)
SparK

21

Prawdziwe zagnieżdżone klasy z public/ protected/ privateaccessibility zostały zaproponowane w 2013 roku dla PHP 5.6 jako RFC, ale tego nie zrobiły (brak głosowania, brak aktualizacji od 2013 - od 2016/12/29 ):

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

class foo {
    public class bar {
 
    }
}

Przynajmniej klasy anonimowe trafiły do ​​PHP 7

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

Z tej strony RFC:

Przyszły zakres

Zmiany wprowadzone przez tę poprawkę oznaczają, że nazwane klasy zagnieżdżone są łatwiejsze do zaimplementowania (tylko odrobinę).

Więc możemy otrzymać zagnieżdżone klasy w jakiejś przyszłej wersji, ale nie jest to jeszcze zdecydowane.



5

Od wersji PHP 5.4 możesz wymusić tworzenie obiektów z prywatnym konstruktorem poprzez odbicie. Może służyć do symulacji zagnieżdżonych klas Java. Przykładowy kod:

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

Zgodnie z komentarzem Xenona do odpowiedzi Anıl Özselgin, klasy anonimowe zostały zaimplementowane w PHP 7.0, który jest tak bliski klasom zagnieżdżonym, jak teraz. Oto odpowiednie specyfikacje RFC:

Klasy zagnieżdżone (stan: wycofane)

Klasy anonimowe (stan: zaimplementowane w PHP 7.0)

Przykład oryginalnego posta, tak wyglądałby Twój kod:

<?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
            }
        }
    }
?>

To jednak wiąże się z bardzo nieprzyjemnym zastrzeżeniem. Jeśli używasz IDE, takiego jak PHPStorm lub NetBeans, a następnie dodaj do Userklasy metodę podobną do tej :

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

... pa pa autouzupełnianie. Dzieje się tak, nawet jeśli kodujesz do interfejsów (I w SOLID), używając wzoru takiego:

<?php
    public class User {
        public $profile;

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

O ile twoje jedyne wywołania do $this->profilenie pochodzą z __construct()metody (lub jakiejkolwiek metody $this->profilezdefiniowanej w), nie otrzymasz żadnych podpowiedzi typu. Twoja własność jest zasadniczo „ukryta” w Twoim IDE, co bardzo utrudnia życie, jeśli polegasz na swoim IDE w zakresie autouzupełniania, wykrywania zapachu kodu i refaktoryzacji.


3

Nie możesz tego zrobić w PHP. PHP obsługuje "dołączanie", ale nie możesz tego zrobić nawet wewnątrz definicji klasy. Nie ma tu wielu świetnych opcji.

To nie odpowiada bezpośrednio na twoje pytanie, ale możesz być zainteresowany "Przestrzeniami nazw", strasznie brzydkim \ syntax \ hacked \ on \ top \ PHP OOP: http://www.php.net/manual/en/language .namespaces.rationale.php


Przestrzenie nazw z pewnością mogą lepiej zorganizować kod, ale nie są tak wydajne jak klasy zagnieżdżone. Dziękuję za odpowiedź!
Lior Elrom

dlaczego nazywasz to „okropnym”? Myślę, że jest w porządku i dobrze oddzielony od innych kontekstów składni.
emfi


2

Myślę, że napisałem eleganckie rozwiązanie tego problemu przy użyciu przestrzeni nazw. W moim przypadku klasa wewnętrzna nie musi znać swojej klasy nadrzędnej (podobnie jak statyczna klasa wewnętrzna w Javie). Jako przykład stworzyłem klasę o nazwie „Użytkownik” i podklasę o nazwie „Typ”, używaną jako odniesienie dla typów użytkowników (ADMINISTRATOR, INNE) w moim przykładzie. Pozdrowienia.

User.php (plik klasy użytkownika)

<?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 (przykład wywołania „podklasy”)

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

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

2

Możesz w ten sposób w 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

Umieść każdą klasę w osobnych plikach i „wymagaj” ich.

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
    }

?>
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.