Clase anidada o interna en PHP

111

Estoy construyendo una clase de usuario para mi nuevo sitio web, sin embargo, esta vez estaba pensando en hacerlo un poco diferente ...

C ++ , Java e incluso Ruby (y probablemente otros lenguajes de programación) están permitiendo el uso de clases anidadas / internas dentro de la clase principal, lo que nos permite hacer que el código esté más orientado a objetos y organizado.

En PHP, me gustaría hacer algo como esto:

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

    public class UserProfile {
      // some code here
    }

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

¿Es eso posible en PHP? ¿Cómo puedo lograrlo?


ACTUALIZAR

Si es imposible, ¿las futuras versiones de PHP admitirán clases anidadas?

Lior Elrom
fuente
4
Esto imposible en PHP
Eugene
Podría hacer que se extienda User, ejemplo: public class UserProfile extends Usery public class UserHestory extends User.
Dave Chen
También puede comenzar con una clase de usuario abstracta y luego extenderla. php.net/manual/en/language.oop5.abstract.php
Matthew Blancarte
@DaveChen Estoy familiarizado con la extensión de clases, sin embargo, estoy buscando una mejor solución OOP :( Thx.
Lior Elrom
4
extender no es lo mismo que contención ... cuando extiendes obtienes una duplicación de la clase de Usuario 3 veces (como Usuario, como Perfil de Usuario y como Historial de Usuario)
Tomer W

Respuestas:

136

Introducción:

Las clases anidadas se relacionan con otras clases de manera un poco diferente a las clases externas. Tomando Java como ejemplo:

Las clases anidadas no estáticas tienen acceso a otros miembros de la clase adjunta, incluso si se declaran privadas. Además, las clases anidadas no estáticas requieren una instancia de la clase principal para crear una instancia.

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

Hay varias razones de peso para usarlos:

  • Es una forma de agrupar lógicamente clases que solo se utilizan en un lugar.

Si una clase es útil solo para otra clase, entonces es lógico relacionarla e incrustarla en esa clase y mantener las dos juntas.

  • Aumenta la encapsulación.

Considere dos clases de nivel superior, A y B, donde B necesita acceso a miembros de A que de otro modo se declararían privados. Al ocultar la clase B dentro de la clase A, los miembros de A pueden declararse privados y B puede acceder a ellos. Además, el propio B puede ocultarse del mundo exterior.

  • Las clases anidadas pueden conducir a un código más legible y fácil de mantener.

Una clase anidada generalmente se relaciona con su clase principal y juntas forman un "paquete"

En PHP

Puede tener un comportamiento similar en PHP sin clases anidadas.

Si todo lo que desea lograr es estructura / organización, como Package.OuterClass.InnerClass, los espacios de nombres PHP pueden ser suficientes. Incluso puede declarar más de un espacio de nombres en el mismo archivo (aunque, debido a las características estándar de carga automática, eso podría no ser aconsejable).

namespace;
class OuterClass {}

namespace OuterClass;
class InnerClass {}

Si desea emular otras características, como la visibilidad de los miembros, se necesita un poco más de esfuerzo.

Definición de la clase "paquete"

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

Pruebas

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

Salida:

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:

Realmente no creo que tratar de emular innerClasses en PHP sea una buena idea. Creo que el código es menos limpio y legible. Además, probablemente haya otras formas de lograr resultados similares utilizando un patrón bien establecido, como el patrón Observador, Decorador o Colocación. A veces, incluso la simple herencia es suficiente.

Tivie
fuente
2
¡Eso es increíble @Tivie! ¡Voy a implementar esa solución en mi marco de extensión OOP! (ver mi github: github.com/SparK-Cruz)
SparK
21

Las clases anidadas reales con public/ protected/ privateaccesibilidad se propusieron en 2013 para PHP 5.6 como un RFC, pero no lo lograron (aún no se votó, no se actualizó desde 2013, a partir del 29/12/2016 ):

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

class foo {
    public class bar {
 
    }
}

Al menos, las clases anónimas llegaron a PHP 7

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

Desde esta página de RFC:

Alcance futuro

Los cambios realizados por este parche significan que las clases anidadas con nombre son más fáciles de implementar (por un poquito).

Por lo tanto, podríamos obtener clases anidadas en alguna versión futura, pero aún no está decidido.

Fabián Schmengler
fuente
5

Desde PHP versión 5.4, puede forzar la creación de objetos con constructor privado a través de la reflexión. Se puede utilizar para simular clases anidadas de Java. Código de ejemplo:

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
fuente
4

Según el comentario de Xenon a la respuesta de Anıl Özselgin, las clases anónimas se han implementado en PHP 7.0, que es lo más parecido a las clases anidadas que obtendrá en este momento. Aquí están las RFC relevantes:

Clases anidadas (estado: retirado)

Clases anónimas (estado: implementado en PHP 7.0)

Un ejemplo de la publicación original, así es como se vería su código:

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

Sin embargo, esto viene con una advertencia muy desagradable. Si usa un IDE como PHPStorm o NetBeans, y luego agrega un método como este a la Userclase:

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

... adiós autocompletado. Este es el caso incluso si codifica en interfaces (la I en SOLID), usando un patrón como este:

<?php
    public class User {
        public $profile;

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

A menos que sus únicas llamadas a $this->profilesean desde el __construct()método (o cualquier método en el que $this->profileesté definido), no obtendrá ningún tipo de sugerencia de tipo. Su propiedad está esencialmente "oculta" para su IDE, lo que le hace la vida muy difícil si confía en su IDE para la finalización automática, la detección de olores de código y la refactorización.

e_i_pi
fuente
3

No puedes hacerlo en PHP. PHP admite "incluir", pero ni siquiera puede hacerlo dentro de una definición de clase. No hay muchas buenas opciones aquí.

Esto no responde a su pregunta directamente, pero puede estar interesado en "espacios de nombres", una \ sintaxis \ pirateada \ en \ top \ de PHP OOP terriblemente fea: http://www.php.net/manual/en/language .namepaces.rationale.php

dkamins
fuente
Los espacios de nombres ciertamente pueden organizar mejor el código, pero no es tan poderoso como las clases anidadas. ¡Gracias por la respuesta!
Lior Elrom
¿Por qué lo llamas "terrible"? Creo que está bien y bien separado de otros contextos de sintaxis.
emfi
2

Está esperando la votación como RFC https://wiki.php.net/rfc/anonymous_classes

Anıl Özselgin
fuente
1
No creo que una clase anónima ofrezca la funcionalidad de una clase anidada.
Eric G
1
En la página de RFC, si busca "anidado", puede ver que tiene soportes. No es exactamente lo mismo con Java, pero es compatible.
Anıl Özselgin
3
Implementado en PHP 7.
Élektra
2

Creo que escribí una elegante solución a este problema usando espacios de nombres. En mi caso, la clase interna no necesita conocer su clase principal (como la clase interna estática en Java). Como ejemplo, hice una clase llamada 'Usuario' y una subclase llamada 'Tipo', utilizada como referencia para los tipos de usuario (ADMIN, OTROS) en mi ejemplo. Saludos.

User.php (archivo de clase de usuario)

<?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 ejemplo de cómo llamar a la 'subclase')

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

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

Puede, así, 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)();
Arlon Arriola
fuente
-6

Coloque cada clase en archivos separados y "solicítelos".

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
    }

?>
priyabagus
fuente