Лучший способ сделать несколько конструкторов в PHP

311

Вы не можете поместить две функции __construct с уникальными сигнатурами сигнатур в классе PHP. Я хотел бы сделать это:

class Student 
{
   protected $id;
   protected $name;
   // etc.

   public function __construct($id){
       $this->id = $id;
      // other members are still uninitialized
   }

   public function __construct($row_from_database){
       $this->id = $row_from_database->id;
       $this->name = $row_from_database->name;
       // etc.
   }
}

Каков наилучший способ сделать это в PHP?

  • 85
    Я мечтаю об именованных конструкторах и перегрузке метода тоже +1
  • 0
    В моем случае я хочу иметь защищенный конструктор, который имеет один менее обязательный аргумент, чем общедоступный, - для стандартизации метода фабрики. Мне нужен класс, чтобы иметь возможность создавать свои копии, и фабрика находится в абстрактном классе, но у конкретных классов могут быть конструкторы, которым требуется второй аргумент - о котором абстрактный класс не имеет представления.
Теги:
constructor

19 ответов

407
Лучший ответ

Я бы, наверное, сделал что-то вроде этого:

<?php

class Student
{
    public function __construct() {
        // allocate your stuff
    }

    public static function withID( $id ) {
        $instance = new self();
        $instance->loadByID( $id );
        return $instance;
    }

    public static function withRow( array $row ) {
        $instance = new self();
        $instance->fill( $row );
        return $instance;
    }

    protected function loadByID( $id ) {
        // do query
        $row = my_awesome_db_access_stuff( $id );
        $this->fill( $row );
    }

    protected function fill( array $row ) {
        // fill all properties from array
    }
}

?>

Тогда, если мне нужен Student, где я знаю ID:

$student = Student::withID( $id );

Или, если у меня есть массив строки db:

$student = Student::withRow( $row );

Технически вы не создаете несколько конструкторов, а только статические вспомогательные методы, но вы можете избежать большого количества кода спагетти в конструкторе таким образом.

  • 2
    Похоже, вы только что ответили на вопрос, который я задал gpilotino. Спасибо! Очень ясно.
  • 1
    @JannieT, на самом деле, фабрики немного сложнее, и, возможно, imho излишне справляется с этой проблемой
Показать ещё 11 комментариев
75

Решение Криса действительно приятно, но я считаю сочетание factory и беглого стиля лучше:

<?php

class Student
{

    protected $firstName;
    protected $lastName;
    // etc.

    /**
     * Constructor
     */
    public function __construct() {
        // allocate your stuff
    }

    /**
     * Static constructor / factory
     */
    public static function create() {
        $instance = new self();
        return $instance;
    }

    /**
     * FirstName setter - fluent style
     */
    public function setFirstName( $firstName) {
        $this->firstName = $firstName;
        return $this;
    }

    /**
     * LastName setter - fluent style
     */
    public function setLastName( $lastName) {
        $this->lastName = $lastName;
        return $this;
    }

}

// create instance
$student= Student::create()->setFirstName("John")->setLastName("Doe");

// see result
var_dump($student);
?>
  • 1
    +1; Решение такого типа может дать действительно хороший код. Хотя я бы предпочел, чтобы setLastName (или, скорее, все сеттеры) в этом решении возвращали $this вместо того, чтобы фактически иметь два сеттера в одном и том же свойстве.
  • 1
    Как кто-то привык к компилируемым, статически типизированным языкам, таким как C #, этот способ работы просто приятен мне.
Показать ещё 2 комментария
40

PHP - это динамический язык, поэтому вы не можете перегружать методы. Вы должны проверить типы своих аргументов следующим образом:

class Student 
{
   protected $id;
   protected $name;
   // etc.

