Classe imbriquée ou intérieure en PHP


111

Je construis une classe d'utilisateurs pour mon nouveau site Web, mais cette fois, je pensais la construire un peu différemment ...

C ++ , Java et même Ruby (et probablement d'autres langages de programmation) permettent l'utilisation de classes imbriquées / internes dans la classe principale, ce qui nous permet de rendre le code plus orienté objet et organisé.

En PHP, j'aimerais faire quelque chose comme ceci:

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

    public class UserProfile {
      // some code here
    }

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

Est-ce possible en PHP? Comment puis-je y parvenir?


METTRE À JOUR

Si c'est impossible, les futures versions de PHP pourraient-elles prendre en charge les classes imbriquées?


4
Ceci impossible en PHP
Eugene

Vous pourriez l'avoir étendu User, par exemple: public class UserProfile extends Useret public class UserHestory extends User.
Dave Chen

Vous pouvez également commencer avec une classe d'utilisateurs abstraite, puis l'étendre. php.net/manual/en/language.oop5.abstract.php
Matthew Blancarte

@DaveChen Je suis familier avec l'extension des classes, mais je recherche une meilleure solution POO: (Thx.
Lior Elrom

4
l'extension n'est pas la même chose que le confinement ... lorsque vous étendez, vous obtenez la duplication de la classe User 3 fois (en tant qu'utilisateur, en tant que UserProfile et en tant que UserHistory)
Tomer W

Réponses:


136

Intro:

Les classes imbriquées se rapportent aux autres classes un peu différemment des classes externes. Prenant Java comme exemple:

Les classes imbriquées non statiques ont accès aux autres membres de la classe englobante, même si elles sont déclarées privées. En outre, les classes imbriquées non statiques nécessitent une instance de la classe parente pour être instanciée.

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

Il existe plusieurs raisons impérieuses de les utiliser:

  • C'est une façon de regrouper logiquement des classes qui ne sont utilisées qu'à un seul endroit.

Si une classe n'est utile qu'à une seule autre classe, il est logique de la relier et de l'intégrer dans cette classe et de garder les deux ensemble.

  • Cela augmente l'encapsulation.

Considérez deux classes de niveau supérieur, A et B, où B a besoin d'accéder aux membres de A qui seraient autrement déclarés privés. En masquant la classe B dans la classe A, les membres de A peuvent être déclarés privés et B peut y accéder. De plus, B lui-même peut être caché du monde extérieur.

  • Les classes imbriquées peuvent conduire à un code plus lisible et maintenable.

Une classe imbriquée se rapporte généralement à sa classe parente et forme ensemble un "package"

En PHP

Vous pouvez avoir un comportement similaire en PHP sans classes imbriquées.

Si tout ce que vous voulez réaliser est la structure / organisation, comme Package.OuterClass.InnerClass, les espaces de noms PHP peuvent suffire. Vous pouvez même déclarer plusieurs espaces de noms dans le même fichier (bien que, en raison des fonctionnalités de chargement automatique standard, cela ne soit pas conseillé).

namespace;
class OuterClass {}

namespace OuterClass;
class InnerClass {}

Si vous souhaitez émuler d'autres caractéristiques, telles que la visibilité des membres, cela demande un peu plus d'efforts.

Définition de la classe "package"

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

Cas d'utilisation

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

Essai

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

Production:

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

REMARQUE:

Je ne pense vraiment pas qu'essayer d'émuler les innerClasses en PHP soit une si bonne idée. Je pense que le code est moins propre et lisible. De plus, il existe probablement d'autres moyens d'obtenir des résultats similaires en utilisant un modèle bien établi tel que le modèle Observer, Decorator ou COmposition. Parfois, même un simple héritage suffit.


2
C'est génial @Tivie! Je vais vraiment implémenter cette solution dans mon framework d'extension OOP! (voir mon github: github.com/SparK-Cruz)
SparK

21

De vraies classes imbriquées avec public/ protected/ privateaccessibilité ont été proposées en 2013 pour PHP 5.6 en tant que RFC mais ne l'ont pas fait (pas encore de vote, pas de mise à jour depuis 2013 - à partir du 29/12/2016 ):

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

class foo {
    public class bar {
 
    }
}

Au moins, des classes anonymes ont été intégrées à PHP 7

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

À partir de cette page RFC:

Portée future

Les modifications apportées par ce patch signifient que les classes imbriquées nommées sont plus faciles à implémenter (d'un tout petit peu).

Donc, nous pourrions avoir des classes imbriquées dans une version future, mais ce n'est pas encore décidé.



5

Depuis la version 5.4 de PHP, vous pouvez forcer la création d'objets avec un constructeur privé par réflexion. Il peut être utilisé pour simuler des classes imbriquées Java. Exemple de code:

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

Selon le commentaire de Xenon à la réponse d'Anıl Özselgin, des classes anonymes ont été implémentées dans PHP 7.0, qui est aussi proche des classes imbriquées que vous l'obtiendrez en ce moment. Voici les RFC pertinentes:

Classes imbriquées (statut: retiré)

Classes anonymes (état: implémenté dans PHP 7.0)

Un exemple de l'article original, voici à quoi ressemblerait votre code:

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

Ceci, cependant, vient avec une mise en garde très désagréable. Si vous utilisez un IDE tel que PHPStorm ou NetBeans, puis ajoutez une méthode comme celle-ci à la Userclasse:

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

... bye bye auto-complétion. C'est le cas même si vous codez sur des interfaces (le I en SOLID), en utilisant un modèle comme celui-ci:

<?php
    public class User {
        public $profile;

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

À moins que vos seuls appels à $this->profileproviennent de la __construct()méthode (ou de toute autre méthode $this->profiledéfinie dans), vous n'obtiendrez aucune sorte d'indication de type. Votre propriété est essentiellement "cachée" à votre IDE, ce qui rend la vie très difficile si vous comptez sur votre IDE pour l'auto-complétion, le reniflage d'odeur de code et la refactorisation.


3

Vous ne pouvez pas le faire en PHP. PHP prend en charge "include", mais vous ne pouvez même pas faire cela dans une définition de classe. Pas beaucoup de bonnes options ici.

Cela ne répond pas directement à votre question, mais vous pourriez être intéressé par "Namespaces", une \ syntaxe \ hacked \ on \ top \ de PHP OOP: http://www.php.net/manual/en/language .namespaces.rationale.php


Les espaces de noms peuvent certainement mieux organiser le code, mais ce n'est pas aussi puissant que les classes imbriquées. Merci d'avoir répondu!
Lior Elrom

pourquoi appelez-vous cela «terrible»? Je pense que c'est correct et bien séparé des autres contextes de syntaxe.
emfi


2

Je pense avoir écrit une solution élégante à ce problème en utilisant des espaces de noms. Dans mon cas, la classe interne n'a pas besoin de connaître sa classe parente (comme la classe interne statique en Java). A titre d'exemple, j'ai créé une classe appelée 'User' et une sous-classe appelée 'Type', utilisée comme référence pour les types d'utilisateurs (ADMIN, OTHERS) dans mon exemple. Cordialement.

User.php (fichier de classe utilisateur)

<?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 exemple de la façon d'appeler la 'sous-classe')

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

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

2

Vous pouvez, comme ça, en 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

Mettez chaque classe dans des fichiers séparés et «exigez» les.

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
    }

?>
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.