dave
dave

Reputation: 64707

How to make PHP class, extending an abstract class, require a more specific type

I am using the Table-Data-Gateway pattern. All of my models which aren't Mappers or DB_Table classes extend Model_Base. All of my mappers extend Model_MapperBase. All of my DB_Table classes extend Zend_Db_Table_Abstract.

In all of my mapper classes, I want to enforce that there is a function map, which takes a DB_Row Object, and the Model class that is maps to. Ideally, what I would like to be able to do, is in my Model_MapperBase, I would have:

abstract public function map(Model_Base $model, Zend_Db_Table_Row_Abstract $row);

And then when I implement it, I would do:

public function map(Model_User $user, Zend_Db_Table_Row_Abstract $row) {
    $user->setId($row->id)
         ->setName($row->name);
    return $user;
}

But obviously, that doesn't work, as the abstract function says it should take ANY class that extends Model_Base. But I would like to ensure that, yes, it MUST be one the extends Model_Base, but then I would like to have the implementations be more specific.

The only thing I can think of is something like:

public function map(Model_Base $user, Zend_Db_Table_Row_Abstract $row) {
  if (get_class($user) != 'Model_User') { 
      throw new Exception('Invalid Class');
  } else {
      $user->setId($row->id)
           ->setName($row->name);
      return $user;
  }
}

But that just seems, wrong on a certain level. Is there a better way to do this?

Edit: So, not being satisfied with the below, I have resorted to this:

final public function map(Model_Base $model, 
                          Zend_Db_Table_Row_Abstract $row)
{
    if (get_class($this) != get_class($model).'Mapper') {
        throw new Exception("Invalid Class: Expected " . substr(get_class($this), 0, strlen($this)-6) . '. Received ' .
            get_class($model) . '.');
    }
    return $this->mapImplementation($model, $row);
}

abstract protected function mapImplementation(Model_Base $model, 
                                              Zend_Db_Table_Row_Abstract $row);

And then have my Mapper Classes implement the mapImplementation method, but always call the map method. This accomplishes what I would like, effectively, but seems a little contrived, and only happens to work because, as of right now, all my Mapper Classes have a class with the same name sans 'mapper'.

Is there a better way to accomplish what I am trying to do, which would not depend on a naming scheme or throwing an exception in each class?

EDIT: I am not looking for "use instanceOf instead of get_class" - that is essentially the same thing for my purposes, and if I plan on extending my non-base mapper classes, I will make that change. I am looking for a way to enforce the correct class type, without manually throwing an exception.

Upvotes: 1

Views: 656

Answers (3)

Orangepill
Orangepill

Reputation: 24655

I don't know how much this gains but one approach would be to create your map method on the abstract class like this

final public function map(Model_Base $user, Zend_Db_Table_Row_Abstract $row) {
    return $this->_map($user, $row);
}

then for your User Mapper

public function _map(Model_User $user, Zend_Db_Table_Row_Abstract $row) {
    $user->setId($row->id)
         ->setName($row->name);
    return $user;
}

Upvotes: 1

Schleis
Schleis

Reputation: 43790

PHP requires that the method signatures must be the same.

the signatures of the methods must match, i.e. the type hints and the number of required arguments must be the same.

http://php.net/manual/en/language.oop5.abstract.php

This makes sense because your abstract class is defining a contract for classes that use it and you are trying to change that contract in your implementation. Naively, I should expect to be able to pass a Model_Base to this function anywhere and it should be able to work.

So in order to enforce a stricter type in derived classes you will need to some sort of type checking. I prefer the same method as in the other answer using instanceof. This allows for extensions of that class to be used without having to modify the code.

Upvotes: 0

Voitcus
Voitcus

Reputation: 4446

As this is not possible to change method's header while extending the abstract class, I think your solution is good.

But in my opinion modifying the code to

public function map(Model_Base $user, Zend_Db_Table_Row_Abstract $row) {
if (!$user instanceof Model_User)
  throw new Exception('Invalid Class');

would be better, because it is more flexible if you'd wish to override this method in descendant classes while using something like parent::map(...).

If classA extends classB, and $objectA and $objectB are instances of the classes, respectively, $objectA instanceof classB is true but get_class($objectA) === 'classA'.

Upvotes: 1

Related Questions