   public function __construct($idOrRow){
    if(is_int($idOrRow))
    {
        $this->id = $idOrRow;
        // other members are still uninitialized
    }
    else if(is_array($idOrRow))
    {
       $this->id = $idOrRow->id;
       $this->name = $idOrRow->name;
       // etc.  
    }
}
  • 18
    Все, что приводит к удивительному коду спагетти. Но это действительно, наверное, самый простой способ сделать это.
  • 0
    Если вы создадите свои конструкторы, как если бы вы использовали статически типизированный язык, это станет кодом спагетти. Но ты не. Создание двух конструкторов с одним параметром и без типа (без типа == любого типа) для этого параметра, в любом случае, не будет работать ни на одном языке (например, не будет иметь двух конструкторов Java с одним параметром Object каждый в одном классе, либо ).
Показать ещё 5 комментариев
19
public function __construct() {
    $parameters = func_get_args();
    ...
}

$o = new MyClass('One', 'Two', 3);

Теперь $paramters будет массивом со значениями "Один", "Два", "3".

Изменить

Могу добавить, что

func_num_args()

предоставит вам количество параметров для функции.

  • 3
    Как это решает проблему знания того, что было передано? Я думаю, что это усложняет проблему, так как вместо того, чтобы проверять тип параметра, вы должны проверить, установлен ли параметр x, а затем его тип.
  • 0
    Это не решает проблему, чтобы узнать, какой тип был передан, но это путь для «нескольких конструкторов» в PHP. Проверка типа до OP, чтобы сделать.
Показать ещё 1 комментарий
14

Как уже было показано здесь, существует много способов объявления конструкторов multiple в PHP, но ни один из них не является способом correct (так как PHP технически не позволяет). Но это не мешает нам взломать эту функциональность... Вот еще один пример:

<?php

class myClass {
    public function __construct() {
        $get_arguments       = func_get_args();
        $number_of_arguments = func_num_args();

        if (method_exists($this, $method_name = '__construct'.$number_of_arguments)) {
            call_user_func_array(array($this, $method_name), $get_arguments);
        }
    }

    public function __construct1($argument1) {
        echo 'constructor with 1 parameter ' . $argument1 . "\n";
    }

    public function __construct2($argument1, $argument2) {
        echo 'constructor with 2 parameter ' . $argument1 . ' ' . $argument2 . "\n";
    }

    public function __construct3($argument1, $argument2, $argument3) {
        echo 'constructor with 3 parameter ' . $argument1 . ' ' . $argument2 . ' ' . $argument3 . "\n";
    }
}

$object1 = new myClass('BUET');
$object2 = new myClass('BUET', 'is');
$object3 = new myClass('BUET', 'is', 'Best.');

Источник: Самый простой способ использования и понимания нескольких конструкторов:

Надеюсь, это поможет.:)

  • 0
    Это лучшее решение. Может быть еще более элегантно, если использовать PHP 5.6+ с оператором new ...
  • 0
    Конечно, это не сработает с первоначальным вопросом JannieT, поскольку ее желаемыми конструкторами были __construct($id) и __construct($row_from_database) . Оба имеют один аргумент, предположительно либо int для первого, либо массив или объект для второго. Конечно, добавление числа может быть расширено до некоторой сигнатуры аргумента в стиле C ++ (т. __construct_i($intArg) и __construct_a($arrayArg) ).
Показать ещё 1 комментарий
14

Начиная с версии 5.4, PHP поддерживает traits. Это не совсем то, что вы ищете, но простой подход, основанный на признаках:

trait StudentTrait {
    protected $id;
    protected $name;

    final public function setId($id) {
        $this->id = $id;
        return $this;
    }

    final public function getId() { return $this->id; }

    final public function setName($name) {
        $this->name = $name; 
        return $this;
    }

    final public function getName() { return $this->name; }

}

class Student1 {
    use StudentTrait;

    final public function __construct($id) { $this->setId($id); }
}

class Student2 {
    use StudentTrait;

    final public function __construct($id, $name) { $this->setId($id)->setName($name); }
}

В итоге мы получаем два класса: по одному для каждого конструктора, что немного контрпродуктивно. Чтобы поддерживать некоторую здравомыслие, я запишу factory:

