在一个类的构造函数中返回一个值

51

到目前为止,我有一个具有构造函数的PHP类。

public function __construct ($identifier = NULL)
{
 // Return me.
if ( $identifier != NULL )
{
  $this->emailAddress = $identifier;
  if ($this->loadUser() )
    return $this;      
  else
  {
// registered user requested , but not found ! 
return false;
  }
}
loadUser的功能是查找特定电子邮件地址的数据库。 当我将标识符设置为某些我确定不在数据库中的电子邮件时; 第一个IF被跳过,然后进入第一个ELSE. 在这里,构造函数应该返回FALSE; 但是它返回了一个具有所有NULL值的类对象!
我如何防止这种情况发生?谢谢。
编辑:
感谢大家的回答。非常快! 我知道OOP的方法是抛出异常。所以我抛出一个异常,我的问题就变成了:我应该如何处理异常?php.net的手册非常令人困惑!
    // Setup the user ( we assume he is a user first. referees, admins are   considered users too )
    try { $him = new user ($_emailAddress);
    } catch (Exception $e_u) { 
      // try the groups database
      try { $him = new group ($_emailAddress); 
      } catch (Exception $e_g) {
          // email address was not in any of them !!  
        }
    }

15
构造函数不应该显式地返回任何内容。如果需要表示失败,则应抛出异常,由调用代码捕获。 - Marc B
6
你应该写下答案,Marc B,我会为你点赞。 - MitMaro
最受欢迎和被接受的答案远非最佳答案。请考虑更改已接受的答案。 - slepic
8个回答

81

构造函数没有返回值,它们完全用于实例化类。

如果不重新组织您已经进行的操作,您可以考虑在此处使用异常。

public function __construct ($identifier = NULL)
{
  $this->emailAddress = $identifier;
  $this->loadUser();
}

private function loadUser ()
{
    // try to load the user
    if (/* not able to load user */) {
        throw new Exception('Unable to load user using identifier: ' . $this->identifier);
    }
}

现在,你可以以这种方式创建一个新用户。

try {
    $user = new User('user@example.com');
} catch (Exception $e) {
    // unable to create the user using that id, handle the exception
}

这个答案只是提出了构造函数中return语句的语义问题的解决方案。但它没有指出这种方法基本上是错误的,实体不应该以这种方式被构建/加载。 - slepic
@slepic 我也不会这样做。然而,我认为你会发现,在编程世界中,意见和实践变化极大。一些ORM在意想不到的位置执行查询,比如在属性访问器中。在这个语境中,“应该”和“不应该”是主观的。 - erisco
无论几个ORM执行查询的方式如何,都与实体对象的构建方式完全无关。请向我展示任何广泛使用的ORM以此方式构建实体对象。你的构造函数方法是基本错误的,这一点毫无主观性可言。这个声明有客观的理由,我不会在这里重复它们,因为我已经在我的其他帖子中写过了。请随意阅读它们... - slepic
你有一组特定的偏好@slepic,这很好。我不同意在每种情况下这些是唯一合理的偏好,因此我不认为它们是客观的。我们对“基本上”这个词有不同的理解。值得庆幸的是,SO允许多个答案。 - erisco

9

最好按照Steve的建议去做。 永远不要创建除将构造函数参数分配给对象属性之外的任何工作,可能会创建一些默认值,但不能有其他操作。 构造函数旨在创建完全功能的对象。这样的对象必须在实例化后始终按预期工作。 用户具有电子邮件、姓名和可能还有其他属性。当您想要实例化一个用户对象时,请将所有这些属性都提供给它的构造函数。 抛出异常也不是一个好方法。异常应该在异常情况下抛出。通过电子邮件请求用户并不是什么异常情况,即使最终发现没有这样的用户。例如,如果您通过email = ''请求用户(除非这是系统中的常规状态,但我更倾向于在这些情况下将电子邮件设置为null)。 要获取用户对象的所有这些属性,您应该拥有一个工厂(或存储库,如果您喜欢)对象(是的,一个对象-使用静态内容是不好的做法) 私有构造函数也是不好的做法(您仍需要一个静态方法,如我已经说明的那样,静态方法非常糟糕)。

因此,结果应该类似于这样:

class User {
  private $name;
  private $email;
  private $otherprop;

  public function __construct($name, $email, $otherprop = null) {
    $this->name = $name;
    $this->email = $email;
    $this->otherprop = $otherprop;
  }
}

class UserRepository {
  private $db;

  public function __construct($db) {
    $this->db = $db; //this is what constructors should only do
  }

  public function getUserByEmail($email) {
    $sql = "SELECT * FROM users WHERE email = $email"; //do some quoting here
    $data = $this->db->fetchOneRow($sql); //supose email is unique in the db
    if($data) {
      return new User($data['name'], $data['email'], $data['otherprop']);
    } else {
      return null;
    }
  }
}

$repository = new UserRepository($database); //suppose we have users stored in db
$user = $repository->getUserByEmail('whatever@wherever.com');
if($user === null) {
  //show error or whatever you want to do in that case
} else {
  //do the job with user object
}

看到了吗?没有静态内容,没有异常,构造函数简单易懂,可读性、可测试性和可修改性都很好。

8
构造函数应该创建一个对象。由于在PHP中布尔值不被视为对象,因此唯一的选择是null。否则,请使用解决方法,例如编写一个静态方法来创建实际对象。
public static function CheckAndCreate($identifier){
  $result = self::loadUser();
  if($result === true){
    return new EmailClassNameHere();
  }else{
    return false;
  }
}

5

我很惊讶这4年来的22k观众中没有人建议创建私有构造函数和一个尝试创建这样对象的方法:

class A {
    private function __construct () {
        echo "Created!\n";
    }
    public static function attemptToCreate ($should_it_succeed) {
        if ($should_it_succeed) {
            return new A();
        }
        return false;
    }
}

var_dump(A::attemptToCreate(0)); // bool(false)
var_dump(A::attemptToCreate(1)); // object(A)#1 (0) {}
//! new A(); - gives error

通过这种方式,你可以得到一个对象或false(你也可以让它返回null)。现在捕捉这两种情况非常容易:

$user = User::attemptToCreate('email@example.com');
if(!$user) { // or if(is_null($user)) in case you return null instead of false
    echo "Not logged.";
} else {
    echo $user->name; // e.g.
}

您可以在此处进行测试:http://ideone.com/TDqSyi 我发现我的解决方案比抛出和捕获异常更方便使用。

@mightyuhu准确地做到了这一点,并得到了多个赞。 - Wilt
@Wilt 是的,我现在明白了...但他根本没有提到私有构造函数... - Al.G.
1
我没有看到其他的回答。被删除了吗?无论如何,这是一个很好的答案,也是我在一些情况下所做的。 - Stephen R

3

构造函数只能返回它试图创建的对象。如果实例化过程未能成功完成,你将得到一个类实例,其中包含许多NULL属性,就像你已经发现的那样。

如果对象以不完整或错误状态加载,则建议设置一个属性来指示该状态。

// error status property
public $error = NULL;

public function __construct ($identifier = NULL)
{
 // Return me.
if ( $identifier != NULL )
{
  $this->emailAddress = $identifier;
  if (!$this->loadUser() )
  {
   // registered user requested , but not found ! 
   $this->error = "user not found";
  }
}

在实例化对象时,您可以检查它是否具有错误状态:

$obj = new MyObject($identifier);
if (!empty($obj->error)) {
   // something failed.
}

另一个(也许更好的)选择是在构造函数中抛出异常,并将实例化包装在 try/catch 中。


这不太好,对象应该代表一个用户。你所做的是一个代表用户“承诺”的对象,它可能会失败,因此你必须在每个场合都检查错误。否则,你就有可能使用一个实际上不是用户的用户。异常也不好。因为通过ID查找用户而没有找到并不是什么特殊情况,这种情况经常发生。只有在你需要堆栈跟踪时才使用异常!而且这应该只在你的代码出现问题需要修复时发生。 - slepic

2
为什么不直接将结果传递给构建对象所需的构造函数,而不是试图使构造函数有时失败?
即使您可以使其有时失败,仍然需要在调用构造函数后进行检查以确保它实际上已经构建,而在这些行中,您可以调用->loadUser()并将结果传递给构造函数。
一个好的提示是,有人告诉我,“始终向构造函数提供构建对象所需的内容,不要让它去寻找它。”
public function __construct ($emailInTheDatabase, $otherFieldNeeded)
{
    $this->emailAddress = $emailInTheDatabase;
    $this->otherField = $otherFieldNeeded;
}

一个好的类的重点之一是,你不必每次都去调整获取数据的方式——这就是类的作用。如果我在创建类之前必须获取数据,那么我就打败了类的部分目的。 - Stephen R
@StephenR 这远非事实。无论哪种方式,您都会每次获取数据。唯一的区别在于您在何处执行此操作。如果您在构造函数中执行此操作,则除了构造函数中的操作之外,您没有其他获取数据的手段。另一方面,如果您将获取操作放在构造函数之外,则可以有多种获取用户的方式-通过电子邮件、ID、某些其他复杂条件等等...您甚至可以使用一个查询获取许多用户数据并从中构建许多用户对象。当获取操作在构造函数内部时,您永远无法做到这一点... - slepic
1
@StephenR 我可以补充一下,也许你不理解,你不应该自己实例化用户对象,应该有另一个类来负责从数据库中高效地获取数据并构造用户对象。相信我,我知道我在说什么,我曾经在一家公司工作过,他们就是这样做的 - 实体类带有接受id的构造函数,然后构造函数会从数据库中获取实体数据。成百上千的查询,至少需要等待一秒钟才能加载主页。而且没有人知道如何摆脱它... - slepic
@slepic 我在某种程度上同意你的观点——但构造函数不是唯一拉数据的地方。看看 PHP 的 DateTime::createFromFormat() 静态函数。也许我的评论措辞不太好。我认为这个答案建议在过程式代码中每次都拉取数据并传递,这是自我打败的。但可以在__constructor函数之外仍然提取数据。此问题的其他答案更好地解释了这一点。 - Stephen R
1
@StephenR 我明白你的意思。如果获取数据的方法是静态的,而不是构造函数中的话,那么它可以工作。但是,静态加载器对数据库连接有一个隐藏的依赖关系。连接的凭据必须要么硬编码在类内部,要么从某个全局变量中提取。这会降低灵活性、可读性和可测试性。DateTime类不依赖于可以以不同方式设置的数据库连接,因此它可以将此放入静态方法中。实际上,我已经发布了自己的答案来回答这个问题,请查看一下我如何做到的。 - slepic

1

感谢所有的评论和解决方案。这是我为解决问题所做的事情:(希望能帮助其他人)

// Setup the user ( we assume he is a user first. referees, admins are considered users too )
    try {
      $him = new user ($_emailAddress); 
      // check the supplied password 
      $pass_ok = $him->auth($_Password);

      // check the activation status 
      $active_ok = $him->makeActive();

    } catch (Exception $e_u) { 
      // try the groups database
      try { 
      $him = new group ($_emailAddress);
      // check the supplied password 
      $pass_ok = $him->auth($_Password);
              //var_dump ($pass_ok);

      // check the activation status 
      $active_ok = $him->makeActive();
      } catch (Exception $e_g) {
          // email address was not in any of them !!
          $pass_ok = false; $active_ok = false;
        }
    }

0

我认为不应该在构造函数中放置过多内容。您应该考虑创建用户的静态函数(工厂),而不是将所有内容放入构造函数中。这样,您仍然可以使用用户对象,而无需隐式调用加载函数,这将节省您的痛苦。

public function __construct(){}

public function setIdentifier($value){
    $this->identifier = $value;
}

public function load(){
    // whatever you need to load here
    //...
    throw new UserParameterNotSetException('identifier not set');
    // ...
    // if user cannot be loaded properly
    throw new UserNotFoundException('could not found user');
}

public static function loadUser($identifier){
    $user = new User();
    $user->setIdentifier($identifier);
    $user->load();
    return $user;
}

使用示例:

$user = new User(); 
try{
    $user->setIdentifier('identifier');
    $user->load();
}
catch(UserParameterNotSetException $e){
    //...
}
catch(UserNotFoundException $e){
    // do whatever you need to do when user is not found
}

// With the factory static function:
try{
    $user2 = User::loadUser('identifier');
}
catch(UserParameterNotSetException $e){
    //...
}
catch(UserNotFoundException $e){
    // do whatever you need to do when user is not found
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接