6

It looks like strict errors do not occur if the classes are in one file like this:

abstract class Food{}

class Meat extends Food{}                    

abstract class Animal{          
    function feed(Food $food){ }
}

class Cat extends Animal{           
    function feed(Meat $meat){
        parent::feed($meat);
    }   
}    

But if you put the class definition in separate files and include them like that:

abstract class Food{}

class Meat extends Food{}                    

require 'Animal.php';
require 'Cat.php';

Strict standards error message is thrown:

Strict standards: Declaration of Cat::feed() should be compatible with Animal::feed(Food $food) in c:\path\to\Cat.php on line...

If all is in one file even this is ok:

class Dog extends Animal{           
  function feed($qty = 1){ 
    for($i = 0; $i < $qty; $i++){
        $Meat = new Meat();
        parent::feed($Meat);
    }       
  } 
}

Is this the intended behavior?

Because Meat is a Food, there shouldn't be a complain in the first place, right? So the solution is plain and clear: Put everything in one file and strict standards are satisfied ;)

Any hints appreciated

rink.attendant.6
  • 44,500
  • 61
  • 101
  • 156
mife
  • 105
  • 1
  • 6
  • What's the full message you're seeing? – Jonnix Jun 04 '15 at 12:59
  • Show the error message and how the code is included – John Conde Jun 04 '15 at 12:59
  • `Cat` can be cast to `Animal`, and `Animal::feed` takes `Food`, but `Cat::feed` does not, so `$animal = (Animal)$cat; $animal->feed($food)` will fail. – Siguza Jun 04 '15 at 13:02
  • How the code is included: require 'Animal.php'; require 'Cat.php'; – mife Jun 04 '15 at 13:39
  • @Siguza Thanks for the explanation, but $Meat is a Food, so doesn't mean feeding meat is feeding food? – mife Jun 04 '15 at 14:42
  • @mife Of course `Meat` is a `Food`, but not every `Food` is a `Meat`. A function may take `Animal` as parameter and feed it a `Potato`. `Potato` is a `Food`, so that's legal, right? Now, since `Cat` is an `Animal`, you can pass it to that function. Also legal, right? Only now you have the situation where `Cat` is fed a `Potato`, which, as per its class definition, is not allowed. I'd say `Cat` has in this case failed to extend `Animal`, because it literally narrowed down its functionality instead of extending (= adding to) it. – Siguza Jun 04 '15 at 18:22
  • @Siguza Feels like overriding methods in php is no fun at all – mife Jun 05 '15 at 04:28
  • @mife This has not really anything to do with PHP, more with the concept of OOP. I couldn't say it any better than **Ja͢ck** in his answer below: You are violating the [Liskov substitution principle](http://en.wikipedia.org/wiki/Liskov_substitution_principle) by making `Cat` incompatible with `Animal`. Or by declaring that *any* `Animal` can be fed *any* `Food`, which is clearly not the case. But yes, sometimes OOP can be not so fun. – Siguza Jun 05 '15 at 06:34

2 Answers2

3

Is this the intended behavior?

Unfortunately, yes. The intricacies that come into play with class declarations make it so that strict rules aren't always applied when they all occur in the same script; a single class per file doesn't exhibit this issue.

Because Meat is a Food, there shouldn't be a complain in the first place, right?

Wrong for two reasons:

  1. Meat is a smaller type than Food and therefore by only allowing the smaller type in your descendent class you're violating LSP; you can't substitute Cat for Animal.

  2. In PHP, argument types are invariant when overloading methods, i.e. the accepted types must match exactly that of the parent. While it could be argued that contravariant types make sense, due to technical reasons this can't be done.

So the solution is plain and clear: Put everything in one file and strict standards are satisfied ;)

No, and you should definitely not rely on that behaviour.

Ja͢ck
  • 170,779
  • 38
  • 263
  • 309
  • `Meat extends Food`. It is not a "smaller" type. [Liskov Substitution Principle](http://en.wikipedia.org/wiki/Liskov_substitution_principle) is not violated. All that `Food` can do, `Meat` can also do, especially in `PHP` where closing the behaviour by inheritance is not allowed (a `public` method in the base class cannot be redeclared as `private` or `protected` in the children classes). – axiac Jun 04 '15 at 17:54
  • @axiac Let's assume `Wheat extends Food` ... `Cat::feed()` should accept that type as well, but now it doesn't. – Ja͢ck Jun 04 '15 at 17:58
  • Hmm... I think you are right. LSP is violated for `Animal` vs. `Cat`. I was thinking only about `Food` vs. `Meat` before. [*"if S is a subtype of T, then objects of type T in a program may be replaced with objects of type S without altering any of the desirable properties of that program"*](http://en.wikipedia.org/wiki/Liskov_substitution_principle) -- If `Cat::feed()` accepts only `Meat` then `Cat` **cannot** replace `Animal` in `$a = new Animal(); $a->feed(new Wheat());`. I interpreted "smaller" as in "has less properties" but in fact it was "less general". – axiac Jun 04 '15 at 18:08
  • @axiac Yeah, I have to think about it every time I bring this up ;-) – Ja͢ck Jun 04 '15 at 18:11
1

It's one of the many strange behaviours of PHP when it comes to classes and namespaces.

The standard solution is to create an Interface (let's name it FoodInterface') and implement it in the base class. Then useFoodInterfaceas the type of the argument of methodfeed()`:

interface FoodInterface {}

abstract class Food implements FoodInterface {}

class Meat extends Food {}

abstract class Animal {
    function feed(FoodInterface $food) {}
}

class Cat extends Animal {           
    function feed(FoodInterface $meat) {
        parent::feed($meat);
    }
}

The FoodInterface interface can be empty or you can declare in it the functions you need to call in Animal::feed().

This way you can feed() your Cat (or any other Animal) with any object that implements the FoodInterface, no matter if they extends the Food class or not. As long as they implement the interface, they are good to be feed to any Animal.

class Toy implements FoodInterface {}

$cat = new Cat();
$cat->feed(new Toy());    // He can't eat it but at least he will have some fun :-)

Because your base class is abstract it can act as the aforementioned interface. Forget about the interface and just declare Cat::feed() with the same argument types as Animal::feed().

Then, in the implementation of Cat::feed() you can use instanceof to check if the type of the received argument is the one you want (Meat):

abstract class Food {}

class Meat extends Food {}

abstract class Animal {
    function feed(Food $food) {}
}

class Cat extends Animal {           
    function feed(Food $meat) {
        if (! $meat instanceof Meat) {
            throw new InvalidArgumentException("Cats don't eat any Food. Meat is required");
        }

        // Here you are sure the type of $meat is Meat
        // and you can safely invoke any method of class Meat
        parent::feed($meat);
    }
}

Comments

The first approach is the right way to do it. The second approach has its own advantages and I recommend using it only when the first approach is not possible for some reason.

axiac
  • 68,258
  • 9
  • 99
  • 134
  • In PHP, my understanding is that, _`Interfaces`_ are treated as `Abstract` classes. They are assumed to have 'empty methods' – Ryan Vincent Jun 04 '15 at 17:40
  • It doesn't matter how PHP treats interfaces and abstract classes internally. The important differences between interfaces and abstract classes are two: 1) an abstract class can implement some or all of its methods, it can have static methods, class properties, static properties and class constants (the interfaces cannot do any of these); 2) a class can implement many interfaces but it can extend only one class (no matter if abstract or not). Besides the domain modelling reasons, these are the technical reasons one chooses to use an interface vs. an abstract class in a certain situation. – axiac Jun 04 '15 at 17:44