class StudentFactory {
    static public function getStudent($id, $name = null) {
        return 
            is_null($name)
                ? new Student1($id)
                : new Student2($id, $name)
    }
}

Итак, все сводится к следующему:

$student1 = StudentFactory::getStudent(1);
$student2 = StudentFactory::getStudent(1, "yannis");

Это ужасно подробный подход, но это может быть очень удобно.

12

Вы можете сделать что-то вроде этого:

public function __construct($param)
{
    if(is_int($param)) {
         $this->id = $param;
    } elseif(is_object($param)) {
     // do something else
    }
 }
  • 0
    +1 за очень работоспособное решение. Однако для этого класса я буду использовать метод @Kris.
4

Вы можете сделать что-то вроде следующего, что очень просто и очень чисто:

public function __construct()    
{
   $arguments = func_get_args(); 

   switch(sizeof(func_get_args()))      
   {
    case 0: //No arguments
        break; 
    case 1: //One argument
        $this->do_something($arguments[0]); 
        break;              
    case 2:  //Two arguments
        $this->do_something_else($arguments[0], $arguments[1]); 
        break;            
   }
}
  • 2
    зачем присваивать func_get_args переменной и вызывать ее снова в следующей строке? также было бы лучше, если бы вы вызывали func_get_args только после того, как решили, что вам нужно, основываясь на fund_num_args .
  • 7
    Имхо это противоположность чистого решения
Показать ещё 1 комментарий
4

Другой вариант - использовать аргументы по умолчанию в конструкторе, подобном этому

class Student {

    private $id;
    private $name;
    //...

    public function __construct($id, $row=array()) {
        $this->id = $id;
        foreach($row as $key => $value) $this->$key = $value;
    }
}

Это означает, что вам нужно создать экземпляр с помощью строки следующим образом: $student = new Student($row['id'], $row), но сохранит ваш конструктор красивым и чистым.

С другой стороны, если вы хотите использовать полиморфизм, вы можете создать два класса:

class Student {

    public function __construct($row) {
         foreach($row as $key => $value) $this->$key = $value;
    }
}

class EmptyStudent extends Student {

    public function __construct($id) {
        parent::__construct(array('id' => $id));
    }
}
  • 1
    теперь у вас есть два класса с разными именами, но с одинаковой функциональностью, но с другой сигнатурой в конструкторе, для меня это звучит довольно плохо.
  • 3
    Для меня это звучит как классический полиморфизм, иначе известный как объектно-ориентированное программирование.
Показать ещё 1 комментарий
4

как указано в других комментариях, поскольку php не поддерживает перегрузку, обычно избегают "трюков проверки типа" в конструкторе и используется шаблон factory intead

т.

$myObj = MyClass::factory('fromInteger', $params);
$myObj = MyClass::factory('fromRow', $params);
  • 0
    Выглядит аккуратно Я не знаком с фабриками. В вашем примере, $ myObj будет иметь тип MyClass? Как бы выглядели две статические функции, возвращающие сконструированный экземпляр $ myObj?
  • 3
    Я бы использовал отдельные методы, как Крис, чтобы предотвратить один большой фабричный метод.
Показать ещё 1 комментарий
3

На этот вопрос уже были ответы очень разумные способы выполнения требования, но мне интересно, почему бы не сделать шаг назад и задать основной вопрос, почему нам нужен класс с двумя конструкторами? Если моему классу нужны два конструктора, то, вероятно, именно так, как я разрабатываю свои классы, требуется немного больше внимания, чтобы придумать более чистую и более проверяемую конструкцию.

Мы пытаемся смешивать процесс создания класса с фактической логикой класса.

Если объект Student находится в допустимом состоянии, имеет значение, если он был создан из строки базы данных или данных из веб-формы или запроса cli?

