Search code examples
phpoopstrong-typing

PHP 'instanceof' failing with class constant


I'm working on a framework that I'm trying to type as strongly as I possibly can. (I'm working within PHP and taking some of the ideas that I like from C# and trying to utilize them within this framework.)

I'm creating a Collection class that is a collection of domain entities/objects. It's kinda modeled after the List<T> object in .Net.

I've run into an obstacle that is preventing me from typing this class. If I have a UserCollection, it should only allow User objects into it. If I have a PostCollection, it should only allow Post objects.

All Collections in this framework need to have certain basic functions, such as add, remove, iterate. I created an interface, but found that I couldn't do the following:

interface ICollection { public function add($obj) }
class PostCollection implements ICollection { public function add(Post $obj) {} }

This broke it's compliance with the interface. But I can't have the interface strongly typed because then all Collections are of the same type. So I attempted the following:

interface ICollection { public function add($obj) }
abstract class Collection implements ICollection { const type = 'null'; }
class PostCollection extends Collection {
    const type = 'Post';
    public function add($obj) {
        if (!($obj instanceof self::type)) {
            throw new UhOhException();
        }
    }
}

When I attempt to run this code, I get syntax error, unexpected T_STRING, expecting T_VARIABLE or '$' on the instanceof statement. A little research into the issue and it looks like the root of the cause is that $obj instanceof self is valid to test against the class. It appears that PHP doesn't process the entire self::type constant statement in the expression. Adding parentheses around the self::type variable threw an error regarding an unexpected '('.

An obvious workaround is to not make the type variable a constant. The expression $obj instanceof $this->type works just fine (if $type is declared as a variable, of course).

I'm hoping that there's a way to avoid that, as I'd like to define the value as a constant to avoid any possible change in the variable later. Any thoughts on how I can achieve this, or have I take PHP to it's limit in this regard? Is there a way of "escaping" or encapsulating self::this so that PHP won't die when processing it?

UPDATE

Based on the feedback, I thought of something to try -- the code below works!

Can anyone think of

  1. a reason not to do this,
  2. a reason this won't ultimately work, or
  3. a better way to pull this off?
interface ICollection { public function add($obj) {...} }
abstract class Collection { const type = null; protected $type = self::type; }
class PostCollection extends Collection {
    const type = 'Post';
    public function add($obj) {
        if (!($obj instanceof $this->type)) {
            throw new UhOhException();
        }
    }
}

UPDATE #2:

After putting the code above into production, it turns out it doesn't work. I have no idea how it worked when I tested it, but it doesn't work at all. I'm stuck with using a protected variable, I think.


Solution

  • This also works correctly, using a static:

    <?php
    
    interface ICollection { 
      public function add($obj); 
    }
    abstract class Collection implements ICollection { 
      static protected $_type = 'null'; 
    }
    class PostCollection extends Collection {
     static protected $_type = 'Post';
     public function add($obj) {
      if(!($obj instanceof self::$_type)) {
       throw new UhOhException();
      }
     }
    }
    
    
    class Post {}
    
    $coll = new PostCollection();
    $coll->add(new Post());
    

    And actually, you probably want to define your add() method on the Collection class anyway, which means you'll have to use get_class() to get around some weirdness with self::type or even self::$_type always wanting to return the base Collection class anyway, so this would probably work:

    abstract class Collection implements ICollection { 
      const type = 'null'; 
      public function add($obj) {
       $c = get_class($this);
       $type = $c::type;
       if(!($obj instanceof $type)) {
        throw new UhOhException();
       }
      }
    }
    
    class PostCollection extends Collection {
     const type = 'Post';
    }
    class Post {}
    
    $coll = new PostCollection();
    $coll->add(new Post());