Суть в том, что как вы и сказали: интерфейс это контракт. Можно его еще назвать "тип". Т.е. завязываясь на интерфейс мы обязуем вызывающий код передать нам штуку которая умеет делать определенный список вещей. Собственно все. Немного не правильно противопоставлять интерфейсы наследованию. Наследование это когда одна штука является спицифичной версией другой штуки. Интерфейс же в свою очередь это больше клиентская часть, когда код говорит - я хочу на вход переметр такого-то типа. Давайте простой высосаный из пальца, но довольно ясный пример:
Есть класс Parser
class Parser
{
public function getPage($url){
return $this->load($url);
}
protected function load($url){
return file_get_contents($url);
}
}
Есть класс Exchanger
class Exchanger extends Parser
{
public function getRate($currency){
return $this->load('...?id=' . $currency);
}
}
В примере выше в классе Exchanger мы унаследовали метод load из базового класса. Дальше используем
$parser = new Parser();
$parser->getPage('...');
$exchanger = new Exchanger();
$exchanger->getRate('USD');
Т.е. и там и там должен быть метод load который по http что-то откуда-то грузит и выдает данные которые дальше как-то используются и т.к. метод load уже есть в классе Parser, ну мы просто унаследовали его и все работает. Но при таком подходе появляются проблемы. Не понятно по какому принципу вообще Exchanger наследуется от Parser, это два семантически друг с другом не связанных класса. И почему у Exchanger есть метод getPage тоже не ясно. Можно пойти по другому. Сделать отдельный класс Loader с методом load и оба унаследовать от него. Это в целом тоже задачу решает, но если появится еще специфичная функциональность которую могут использовать оба наших класса? Множественного наследования нет. Добавлять эту функциональность в Loader? Опять же нарушится консистентность нашего Loader. Можно пойти третим путем. Сделать интерфейс Loader и указать что Parser и Exchanger требуют в качестве зависимости что-то, что имеет метод load. В этом случае нет проблем описанных выше, и есть доп. плюшки, например упрощается тестирование, потому что можно будет вместо обьекта который реально лезет куда-то по http передать заглушку которая просто в методе load возвращает готовые данные.