Теперь, чтобы ответить на вопрос, что это может возникнуть здесь, если мы не добавим логику создания объекта из строки db, то как мы создадим объект из данных db, мы можем просто добавить другой класс, вызвать это StudentMapper, если вам нравится шаблон карты данных, в некоторых случаях вы можете использовать StudentRepository, и если ничто не соответствует вашим потребностям, вы можете сделать StudentFactory для решения всех задач по строительству объектов.

Нижняя строка состоит в том, чтобы удерживать слой persistence из нашей головы, когда мы работаем над объектами домена.

2

Позвольте мне добавить здесь мое зерно песка

Мне лично нравится добавлять конструкторы в качестве статических функций, возвращающих экземпляр класса (объекта). Ниже приведен пример кода:

 class Person
 {
     private $name;
     private $email;

     public static function withName($name)
     {
         $person = new Person();
         $person->name = $name;

         return $person;
     }

     public static function withEmail($email)
     {
         $person = new Person();
         $person->email = $email;

         return $person;
     }
 }

Обратите внимание, что теперь вы можете создать экземпляр класса Person следующим образом:

$person1 = Person::withName('Example');
$person2 = Person::withEmail('yo@mi_email.com');

Я взял этот код из:

http://alfonsojimenez.com/post/30377422731/multiple-constructors-in-php

1

Я знаю, что я очень опоздал на вечеринку здесь, но я придумал довольно гибкий шаблон, который должен позволить некоторые действительно интересные и универсальные реализации.

Настройте свой класс, как обычно, с любыми переменными, которые вам нравятся.

class MyClass{
    protected $myVar1;
    protected $myVar2;

    public function __construct($obj = null){
        if($obj){
            foreach (((object)$obj) as $key => $value) {
                if(isset($value) && in_array($key, array_keys(get_object_vars($this)))){
                    $this->$key = $value;
                }
            }
        }
    }
}

Когда вы создаете свой объект, просто передавайте ассоциативный массив с ключами массива, такими же, как имена ваших переменных, например так...

$sample_variable = new MyClass([
    'myVar2'=>123, 
    'i_dont_want_this_one'=> 'This won\'t make it into the class'
    ]);

print_r($sample_variable);

print_r($sample_variable); после этого создания получается следующее:

MyClass Object ( [myVar1:protected] => [myVar2:protected] => 123 )

Поскольку в нашей __construct(...) мы инициализировали $group в null, также допустимо ничего не передавать в конструктор, например, так...

$sample_variable = new MyClass();

print_r($sample_variable);

Теперь результат в точности соответствует ожиданиям:

MyClass Object ( [myVar1:protected] => [myVar2:protected] => )

Причина, по которой я написал это, состояла в том, чтобы я мог напрямую передавать вывод json_decode(...) своему конструктору и не беспокоиться об этом слишком сильно.

Это было выполнено в PHP 7.1. Наслаждайтесь!

  • 0
    Вы можете делать некоторые интересные вещи, такие как создание исключения, когда в массив вводится неожиданное значение. Вот пример этого, который я написал
0

Я создал этот метод, чтобы использовать его не только для конструкторов, но и для методов:

Мой конструктор:

function __construct() {
    $paramsNumber=func_num_args();
    if($paramsNumber==0){
        //do something
    }else{
        $this->overload('__construct',func_get_args());
    }
}

Мой метод doSomething:

public function doSomething() {
    $paramsNumber=func_num_args();
    if($paramsNumber==0){
        //do something
    }else{
        $this->overload('doSomething',func_get_args());
    }
}

Оба работают с помощью этого простого метода:

public function overloadMethod($methodName,$params){
    $paramsNumber=sizeof($params);
    //methodName1(), methodName2()...
    $methodNameNumber =$methodName.$paramsNumber;
    if (method_exists($this,$methodNameNumber)) {
        call_user_func_array(array($this,$methodNameNumber),$params);
    }
}

Итак, вы можете объявить

__construct1($arg1), __construct2($arg1,$arg2)...

или

