Classe annidata o interna in PHP


111

Sto costruendo una classe utente per il mio nuovo sito Web, tuttavia questa volta stavo pensando di costruirla in modo leggermente diverso ...

C ++ , Java e persino Ruby (e probabilmente altri linguaggi di programmazione) consentono l'uso di classi nidificate / interne all'interno della classe principale, il che ci consente di rendere il codice più orientato agli oggetti e organizzato.

In PHP, vorrei fare qualcosa del genere:

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

    public class UserProfile {
      // some code here
    }

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

È possibile in PHP? Come posso ottenerlo?


AGGIORNARE

Se è impossibile, le future versioni di PHP potrebbero supportare le classi nidificate?


4
Questo impossibile in PHP
Eugene

Potresti estenderlo User, esempio: public class UserProfile extends Usere public class UserHestory extends User.
Dave Chen

Puoi anche iniziare con una classe utente astratta, quindi estenderla. php.net/manual/en/language.oop5.abstract.php
Matthew Blancarte

@DaveChen ho familiarità con l'estensione delle classi, tuttavia sto cercando una soluzione OOP migliore :( Thx.
Lior Elrom

4
estendere non è la stessa cosa del contenimento ... quando estendi ottieni la duplicazione della classe User 3 volte (come User, come UserProfile e come UserHistory)
Tomer W

Risposte:


136

Intro:

Le classi annidate si riferiscono ad altre classi in modo leggermente diverso dalle classi esterne. Prendendo Java come esempio:

Le classi nidificate non statiche hanno accesso ad altri membri della classe che la racchiude, anche se sono dichiarate private. Inoltre, le classi nidificate non statiche richiedono l'istanza di un'istanza della classe genitore.

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

Ci sono diversi validi motivi per usarli:

  • È un modo per raggruppare logicamente le classi che vengono utilizzate solo in un posto.

Se una classe è utile solo per un'altra classe, è logico metterla in relazione e incorporarla in quella classe e tenerle insieme.

  • Aumenta l'incapsulamento.

Considera due classi di primo livello, A e B, in cui B ha bisogno dell'accesso ai membri di A che altrimenti sarebbero dichiarati privati. Nascondendo la classe B all'interno della classe A, i membri di A possono essere dichiarati privati ​​e B può accedervi. Inoltre, B stesso può essere nascosto al mondo esterno.

  • Le classi annidate possono portare a codice più leggibile e gestibile.

Una classe annidata di solito si riferisce alla sua classe genitore e insieme formano un "pacchetto"

In PHP

Puoi avere un comportamento simile in PHP senza classi nidificate.

Se tutto ciò che vuoi ottenere è struttura / organizzazione, come Package.OuterClass.InnerClass, gli spazi dei nomi PHP potrebbero essere sufficienti. Puoi persino dichiarare più di uno spazio dei nomi nello stesso file (sebbene, a causa delle funzionalità di caricamento automatico standard, ciò potrebbe non essere consigliabile).

namespace;
class OuterClass {}

namespace OuterClass;
class InnerClass {}

Se desideri emulare altre caratteristiche, come la visibilità dei membri, ci vuole un po 'più di impegno.

Definizione della classe "pacchetto"

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

Caso d'uso

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

analisi

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

Produzione:

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

NOTA:

Non credo proprio che provare ad emulare innerClasses in PHP sia una così buona idea. Penso che il codice sia meno pulito e leggibile. Inoltre, ci sono probabilmente altri modi per ottenere risultati simili utilizzando un modello ben consolidato come Observer, Decorator o COmposition Pattern. A volte è sufficiente anche una semplice eredità.


2
È fantastico @Tivie! Implementerò quella soluzione nel mio framework di estensione OOP! (vedi il mio github: github.com/SparK-Cruz)
SparK

21

Classi nidificate reali con public/ protected/ privateaccessibilità sono state proposte nel 2013 per PHP 5.6 come RFC ma non ce l'hanno fatta (nessuna votazione ancora, nessun aggiornamento dal 2013 - al 29/12/2016 ):

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

class foo {
    public class bar {
 
    }
}

Almeno, le classi anonime sono entrate in PHP 7

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

Da questa pagina RFC:

Ambito futuro

Le modifiche apportate da questa patch indicano che le classi annidate denominate sono più facili da implementare (di un po ').

Quindi, potremmo ottenere classi annidate in qualche versione futura, ma non è ancora deciso.



5

Dalla versione 5.4 di PHP è possibile forzare la creazione di oggetti con un costruttore privato tramite la reflection. Può essere utilizzato per simulare classi nidificate Java. Codice di esempio:

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

Secondo il commento di Xenon alla risposta di Anıl Özselgin, le classi anonime sono state implementate in PHP 7.0, che è il più vicino alle classi nidificate come avrai in questo momento. Ecco le RFC rilevanti:

Classi annidate (stato: ritirato)

Classi anonime (stato: implementato in PHP 7.0)

Un esempio del post originale, questo è l'aspetto del tuo codice:

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

Questo, tuttavia, viene fornito con un avvertimento molto sgradevole. Se usi un IDE come PHPStorm o NetBeans, e poi aggiungi un metodo come questo alla Userclasse:

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

... ciao ciao autocompletamento. Questo è il caso anche se si codifica per interfacce (la I in SOLID), utilizzando un modello come questo:

<?php
    public class User {
        public $profile;

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

A meno che le tue uniche chiamate a $this->profilenon __construct()provengano dal metodo (o da qualunque metodo $this->profilesia definito), non otterrai alcun tipo di suggerimento sul tipo. La tua proprietà è essenzialmente "nascosta" al tuo IDE, rendendo la vita molto difficile se ti affidi al tuo IDE per il completamento automatico, lo sniffing dell'odore del codice e il refactoring.


3

Non puoi farlo in PHP. PHP supporta "include", ma non puoi farlo nemmeno all'interno di una definizione di classe. Non ci sono molte ottime opzioni qui.

Questo non risponde direttamente alla tua domanda, ma potresti essere interessato a "Namespaces", una \ syntax \ hacked terribilmente brutta in \ top \ di PHP OOP: http://www.php.net/manual/en/language .namespaces.rationale.php


Gli spazi dei nomi possono certamente organizzare meglio il codice, ma non è potente come le classi annidate. Grazie per la risposta!
Lior Elrom

perché lo chiami "terribile"? Penso che sia ok e ben separato da altri contesti di sintassi.
emfi


2

Penso di aver scritto una soluzione elegante a questo problema utilizzando gli spazi dei nomi. Nel mio caso, la classe interna non ha bisogno di conoscere la sua classe genitore (come la classe interna statica in Java). Come esempio ho creato una classe chiamata "Utente" e una sottoclasse chiamata "Tipo", usata come riferimento per i tipi di utente (ADMIN, ALTRI) nel mio esempio. Saluti.

User.php (file di classe utente)

<?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 (Un esempio di come chiamare la 'sottoclasse')

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

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

2

Puoi, in questo modo, in 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

Metti ogni classe in file separati e "richiedili".

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
    }

?>
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.