[Open source, PHP, PostgreSQL, GitHub, Laravel] Как подружить ltree и Laravel

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
26-Янв-2021 11:31

Я участвую в разработке ERP системы, в детали посвящать вас не буду, это тайна за семью печатями, но скажу лишь одно, древовидных справочников у нас много и вложенность их не ограничена, кол-во в некоторых составляет несколько тысяч, а работать с ними надо максимально эффективно и удобно, как с точки зрения кода, так с точки зрения производительности.Что нам нужно
  • Быстро доставать любые срезы дерева и его ветвей, как вверх к родителям, так и вниз к листьям
  • Уметь также доставать все узлы, кроме листьев
  • Дерево должно быть консистентным, т.е. не иметь пропущенных узлов в иерархии родителей
  • Материализованные пути должны строиться автоматом
  • При перемещении или удалении узла, обновляются также все дочерние узлы и перестраиваются их пути
  • Научиться из плоской коллекции, быстро построить дерево.
  • Так как справочников много, компоненты должны быть переиспользуемые
  • Так как планируется выносить в гитхаб, задействовать абстракции и интерфейсы.
Так как мы используем Postgres, выбор пал на ltree, подробнее о том, что это такое можно прочитать в конце статьи.Установка расширенияПример миграции для создания расширения
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\DB;
class CreateLtreeExtension extends Migration
{
    public function up(): void
    {
        DB::statement('CREATE EXTENSION IF NOT EXISTS LTREE');
    }
    public function down(): void
    {
        DB::statement('DROP EXTENSION IF EXISTS LTREE');
    }
}
ЗадачаНапример, у нас есть товары и есть категории товаров. Категории представляют собой дерево и могут иметь подчиненные категории, уровень вложенности неограничен и может быть любой. Допустим это будут таблицы.Категории товаров (categories)idbigserialPKpathltreeматериализованный путьparent_idbigintegerродительская категорияВ path будут храниться материализованные пути, пример
id: 1
path: 1
parent_id: null
id: 2
path: 1.2
parent_id: 2
и тд..Товары (products)idbigserialPKcategory_idbigintegerкатегорияЕсли вы уже используете пакет для Postgres, значит вы уже знаете, что в добавляя новый extension, у нас появляется новый тип данных для Doctrine и его нужно зарегистрировать, сделать это не сложно, достаточно через composer установить этот пакет и тогда это бремя за вас сделает провайдер, который автоматом будет зарегистрирован:
composer require umbrellio/laravel-ltree
Пример миграции, использованием нового типа в Postgres
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Umbrellio\Postgres\Schema\Blueprint;
class CreateCategoryExtension extends Migration
{
    public function up(): void
    {
        Schema::table('categories', function (Blueprint $table) {
            $table->bigIncrements('id');
            $table->bigInteger('parent_id')->nullable();
            $table->ltree('path')->nullable();
            $table
                ->foreign('parent_id')
                ->references('id')
                ->on('categories');
            $table->index(['parent_id']);
            $table->unique(['path']);
        });
        DB::statement("COMMENT ON COLUMN categories.path IS '(DC2Type:ltree)'");
    }
    public function down(): void
    {
        Schema::drop('categories');
    }
}
При такой структуре самое простое, что приходит на ум, чтобы достать все дерево категорий товаров, это такой запрос:
SELECT * FROM categories;
Но, это нам вернет плоский список категорий, а отрисовать их, разумеется, нам надо в виде дерева. Поэтому самое простое решение, если бы мы не использовали ltree, был бы такой запрос:
SELECT * FROM categories WHERE parent_id IS NULL
Иными словами, только корневые категории. Ну а далее, используя рекурсию, отрисовывая корневые категории, будем дергать запрос для получения дочерних категорий, передавая туда ID категории:
SELECT * FROM categories WHERE parent_id = <ID>
Но, как вы видите, это долго, муторно и совсем не интересно, когда у нас в распоряжении есть ltree. С ним становится все гораздо проще, чтобы достать все дочерние категории, начиная от корня, ну или не от корня, а от произвольного узла, достаточно такого запроса:
SELECT * FROM categories WHERE path @> text2ltree('<ID>')
Вернемся к LaravelДля начала напишем интерфейс древовидной модели и пару методов, для работы с ltree. Как вы их писать через абстрактный класс или через трейт не так важно, дело вкуса каждого, я выбираю трейты, т.к. если мне понадобится унаследовать модели не от Eloquent\Model я всегда смогу это сделать.Пример интерфейса: LTreeInterface
<?php
namespace Umbrellio\LTree\Interfaces;
interface LTreeInterface
{
    public const AS_STRING = 1;
    public const AS_ARRAY = 2;
    public function getLtreeParentColumn(): string;
    public function getLtreeParentId(): ?int;
    public function getLtreePathColumn(): string;
    public function getLtreePath($mode = self::AS_ARRAY);
    public function getLtreeLevel(): int;
}
Пример трейта: LTreeTrait
<?php
trait LTreeTrait
{
    abstract public function getAttribute($key);
    public function getLtreeParentColumn(): string
    {
        return 'parent_id';
    }
    public function getLtreePathColumn(): string
    {
        return 'path';
    }
    public function getLtreeParentId(): ?int
    {
        $value = $this->getAttribute($this->getLtreeParentColumn());
        return $value ? (int) $value : null;
    }
    public function getLtreePath($mode = LTreeInterface::AS_ARRAY)
    {
        $path = $this->getAttribute($this->getLtreePathColumn());
        if ($mode === LTreeModelInterface::AS_ARRAY) {
            return $path !== null ? explode('.', $path) : [];
        }
        return (string) $path;
    }
    public function getLtreeLevel(): int
    {
        return is_array($path = $this->getLtreePath()) ? count($path) : 1;
Пример модели, реализующей интерфейс LTreeInterface: Category
<?php
final class Category extends Model implements LTreeInterface
{
    use LTreeTrait;
    protected $table = 'categories';
    protected $fillable = ['parent_id', 'path'];
    protected $timestamps = false;
}
Теперь, добавим несколько скоупов для удобной работы с материализованными путями:Пример трейта: LTreeTrait
<?php
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\SoftDeletes;
use Umbrellio\LTree\Collections\LTreeCollection;
use Umbrellio\LTree\Interfaces\LTreeModelInterface;
trait LTreeTrait
{
    //...
    public function scopeParentsOf(Builder $query, array $paths): Builder
    {
        return $query->whereRaw(sprintf(
            "%s @> array['%s']::ltree[]",
            $this->getLtreePathColumn(),
            implode("', '", $paths)
        ));
    }
    public function scopeRoot(Builder $query): Builder
    {
        return $query->whereRaw(sprintf('nlevel(%s) = 1', $this->getLtreePathColumn()));
    }
    public function scopeDescendantsOf(Builder $query, LTreeModelInterface $model): Builder
    {
        return $query->whereRaw(sprintf(
            "({$this->getLtreePathColumn()} <@ text2ltree('%s')) = true",
            $model->getLtreePath(LTreeModelInterface::AS_STRING),
        ));
    }
    public function scopeAncestorsOf(Builder $query, LTreeModelInterface $model): Builder
    {
        return $query->whereRaw(sprintf(
            "({$this->getLtreePathColumn()} @> text2ltree('%s')) = true",
            $model->getLtreePath(LTreeModelInterface::AS_STRING),
        ));
    }
    public function scopeWithoutSelf(Builder $query, int $id): Builder
    {
        return $query->whereRaw(sprintf('%s <> %s', $this->getKeyName(), $id));
    }
Где:
  • scopeAncestorsOf - позволяет доставать нам всех родителей вверх до корня (включая текущий узел)
  • scopeDescendantsOf - позволяет доставать всех детей вниз до листика (включая текущий узел)
  • scopeWithoutSelf - исключает текущий узел
  • scopeRoot - позволяет достать только корневые узлы 1-ого уровня
  • scopeParentsOf - почти тоже самое что и scopeAncestorsOf, только для нескольких узлов.
Т.е. добавив трейт к модели, она уже умеет при помощи нехитрых манипуляций работать с отдельными ветками, получать родителей, детей и тд. Усложним задачуПредставим, что товары могут лежать не только в листиках, но и в промежуточных узлах. На входе мы имеем категорию - уровня листик, какого уровня вложенности мы не знаем, да это и не важно. Нужно достать все товары из категории, а также из всех родительских категорий этого листика. Самое простое, что приходит на ум, нам нужно достать все идентификаторы категорий, а потом запросить товары находящиеся в выбранных категориях:
<?php
// ID категории (листика) = 15
$categories = Category::ancestorsOf(15)->get()->pluck('id')->toArray();
$products = Product::whereIn('category_id', $caregories)->get();
Рисуем деревоДа, мы научились работать с древовидной структурой на уровне БД, но как же отрисовать дерево, все еще не понятно.Задача все та же, это должен быть 1 запрос и нарисовать мы должны дерево, вернемся к запросу:
SELECT * FROM categories;
Для того, чтобы используя этот запрос, у нас было дерево, первое что приходит на ум, это нам надо каким-то образом преобразовать массив вида:
<?php
$a = [
  [1, '1', null],
  [2, '1.2', 1],
  [3, '1.2.3', 2],
  [4, '4', null],
  [5, '1.2.5', 2],
  [6, '4.6', 4],
  // ...
];
К такому:
<?php
$a = [
    0 => [
        'id' => 1,
        'level' => 1,
        'children' => [
            0 => [
                'id' => 2,
                'level' => 2,
                'children' => [
                    0 => [
                        'id' => 3,
                        'level' => 3,
                        'children' => [],
                    ],
                    1 => [
                        'id' => 5,
                        'level' => 3,
                        'children' => [],
                    ],
                ]
            ]
        ]
   ],
   1 => [
       'id' => 4,
       'level' => 1,
       'children' => [
            0 => [
                'id' => 6,
                'level' => 2,
                'children' => [],
            ]
       ]
   ]
];
Тогда при помощи обычной рекурсии мы смогли бы отрисовать дерево, пример грубый, написан на коленке, но это сейчас не важно:
<?php
$categories = Category::all()->toTree(); // Collection
function renderTree(Collection $collection) {
   /** @var LTreeNode $item */
   foreach ($collection as $item) {
      if ($item->children->isNotEmpty()) {
         renderTree($item->children);
         return;
      }
   }
   echo str_pad($item->id, $item->level - 1, "---", STR_PAD_LEFT) . PHP_EOL;
}
Немного измененийДоработаем нашу модель, а именно трейт, добавив в него методы для гидрации в особую коллекцию:
<?php
trait LTreeTrait
{
    //...
    public function newCollection(array $models = []): LTreeCollection
    {
        return new LTreeCollection($models);
    }
    public function ltreeParent(): BelongsTo
    {
        return $this->belongsTo(static::class, $this->getLtreeParentColumn());
    }
    public function ltreeChildren(): HasMany
    {
        return $this->hasMany(static::class, $this->getLtreeParentColumn());
    }
}
Данный метод будет возвращать специальную коллекцию, получив на входе плоский массив, преобразует его к дереву, когда это будет необходимо.Пример коллекции: LTreeCollection
<?php
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Umbrellio\LTree\Helpers\LTreeBuilder;
use Umbrellio\LTree\Helpers\LTreeNode;
use Umbrellio\LTree\Interfaces\HasLTreeRelations;
use Umbrellio\LTree\Interfaces\LTreeInterface;
use Umbrellio\LTree\Interfaces\ModelInterface;
use Umbrellio\LTree\Traits\LTreeModelTrait;
class LTreeCollection extends Collection
{
    private $withLeaves = true;
    public function toTree(bool $usingSort = true, bool $loadMissing = true): LTreeNode
    {
        if (!$model = $this->first()) {
            return new LTreeNode();
        }
        if ($loadMissing) {
            $this->loadMissingNodes($model);
        }
        if (!$this->withLeaves) {
            $this->excludeLeaves();
        }
        $builder = new LTreeBuilder(
            $model->getLtreePathColumn(),
            $model->getKeyName(),
            $model->getLtreeParentColumn()
        );
        return $builder->build($collection ?? $this, $usingSort);
    }
    public function withLeaves(bool $state = true): self
    {
        $this->withLeaves = $state;
        return $this;
    }
    private function loadMissingNodes($model): self
    {
        if ($this->hasMissingNodes($model)) {
            $this->appendAncestors($model);
        }
        return $this;
    }
    private function excludeLeaves(): void
    {
        foreach ($this->items as $key => $item) {
            if ($item->ltreeChildren->isEmpty()) {
                $this->forget($key);
            }
        }
    }
    private function hasMissingNodes($model): bool
    {
        $paths = collect();
        foreach ($this->items as $item) {
            $paths = $paths->merge($item->getLtreePath());
        }
        return $paths
            ->unique()
            ->diff($this->pluck($model->getKeyName()))
            ->isNotEmpty();
    }
    private function appendAncestors($model): void
    {
        $paths = $this
            ->pluck($model->getLtreePathColumn())
            ->toArray();
        $ids = $this
            ->pluck($model->getKeyName())
            ->toArray();
        $parents = $model::parentsOf($paths)
            ->whereKeyNot($ids)
            ->get();
        foreach ($parents as $item) {
            $this->add($item);
        }
    }
}
Эта коллекция по сути ничем не отличается от встроенной в Laravel, т.е. если ее итерировать мы все еще будем иметь плоский список. Но если вызвать метод toTree, то плоская коллекция, где все категории всех уровней вложенности были на одном уровне, рекурсивно выстроятся в свойства children и мы получим из обычного плоского массива - многоуровневый массив соответствующий нашему дереву.А используя специальные скоупы, мы сможем строить деревья для отдельных ветвей, например:
<?php
$categories = Category::ancestorsOf(15)->get()->toTree();
Также нам понадобится еще два класса, которые будут собственно строить дерево, это класс - представляющий узел и класс билдера, который будет рекурсивно обходить коллекцию и выстраивать узлы в нужном нам порядке (и сортировке):LTreeNode
<?php
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use InvalidArgumentException;
use Umbrellio\Common\Contracts\AbstractPresenter;
use Umbrellio\LTree\Collections\LTreeCollection;
use Umbrellio\LTree\Interfaces\LTreeInterface;
use Umbrellio\LTree\Interfaces\ModelInterface;
class LTreeNode extends AbstractPresenter
{
    protected $parent;
    protected $children;
    public function __construct($model = null)
    {
        parent::__construct($model);
    }
    public function isRoot(): bool
    {
        return $this->model === null;
    }
    public function getParent(): ?self
    {
        return $this->parent;
    }
    public function setParent(?self $parent): void
    {
        $this->parent = $parent;
    }
    public function addChild(self $node): void
    {
        $this
            ->getChildren()
            ->add($node);
        $node->setParent($this);
    }
    public function getChildren(): Collection
    {
        if (!$this->children) {
            $this->children = new Collection();
        }
        return $this->children;
    }
    public function countDescendants(): int
    {
        return $this
            ->getChildren()
            ->reduce(
                static function (int $count, self $node) {
                    return $count + $node->countDescendants();
                },
                $this
                    ->getChildren()
                    ->count()
            );
    }
    public function findInTree(int $id): ?self
    {
        if (!$this->isRoot() && $this->model->getKey() === $id) {
            return $this;
        }
        foreach ($this->getChildren() as $child) {
            $result = $child->findInTree($id);
            if ($result !== null) {
                return $result;
            }
        }
        return null;
    }
    public function each(callable $callback): void
    {
        if (!$this->isRoot()) {
            $callback($this);
        }
        $this
            ->getChildren()
            ->each(static function (self $node) use ($callback) {
                $node->each($callback);
            });
    }
    public function toCollection(): LTreeCollection
    {
        $collection = new LTreeCollection();
        $this->each(static function (self $item) use ($collection) {
            $collection->add($item->model);
        });
        return $collection;
    }
    public function pathAsString()
    {
        return $this->model ? $this->model->getLtreePath(LTreeInterface::AS_STRING) : null;
    }
    public function toTreeArray(callable $callback)
    {
        return $this->fillTreeArray($this->getChildren(), $callback);
    }
    /**
     * Usage sortTree(['name' =>'asc', 'category'=>'desc'])
     * or callback with arguments ($a, $b) and return -1 | 0 | 1
     * @param array|callable $options
     */
    public function sortTree($options)
    {
        $children = $this->getChildren();
        $callback = $options;
        if (!is_callable($options)) {
            $callback = $this->optionsToCallback($options);
        }
        $children->each(static function ($child) use ($callback) {
            $child->sortTree($callback);
        });
        $this->children = $children
            ->sort($callback)
            ->values();
    }
    private function fillTreeArray(iterable $nodes, callable $callback)
    {
        $data = [];
        foreach ($nodes as $node) {
            $item = $callback($node);
            $children = $this->fillTreeArray($node->getChildren(), $callback);
            $item['children'] = $children;
            $data[] = $item;
        }
        return $data;
    }
    private function optionsToCallback(array $options): callable
    {
        return function ($a, $b) use ($options) {
            foreach ($options as $property => $sort) {
                if (!in_array(strtolower($sort), ['asc', 'desc'], true)) {
                    throw new InvalidArgumentException("Order '${sort}'' must be asc or desc");
                }
                $order = strtolower($sort) === 'desc' ? -1 : 1;
                $result = $a->{$property} <=> $b->{$property};
                if ($result !== 0) {
                    return $result * $order;
                }
            }
            return 0;
        };
    }
}
LTreeBuilder
<?php
class LTreeBuilder
{
    private $pathField;
    private $idField;
    private $parentIdField;
    private $nodes = [];
    private $root = null;
    public function __construct(string $pathField, string $idField, string $parentIdField)
    {
        $this->pathField = $pathField;
        $this->idField = $idField;
        $this->parentIdField = $parentIdField;
    }
    public function build(LTreeCollection $items, bool $usingSort = true): LTreeNode
    {
        if ($usingSort === true) {
            $items = $items->sortBy($this->pathField, SORT_STRING);
        }
        $this->root = new LTreeNode();
        foreach ($items as $item) {
            $node = new LTreeNode($item);
            [$id, $parentId] = $this->getNodeIds($item);
            $parentNode = $this->getNode($parentId);
            $parentNode->addChild($node);
            $this->nodes[$id] = $node;
        }
        return $this->root;
    }
    private function getNodeIds($item): array
    {
        $parentId = $item->{$this->parentIdField};
        $id = $item->{$this->idField};
        if ($id === $parentId) {
            throw new LTreeReflectionException($id);
        }
        return [$id, $parentId];
    }
    private function getNode(?int $id): LTreeNode
    {
        if ($id === null) {
            return $this->root;
        }
        if (!isset($this->nodes[$id])) {
            throw new LTreeUndefinedNodeException($id);
        }
        return $this->nodes[$id];
    }
}
Для простоты отрисовки дерева напишем еще два абстрактных ресурса:AbstractLTreeResource
<?php
namespace Umbrellio\LTree\Resources;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Support\Collection;
use Umbrellio\LTree\Collections\LTreeCollection;
abstract class LTreeResourceCollection extends ResourceCollection
{
    /**
     * @param LTreeCollection|Collection $resource
     */
    public function __construct($resource, $sort = null, bool $usingSort = true, bool $loadMissing = true)
    {
        $collection = $resource->toTree($usingSort, $loadMissing);
        if ($sort) {
            $collection->sortTree($sort);
        }
        parent::__construct($collection->getChildren());
    }
}
AbstractLTreeResourceCollection
<?php
namespace Umbrellio\LTree\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Umbrellio\LTree\Helpers\LTreeNode;
use Umbrellio\LTree\Interfaces\LTreeInterface;
/**
* @property LTreeNode $resource
*/
abstract class LTreeResource extends JsonResource
{
    final public function toArray($request)
    {
        return array_merge($this->toTreeArray($request, $this->resource->model), [
            'children' => static::collection($this->resource->getChildren())->toArray($request),
        ]);
    }
    /**
     * @param LTreeInterface $model
     */
    abstract protected function toTreeArray($request, $model);
}
Вперед на амбразуруИспользовать примерно будем так, создадим два ресурса JsonResource:CategoryResource
<?php
use Umbrellio\LTree\Helpers\LTreeNode;
use Umbrellio\LTree\Resources\LTreeResource;
class CategoryResource extends LTreeResource
{
    public function toTreeArray($request, LTreeNode $model)
    {
        return [
            'id' => $model->id,
            'level' => $model->getLtreeLevel(),
        ];
    }
}
CategoryResourceCollection
<?php
use Umbrellio\LTree\Resources\LTreeResourceCollection;
class CategoryResourceCollection extends LTreeResourceCollection
{
    public $collects = CategoryResource::class;
}
Представим, что у вас есть контроллер CategoryController и метод АПИ data возвращающий категории в формате json:Пример контроллера
<?php
use Illuminate\Routing\Controller;
use Illuminate\Http\Request;
class CategoryController extends Controller
{
    //...
    public function data(Request $request)
    {
        return response()->json(
            new CategoryResourceCollection(
                Category::all(),
                ['id' => 'asc']
            )
        );
    }
}
Используя специальные ресурсы, вам не нужно принудительно вызывать метод toTree, т.к. все методы Eloquent\Builder-а (get, all, first и тд) в модели реализующей интерфейс LtreeInterface возвращают LtreeCollection, то при использовании данных ресурсов, плоская коллекция автоматически преобразуется к дереву, причем без дополнительных запросов к БД.Читать научились, научимся писатьВсе описанное выше, помогло нам прочитать дерево из базы данных, отдельную ветвь или все дерево, а также мы научились отрисовывать дерево.Но чтобы это все мы могли делать, мы должны сначала это дерево уметь сохранять, для этого необходимо сохранить в БД материализованные пути для каждого узла нашего будущего дерева.Первое что приходит на ум, это форма с двумя полями:idinputparent_idselectТ.е. мы создаем элемент, и если нам нужен корневой, то parent_id не заполняем, а если нужно то заполняем. А path по идее должен генерироваться автоматически на основании id и parent_id.Для этого создадим сервис, который будем вызывать после сохранения нашей модели.Пример сервиса генерирующего path
<?php
namespace Umbrellio\LTree\Services;
use Illuminate\Database\Eloquent\Model;
use Umbrellio\LTree\Helpers\LTreeHelper;
use Umbrellio\LTree\Interfaces\LTreeModelInterface;
use Umbrellio\LTree\Interfaces\LTreeServiceInterface;
final class LTreeService implements LTreeServiceInterface
{
    private $helper;
    public function __construct(LTreeHelper $helper)
    {
        $this->helper = $helper;
    }
    public function createPath(LTreeModelInterface $model): void
    {
        $this->helper->buildPath($model);
    }
    public function updatePath(LTreeModelInterface $model): void
    {
        $columns = array_intersect_key($model->getAttributes(), array_flip($model->getLtreeProxyUpdateColumns()));
        $this->helper->moveNode($model, $model->ltreeParent, $columns);
        $this->helper->buildPath($model);
    }
    public function dropDescendants(LTreeModelInterface $model): void
    {
        $columns = array_intersect_key($model->getAttributes(), array_flip($model->getLtreeProxyDeleteColumns()));
        $this->helper->dropDescendants($model, $columns);
    }
}
  • createPath - создает path для нового узла, нужно вызывать после создания
  • updatePath - обновляет path при редактировании. Тут важно понимать, что меняя path текущего узла, необходимо также обновить и пути всех его дочерних элементов, и желательно сделать это одним запросом, т.к. в случае если дочерних элементов будет 1000 - делать тысячу запросов на UPDATE как-то не комильфо.
    т.е. если мы перемещаем узел в другую подветвь, то вместе с ним перемещаются также и все его дети.
  • dropDescendants - метод удаляющий всех детей, при удалении узла, тут тоже важно понимать очередность, нельзя удалить узел, не удалив детей, вы сделаете дерево неконсистентным, а детей нужно удалять прежде, чем будете удалять узел.
Методы getLtreeProxyDeleteColumns и getLtreeProxyUpdateColumns - нужны для проксирования полей типа deleted_at, updated_at, editor_id и других полей, которые вы также хотите обновить в дочерних узлах при обновлении текущего узла или удалении.
<?php
class CategoryService
{
   private LTreeService $service;
   public function __construct (LTreeService $service)
   {
      $this->service = $service;
   }
   public function create (array $data): void
   {
       $model = App::make(Category::class);
       $model->fill($data);
       $model->save();
       // создаем материализованный путь для узла
       $this->service->createPath($model);
   }
}
Конечно, можно запилить немного магии, добавить эвенты, листенеры и дергать методы автоматически, но я больше предпочитаю самому вызывать нужные методы, так хоть есть немного понимания, что происходит за кулисами + можно все это обернуть в транзакцию и не бояться, что где-то вылезет Deadlock.Подведем итогЕсли у вас Postgres / PHP / Laravel и у вас есть потребность в использовании древовидных справочников (категории, группы и тд), вложенность неограниченная, и вы хотите быстро и просто доставать любые срезы ветвей, вам точно будет полезен этот пакет.Использовать его достаточно просто:
  • Подключаете зависимость в composer: umbrellio/laravel-ltree
  • Пишете миграцию с использованием типа ltree (добавляете parent_id и path в вашу таблицу-справочник)
  • Имплементируете интерфейс LTreeModelInterface и подключаете трейт LTreeModelTrait в Eloquent\Model-и.
  • Используете сервис LTreeService при операциях создания / обновления и удаления модели
  • Используете ресурсы LTreeResource и LTreeResourceCollection, если у вас SPA
Ресурсы для изучения Спасибо за внимание.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_open_source, #_php, #_postgresql, #_github, #_laravel, #_ltree, #_postgresql, #_postgres, #_php, #_laravel, #_derevja (деревья), #_open_source, #_open_source, #_php, #_postgresql, #_github, #_laravel
Профиль  ЛС 
Показать сообщения:     

Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы

Текущее время: 22-Ноя 18:07
Часовой пояс: UTC + 5