methodName1($arg1), methodName2($arg1,$arg2)...

и т.д.:)

И при использовании:

$myObject =  new MyClass($arg1, $arg2,..., $argN);

он выберет __constructN, где вы определили N args

то  $ myObject → doSomething ($ arg1, $arg2,..., $argM)

он выберет doSomethingM, где вы определили M args;

0

Для php7 я также сравниваю тип параметров, у вас могут быть два конструктора с таким же количеством параметров, но с другим типом.

trait GenericConstructorOverloadTrait
{
    /**
     * @var array Constructors metadata
     */
    private static $constructorsCache;
    /**
     * Generic constructor
     * GenericConstructorOverloadTrait constructor.
     */
    public function __construct()
    {
        $params = func_get_args();
        $numParams = func_num_args();

        $finish = false;

        if(!self::$constructorsCache){
            $class = new \ReflectionClass($this);
            $constructors =  array_filter($class->getMethods(),
                function (\ReflectionMethod $method) {
                return preg_match("/\_\_construct[0-9]+/",$method->getName());
            });
            self::$constructorsCache = $constructors;
        }
        else{
            $constructors = self::$constructorsCache;
        }
        foreach($constructors as $constructor){
            $reflectionParams = $constructor->getParameters();
            if(count($reflectionParams) != $numParams){
                continue;
            }
            $matched = true;
            for($i=0; $i< $numParams; $i++){
                if($reflectionParams[$i]->hasType()){
                    $type = $reflectionParams[$i]->getType()->__toString();
                }
                if(
                    !(
                        !$reflectionParams[$i]->hasType() ||
                        ($reflectionParams[$i]->hasType() &&
                            is_object($params[$i]) &&
                            $params[$i] instanceof $type) ||
                        ($reflectionParams[$i]->hasType() &&
                            $reflectionParams[$i]->getType()->__toString() ==
                            gettype($params[$i]))
                    )
                ) {
                    $matched = false;
                    break;
                }

            }

            if($matched){
                call_user_func_array(array($this,$constructor->getName()),
                    $params);
                $finish = true;
                break;
            }
        }

        unset($constructor);

        if(!$finish){
            throw new \InvalidArgumentException("Cannot match construct by params");
        }
    }

}

Чтобы использовать его:

class MultiConstructorClass{

    use GenericConstructorOverloadTrait;

    private $param1;

    private $param2;

    private $param3;

    public function __construct1($param1, array $param2)
    {
        $this->param1 = $param1;
        $this->param2 = $param2;
    }

    public function __construct2($param1, array $param2, \DateTime $param3)
    {
        $this->__construct1($param1, $param2);
        $this->param3 = $param3;
    }

    /**
     * @return \DateTime
     */
    public function getParam3()
    {
        return $this->param3;
    }

    /**
     * @return array
     */
    public function getParam2()
    {
        return $this->param2;
    }

    /**
     * @return mixed
     */
    public function getParam1()
    {
        return $this->param1;
    }
}
  • 0
    Можете ли вы показать, как вы MultiConstructorClass два экземпляра вашего MultiConstructorClass используя два разных метода конструктора? Благодарю.
0

Вы всегда можете добавить дополнительный параметр в конструктор, называемый как-то вроде режима, а затем выполнить оператор switch на нем...

class myClass 
{
    var $error ;
    function __construct ( $data, $mode )
    {
        $this->error = false
        switch ( $mode )
        {
            'id' : processId ( $data ) ; break ;
            'row' : processRow ( $data ); break ;
            default : $this->error = true ; break ;
         }
     }

     function processId ( $data ) { /* code */ }
     function processRow ( $data ) { /* code */ }
}

$a = new myClass ( $data, 'id' ) ;
$b = new myClass ( $data, 'row' ) ;
$c = new myClass ( $data, 'something' ) ;

if ( $a->error )
   exit ( 'invalid mode' ) ;
if ( $b->error )
   exit ('invalid mode' ) ;
