В этой заметке, постараюсь, кратко, объяснить в каких случаях стоит использовать интерфейсы в PHP и привести пример практического их использования.

Интерфейсы стоит создавать когда есть общая задача и несколько вариантов ее решения, применяемых в зависимости от ситуации. Например задача сохранения данных, а в качестве решения сохранение данных:
  • в базу данных;
  • в файл;
  • в сессию и др.

В данном случае есть смысл создать один общий интерфейс с обязательными методами вроде insert() - непосредственное сохранение данных. В зависимости от места хранения данных, каждый класс будет по своему реализовывать сохранение данных, но называться данный метод всегда будет insert(), если мы пропишем его в интерфейсе.
После создания интерфейса нужно создать несколько классов-сервисов его реализующих (имплементирующих).


Для начала, давайте рассмотрим пример без использования интерфейса.

При создании приложения согласно ООП, нужно отделять логику от функционала, разбивая задачи на отдельные классы.
Допустим есть 3 класса:
- Date (общий класс, получающий данные и использующий методы вспомогательного класса(сервиса) для их сохранения);
- saveDb (класс сохраняющий данные в базу данных);
- saveFile (класс сохраняющий данные в файл)
class Date{
    
    //Db – класс сохраняющий данные в БД, $base – экземпляр этого класса
    public function date(saveDb $base){ 
        $message = array(); //получение данных из формы
        $date = $this->validate($message); //валидация
        $base->insertInto(); //вызов методов класса Db для сохранения
    }
    
    public function validate($date){
        //проверка данных
        return $date;
    }
}
в данном примере, для сохранения данных, методу date() класса Date, в качестве аргумента, нужно передать экземпляр конкретного класса - saveDb или saveFile.

Недостаток данного подхода в том, что класс проверяющий данные и класс сохраняющий данные сильно связаны между собой. В примере, мы указываем, что аргумент метода date() - это экземпляр класса работающего с базой данных. Далее используются нужные методы данного класса (saveDb) вроде insertInto().
А что, если понадобится сохранить данные не в БД, а в файл?!! В этом случае нужно будет менять весь метод date(), ведь у другого класса могут быть совсем другие методы, а не метод insertInto() и весь принцип построения другого класса может сильно отличаться. Может быть, там даже будет предусмотрена своя валидация и метод validate() класса Date окажется лишним.



Вот в таких случаях и используются интерфейсы. Они помогают в создании шаблонов для классов, которые будут использоваться в приложении и иметь общие методы делающие одни и те же задачи, но своим способом. Интерфейсы позволяют быстрее разобраться в коде и легче его поддерживать. Так же, они помогают создавать слабую связанность между классами, что можно продемонстрировать на примере:

interface Save {
    public function insert($date);
}

class saveDb implements Save {
    protected function connectDb(){}
    public function insert($date){
        echo $date.' сохранено в БД <br>';
    }
}

class saveFile implements Save {
    protected function openFile(){}
    public function insert($date){
        echo $date.' сохранено в файл <br>';
    }
    protected function closeFile(){}
}

class saveSession implements Save {
    public function insert($date){
        echo $date.' сохранено в сессию <br>';
    }
}

////////////////////////////////////
class Date {

    public $date;
    
    public function __construct($date) {
        $this->date = $date;
    }
    
    // Save – интерфейс для сохранения данных, $obj – экземпляр одного из классов реализующих данный интерфейс.
    public function save(Save $obj){ 
        $date = $this->validate($this->date); //валидация
        $obj->insert($date); //вызов методов класса Db для сохранения
    }
    
    public function validate($date){
        // тут проверка данных
        return $date;
    }
}

$date = new Date('Контент');

$db = new saveDb();
$date->save($db); //Контент сохранено в БД 

$file = new saveFile();
$date->save($file); //Контент сохранено в файл 

$ses = new saveSession();
$date->save($ses); //Контент сохранено в сессию

В моем упрощенном примере видно, что интерфейс обязует реализующие его классы иметь метод insert(), который (допустим) непосредственно занимаются вставкой данных. А кроме него, каждый класс может иметь другие необходимые для его работы методы. Для класса saveDb, это, например, метод connectDb(), создающий соединение с базой данных. Для тестирования, я сделал вывод сообщений при срабатывании метода insert().

Таким образом, при работе с данными, в конструктор нужного класса или другой метод, мы можем передавать класс-сервис, указывая, что он относится к определенному интерфейсу и быть уверенными, что для всех классов данной группы используются одинаковые методы. Информацию про данные методы (документацию), кстати, можно указать прямо в коде интерфейса возле соответствующих методов, тем более, что там будет намного меньше строк чем в реализующих его классах.

В данном примере, вместо зависимости от конкретного класса (saveDb) передается зависимость от интерфейса Save:
public function save(Save $obj)

Код класса Date не зависит от конкретного реализатора, а только от интерфейса. Стоит отметить, что данный пример построен на базе паттерна "Dependency injection", что переводится как "внедрение зависимости".
Интерфейс обеспечивает наличие указанных в нем методов во всех классах которые его реализуют. Поэтому можно быть уверенным, что вызов метода insert():
$obj->insert($date);
приведет к сохранению данных. При этом место хранения зависит от реализации данного метода в каждом из классов. Так, метод insert() класса saveDb сохраняет в базу данных, метод insert() класса saveFile сохраняет в файл и тд. Выполните код данного примера и увидите, что класс Date отработал со всеми классами интерфейса Save и при этом, код самого класса Date никак не пришлось менять.