
В данной заметке пример такой рекурсии и подробным описанием. Цель - научиться «читать» и составлять рекурсии.
Допустим в БД есть таблица с 3-мя полями:
id
name
parent_id
где поле parent_id ссылается на другой (родительский) элемент (на id) из этой же таблицы.
Для примера - есть 8 строк в таблице с такими данными (id - name - parent_id):
1 - name1 - null
2 - name2 - 1
3 - name3 - null
4 - name4 - 1
5 - name5 - 4
6 - name6 - 5
7 - name7 - null
8 - name8 - 4
Задача.
Составить метод, который будет относительно любого из элементов строить список (дерево) начиная с родительских элементов и до текущего, с разделителем «/». Например это может понадобиться для того, чтобы сформировать свою маршрутизацию, вывести в адресной строке названия рубрик интернет-магазина:компьютернаяТехника / системныеБлоки / Core-i7
Данный пример из реального проекта использующего php-фреймворк с Active Record, т.е. каждая строка в БД представляет из себя отдельный объект. Поэтому в примере поля представляют из себя свойства объекта.
Допустим, что нужно получить такой URL для строки с id=6.
Реализация.
Метод использующий рекурсию (который добавляем в нужную модель/сущность) будет выглядеть так:public function getPath(): string { return ($this->parent_id ? $this->parent_id->getPath() . '/' : '') . $this->name; }
В нем проверяется наличие родительского элемента (по полю parent_id) и в случае наличия - рекурсивный (повторный) вызов этого же метода но уже для родительского элемента.
Выражение после return берется в скобки для того, чтобы вызов $this->name присоединить к результату который будет получен.
Вызовы метода.
Итак, метод getPath() вызван для строки с id=6. Сначала будет несколько раз выполняться условие $this->parent_id т.к. для элемента «6 - name6 - 5» заполнено свойство parent_id (5) и для элемента с id=5 так же есть родительский элемент и тд.Поэтому будет вызываться рекурсивно $this->parent->getPath() т.е. этот же метод (getPath) но каждый раз для нового (родительского) элемента. Вызов пойдет по цепочке:
6 - name6 - 5
5 - name5 - 4
4 - name4 - 1
1 - name1 - null
и только когда не будет найден $this->parent (т.е. для элемента с id=1) перестанет вызываться рекурсия, начнутся вызовы $this->name и будут возвращены данные в таком виде:
первый раз $this->name вернет «name1» и эта строка разместится на месте вызова $this->parent->getPath() добавив в конце '/', потом $this->name вернет «name4» добавив в конце '/' и тд.
Результат:
name1/name4/name5/name6
Чтобы проще понять, что же возвращает рекурсия, нужно представить ее работу с конца. Если рекурсивные вызовы в данном примере завязаны на условие $this->parent_id, то нужно мысленно подняться наверх до ближайшего элемента при котором данное условие будет нарушено и начнется по цепочке возврат данных с конца до самого первого элемента для которого изначально метод и вызывался.
Итак поднялись наверх до элемента при котором условие не выполняется и ставим, мысленно, результат выполнения метода (в примере это $this->name) на место вызова рекурсии на предыдущем шаге (в примере это $this->parent_id->getPath()). Тут мы еще к результату слэш добавляем. Остальные шаги будут выполняться аналогично с конца и до первого элемента.
Тестирование без БД.
Чаще всего такая задача стоит при извлечении данных из БД, но можно это реализовать (для тестирования) без использования Active Record фреймворков, своими объектами:<?php class Test { private $id; private $name; private $parent_id; public function __construct(int $id, string $name, self $parent_id = null) { $this->id = $id; $this->name = $name; $this->parent_id = $parent_id; } public function getPath(): string { return ($this->parent_id ? $this->parent_id->getPath() . '/' : '') . $this->name; } } $obj1 = new Test(1, 'name1', null); $obj2 = new Test(2, 'name2', $obj1); $obj3 = new Test(3, 'name3', null); $obj4 = new Test(4, 'name4', $obj1); $obj5 = new Test(5, 'name5', $obj4); $obj6 = new Test(6, 'name6', $obj5); $obj7 = new Test(7, 'name7', null); $obj8 = new Test(8, 'name8', $obj4); echo $obj6->getPath();
Результат такой же :
name1/name4/name5/name6
Для другого элемента:
echo $obj8->getPath();name1/name4/name8