if ( $c->error )
   exit ('invalid mode' ) ;

Также с помощью этого метода в любое время, если вы хотите добавить больше функциональных возможностей, вы можете просто добавить еще один случай в оператор switch, и вы также можете проверить, чтобы кто-то отправил правильную вещь через - в приведенном выше примере все данные одобрены, за исключением того, что для C установлено значение "что-то", и поэтому флаг ошибки в классе задан, и управление возвращается обратно в основную программу, чтобы решить, что делать дальше (в примере, который я только что сказал ему, выйдите с сообщением об ошибке "недействительный режим", но в качестве альтернативы вы можете запрограммировать его назад до тех пор, пока не будут найдены действительные данные.)

0

В ответ на лучший ответ Криса (который поразительно помог разработать мой собственный класс btw), здесь приведена модифицированная версия для тех, кто может найти ее полезной. Включает методы для выбора из любого столбца и данных объекта демпинга из массива. Ура!

public function __construct() {
    $this -> id = 0;
    //...
}

public static function Exists($id) {
    if (!$id) return false;
    $id = (int)$id;
    if ($id <= 0) return false;
    $mysqli = Mysql::Connect();
    if (mysqli_num_rows(mysqli_query($mysqli, "SELECT id FROM users WHERE id = " . $id)) == 1) return true;
    return false;
}

public static function FromId($id) {
    $u = new self();
    if (!$u -> FillFromColumn("id", $id)) return false;
    return $u;
}

public static function FromColumn($column, $value) {
    $u = new self();
    if (!$u -> FillFromColumn($column, $value)) return false;
    return $u;
}

public static function FromArray($row = array()) {
    if (!is_array($row) || $row == array()) return false;
    $u = new self();
    $u -> FillFromArray($row);
    return $u;
}

protected function FillFromColumn($column, $value) {
    $mysqli = Mysql::Connect();
    //Assuming we're only allowed to specified EXISTENT columns
    $result = mysqli_query($mysqli, "SELECT * FROM users WHERE " . $column . " = '" . $value . "'");
    $count = mysqli_num_rows($result);
    if ($count == 0) return false;
    $row = mysqli_fetch_assoc($result);
    $this -> FillFromArray($row);
}

protected function FillFromArray(array $row) {
    foreach($row as $i => $v) {
        if (isset($this -> $i)) {
            $this -> $i = $v;
        }
    }
}

public function ToArray() {
    $m = array();
    foreach ($this as $i => $v) {
        $m[$i] = $v;    
    }
    return $m;
}

public function Dump() {
    print_r("<PRE>");
    print_r($this -> ToArray());
    print_r("</PRE>");  
}
0

Конструкторы вызовов по типу данных:

class A 
{ 
    function __construct($argument)
    { 
       $type = gettype($argument);

       if($type == 'unknown type')
       {
            // type unknown
       }

       $this->{'__construct_'.$type}($argument);
    } 

    function __construct_boolean($argument) 
    { 
        // do something
    }
    function __construct_integer($argument) 
    { 
        // do something
    }
    function __construct_double($argument) 
    { 
        // do something
    }
    function __construct_string($argument) 
    { 
        // do something
    }
    function __construct_array($argument) 
    { 
        // do something
    }
    function __construct_object($argument) 
    { 
        // do something
    }
    function __construct_resource($argument) 
    { 
        // do something
    }

    // other functions

} 
  • 0
    Вы должны упомянуть, что вы получили этот фрагмент кода здесь -> php.net/manual/en/language.oop5.decon.php#99903 .
  • 0
    Это было около 6 месяцев назад, проверьте мое обновление @LavaSlider
Показать ещё 1 комментарий
0

Насколько я знаю, перегрузка не поддерживается в PHP. Вы можете перегружать свойства get и set методами с помощью функции overload(); (http://www.php.net/manual/en/overload.examples.basic.php)

Ещё вопросы

Сообщество Overcoder
Наверх
Меню