Classe aninhada ou interna em PHP

111

Estou criando uma classe de usuário para meu novo site, mas desta vez eu estava pensando em criá-la de forma um pouco diferente ...

C ++ , Java e até Ruby (e provavelmente outras linguagens de programação) estão permitindo o uso de classes aninhadas / internas dentro da classe principal, o que nos permite tornar o código mais orientado a objetos e organizado.

Em PHP, gostaria de fazer algo assim:

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

    public class UserProfile {
      // some code here
    }

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

Isso é possível em PHP? Como posso conseguir isso?


ATUALIZAR

Se for impossível, as futuras versões do PHP podem suportar classes aninhadas?

Lior Elrom
fonte
4
Isso é impossível em PHP
Eugene
Você poderia estendê-lo User, por exemplo: public class UserProfile extends Usere public class UserHestory extends User.
Dave Chen
Você também pode começar com uma classe de usuário abstrata e, em seguida, estendê-la. php.net/manual/en/language.oop5.abstract.php
Matthew Blancarte
@DaveChen Estou familiarizado com a extensão de classes, mas estou procurando uma solução OOP melhor :( Thx.
Lior Elrom
4
estender não é o mesmo que contenção ... quando você estende, obtém a duplicação da classe User 3 vezes (como User, como UserProfile e como UserHistory)
Tomer W

Respostas:

136

Introdução:

As classes aninhadas se relacionam com outras classes de maneira um pouco diferente das classes externas. Tomando Java como exemplo:

As classes aninhadas não estáticas têm acesso a outros membros da classe envolvente, mesmo se forem declarados privados. Além disso, as classes aninhadas não estáticas requerem que uma instância da classe pai seja instanciada.

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

Existem vários motivos convincentes para usá-los:

  • É uma maneira de agrupar logicamente as classes que são usadas apenas em um lugar.

Se uma classe é útil para apenas uma outra classe, então é lógico relacioná-la e incorporá-la nessa classe e manter as duas juntas.

  • Aumenta o encapsulamento.

Considere duas classes de nível superior, A e B, onde B precisa de acesso aos membros de A que, de outra forma, seriam declarados privados. Ao ocultar a classe B dentro da classe A, os membros de A podem ser declarados privados e B pode acessá-los. Além disso, o próprio B pode ser escondido do mundo exterior.

  • As classes aninhadas podem levar a um código mais legível e sustentável.

Uma classe aninhada geralmente se relaciona com sua classe pai e, juntas, formam um "pacote"

Em PHP

Você pode ter um comportamento semelhante em PHP sem classes aninhadas.

Se tudo o que você deseja alcançar é estrutura / organização, como Package.OuterClass.InnerClass, namespaces de PHP podem ser suficientes. Você pode até declarar mais de um namespace no mesmo arquivo (embora, devido aos recursos de carregamento automático padrão, isso possa não ser aconselhável).

namespace;
class OuterClass {}

namespace OuterClass;
class InnerClass {}

Se você deseja emular outras características, como a visibilidade dos membros, é preciso um pouco mais de esforço.

Definindo a classe "pacote"

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

Testando

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

Resultado:

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:

Eu realmente não acho que tentar emular innerClasses em PHP seja uma ideia tão boa. Acho que o código é menos limpo e legível. Além disso, provavelmente existem outras maneiras de obter resultados semelhantes usando um padrão bem estabelecido, como o Observer, Decorator ou COmposition Pattern. Às vezes, mesmo uma simples herança é suficiente.

Tivie
fonte
2
Isso é incrível @Tivie! Vou implementar essa solução em minha estrutura de extensão OOP! (veja meu github: github.com/SparK-Cruz)
SparK
21

Classes aninhadas reais com public/ protected/ privateacessibilidade foram propostas em 2013 para PHP 5.6 como um RFC, mas não conseguiram (nenhuma votação ainda, nenhuma atualização desde 2013 - a partir de 2016/12/29 ):

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

class foo {
    public class bar {
 
    }
}

Pelo menos, classes anônimas chegaram ao PHP 7

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

A partir desta página RFC:

Escopo Futuro

As alterações feitas por este patch significam que as classes aninhadas nomeadas são mais fáceis de implementar (por um pouco).

Portanto, podemos obter classes aninhadas em alguma versão futura, mas ainda não está decidido.

Fabian Schmengler
fonte
5

Desde a versão 5.4 do PHP você pode forçar a criação de objetos com construtor privado por meio de reflexão. Ele pode ser usado para simular classes Java aninhadas. Código de exemplo:

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

De acordo com o comentário de Xenon à resposta de Anil Özselgin, classes anônimas foram implementadas no PHP 7.0, que é o mais próximo das classes aninhadas que você verá agora. Aqui estão os RFCs relevantes:

Classes aninhadas (status: retirado)

Classes anônimas (status: implementado no PHP 7.0)

Um exemplo para a postagem original, esta é a aparência do seu 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
            }
        }
    }
?>

Isso, porém, vem com uma advertência muito desagradável. Se você usar um IDE como PHPStorm ou NetBeans e adicionar um método como este à Userclasse:

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

... tchau tchau autocompletar. Este é o caso mesmo se você codificar para interfaces (o I em SOLID), usando um padrão 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 suas únicas chamadas para $this->profilesejam do __construct()método (ou qualquer método $this->profiledefinido em), você não obterá qualquer tipo de sugestão de tipo. Sua propriedade está essencialmente "escondida" em seu IDE, tornando a vida muito difícil se você confiar em seu IDE para autocompletar, farejar código e refatorar.

e_i_pi
fonte
3

Você não pode fazer isso em PHP. PHP suporta "incluir", mas você não pode fazer isso dentro de uma definição de classe. Não há muitas opções boas aqui.

Isso não responde sua pergunta diretamente, mas você pode estar interessado em "Namespaces", uma sintaxe \ hackeada \ on \ top \ de PHP OOP: http://www.php.net/manual/en/language .namespaces.rationale.php

dkamins
fonte
Os namespaces podem certamente organizar melhor o código, mas não são tão poderosos quanto as classes aninhadas. Obrigado pela resposta!
Lior Elrom
por que você chama de "terrível"? acho que está tudo bem e bem separado de outros contextos de sintaxe.
emfi
2

Ele está esperando para votar como RFC https://wiki.php.net/rfc/anonymous_classes

Anil Özselgin
fonte
1
Eu não acredito e a classe anônima oferecerá a funcionalidade de uma classe aninhada.
Eric G,
1
Na página RFC, se você pesquisar por "aninhado", verá que ele tem suporte. Não exatamente o mesmo com o modo Java, mas suporta.
Anil Özselgin
3
Implementado em PHP 7.
Élektra
2

Acho que escrevi uma solução elegante para esse problema usando namespaces. No meu caso, a classe interna não precisa conhecer sua classe pai (como a classe interna estática em Java). Como exemplo fiz uma classe chamada 'User' e uma subclasse chamada 'Type', usada como referência para os tipos de usuário (ADMIN, OTHERS) no meu exemplo. Saudações.

User.php (arquivo de classe de usuário)

<?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 (Um exemplo de como chamar a 'subclasse')

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

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

Você pode, assim, no 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
fonte
-6

Coloque cada classe em arquivos separados e "solicite".

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
fonte