Abschnittsbaum der unbegrenzten Verschachtelung und URL

  • Tutorial
In diesem Artikel werden wir einen der möglichen Ansätze betrachten, um einen vollständigen Pfad zu einer Partition zu generieren, der in anderen Partitionen unbegrenzt verschachtelt sein kann, und die gewünschte Partition schnell über einen bestimmten Pfad zu erhalten.

Stellen Sie sich vor, wir programmieren einen Online-Shop, in dem es einen Baum mit verschiedenen Abschnitten geben soll, und es sollte auch „angenehme“ Links zu Abschnitten geben, die alle Abschnitte enthalten. Beispiel http://example.com/catalog/category/sub-category.

Abschnitte


Die naheliegendste Möglichkeit besteht darin, eine übergeordnete parent_idBeziehung über ein Attribut und eine Beziehung zu erstellen parent.

class Category extends Model
{
    public function parent()
    {
        return $this->belongsTo(self::class);
    }
}

Unser Modell hat auch ein Attribut slug- einen Stub, der den Abschnitt in der URL widerspiegelt. Sie kann aus dem Namen generiert oder vom Benutzer manuell angegeben werden. Am wichtigsten ist, dass der Stub die Validierungsregel erfüllt alphadash(dh aus Buchstaben, Zahlen und Zeichen -besteht _) und auch innerhalb des übergeordneten Abschnitts eindeutig ist. Für letztere reicht es aus, einen eindeutigen Index in der Datenbank zu erstellen (parent_id, slug).

Um einen Link zu einem Abschnitt zu erhalten, müssen Sie alle seine Eltern der Reihe nach ziehen. Die URL-Generierungsfunktion sieht ungefähr so ​​aus:

public function getUrl()
{ 
    $url = $this->slug;
    $category = $this;
    while ($category = $category->parent) {
        $url = $category->slug.'/'.$url;
    }
    return 'catalog/'.$url;
}

Je mehr der Abschnitt Vorfahren hat, desto mehr Abfragen werden in der Datenbank ausgeführt. Dies ist jedoch nur ein Teil des Problems. Wie erstelle ich eine Route zum Abschnitt? Lass es uns versuchen:

$router->get('catalog/{category}', ...);

Füttere den Browser einen Link http://example.com/catalog/category. Die Route wird funktionieren. Nun ist der folgende Link: http://example.com/catalog/category/sub-category. Die Route wird nicht mehr funktionieren, weil Backslash ist ein Parameter-Begrenzer. Hmm, dann füge einen weiteren Parameter hinzu und mache es optional:

$router->get('catalog/{category}/{subcategory?}', ...);

Diese Route funktioniert bereits, aber wenn Sie der URL einen weiteren Unterabschnitt hinzufügen, funktioniert nichts. Und das Problem ist, dass die Anzahl solcher Unterabschnitte nicht begrenzt ist.

Um den erforderlichen Abschnitt aus der Datenbank zu erhalten, müssen Sie zunächst den Abschnitt mit der Kennung categoryund, falls angegeben, den Unterabschnitt subcategoryusw. suchen . All dies verursacht Unannehmlichkeiten und belastet den Server stärker. Die Anzahl der Anforderungen ist proportional zur Anzahl der Unterabschnitte.

Optimierung


Die Erweiterung für laravel kalnoy / nestedset hilft uns dabei, die Anzahl der Anfragen erheblich zu reduzieren . Es soll die Arbeit mit Bäumen vereinfachen.

Installation


Die Installation ist sehr einfach. Zuerst müssen Sie die Erweiterung über Composer installieren:

composer require kalnoy/nestedset

Das Modell benötigt zwei zusätzliche Attribute, die bei der neuen Migration hinzugefügt werden müssen:

Schema::table('categories', function (Blueprint $table) {
    $table->unsignedInteger('_lft');
    $table->unsignedInteger('_rgt');
});

Jetzt müssen Sie nur noch die alten Beziehungen löschen parentund children, falls vorhanden, Eigenschaften hinzufügen Kalnoy\Nestedset\NodeTrait. Nach dem Update sieht unser Modell so aus:

class Category extends Model
{
    use Kalnoy\Nestedset\NodeTrait;
}

der Wert jedoch _lftund _rgtdie Dinge Arbeit nicht gefüllt ist, war der letzte Schliff:

Category::fixTree();

Dieser Code repariert den Baum basierend auf dem Attribut parent_id.

Vereinfachte Erzeugung


Der URL-Generierungsprozess sieht folgendermaßen aus:

public function getUrl()
{
    // Получаем заглушки всех предков
    $slugs = $this->ancestors()->lists('slug');
    // Добавляем заглушку самого раздела
    $slugs[] = $this->slug;
    // И склеиваем это все
    return 'catalog/'.implode('/', $slugs);
}

Viel einfacher, oder? Unabhängig davon, wie viele Nachkommen dieser Abschnitt hat, werden sie alle auf einmal angefordert. Aber die Routen sind nicht so einfach. Es schlägt immer noch fehl, die Partitionskette in einer Anforderung abzurufen.

Routen


Aufgabennummer 1. Wie lege ich eine Route zu einem Abschnitt fest, der alle seine Vorfahren in einem Link anzeigt?

Aufgabennummer 2. Wie komme ich mit einer Anfrage zum gewünschten Abschnitt?

Routenbeschreibung


Die Antwort auf die erste Aufgabe: Verwenden Sie den gesamten Pfad als Routenparameter .

$router->get('catalog/{path}', 'CategoriesController@show')
       ->where('path', '[a-zA-Z0-9/_-]+');

Wir weisen lediglich darauf hin, dass der Parameter {path}nicht nur den üblichen String, sondern auch einen Backslash enthalten kann. Somit erfasst dieser Parameter sofort den gesamten Pfad, der dem Steuerwort folgt catalog.

Jetzt erhalten wir im Eingabecontroller nur einen Parameter, aber wir können ihn in alle Unterabschnitte unterteilen:

public function show($path)
{
    $path = explode('/', $path);
}

Dies vereinfachte jedoch nicht die Aufgabe, den in dem Link angegebenen Abschnitt zu erhalten.

Eine Reihe von Pfaden mit einem Abschnitt


Wie kann man diesen Prozess optimieren? Speichern Sie den vollständigen Pfad für jede Partition in der Datenbank .

Angenommen, es gibt einen so einfachen Baum:

- Category
-- Sub category
--- Sub sub category

Die folgenden Pfade entsprechen diesen Abschnitten:

- category
-- category/sub-category
--- category/sub-category/sub-sub-category

Dann kann die gewünschte Kategorie ganz einfach ermittelt werden:

public function show($path)
{
    $category = Category::where('path', '=', $path)->firstOrFail();
}

Jetzt speichern wir in der Datenbank, was zuvor für den Link generiert wurde, und die Link-Generierung wird jetzt erheblich vereinfacht:

// Генерация пути
public function generatePath()
{
    $slugs = $this->ancestors()->lists('slug');
    $slugs[] = $this->slug;
    $this->path = implode('/', $slugs);
    return $this;
}
// Получение ссылки
public function getUrl()
{
    return 'catalog/'.$this->path;
}

Wenn Sie sich die Liste der Pfade im Beispiel genau ansehen, werden Sie feststellen, dass der Pfad für jedes Modell dieser ist путь-родителя/заглушка-модели. Daher kann die Pfadgenerierung weiter optimiert werden:

public function generatePath()
{
    $slug = $this->slug;
    $this->path = $this->isRoot() ? $slug : $this->parent->path.'/'.$slug;
    return $this;
}

Das folgende Problem bleibt bestehen. Wenn der Stub eines Abschnitts aktualisiert wird oder der Abschnitt den übergeordneten Abschnitt ändert, sollten die Links aller untergeordneten Abschnitte aktualisiert werden. Der Algorithmus ist einfach: Holen Sie sich alle Nachkommen und generieren Sie einen neuen Pfad für sie. Nachfolgend finden Sie die Aktualisierungsmethode für Nachkommen:

public function updateDescendantsPaths()
{
    // Получаем всех потомков в древовидном порядке
    $descendants = $this->descendants()->defaultOrder()->get();
    // Данный метод заполняет отношения parent и children
    $descendants->push($this)->linkNodes()->pop();
    foreach ($descendants as $model) {
        $model->generatePath()->save();
    }
}

Lassen Sie uns näher darauf eingehen.

In der ersten Zeile erhalten wir alle Nachkommen (in einer Anfrage). defaultOrderHier wird die Baumsortierung angewendet. Seine Bedeutung ist, dass in der Liste jeder Abschnitt nach seinem Vorfahren steht . Der Pfadbildungsalgorithmus verwendet das übergeordnete Element. Daher muss das übergeordnete Element seinen Pfad aktualisieren, bevor der Pfad eines der untergeordneten Elemente aktualisiert wird.

Die zweite Zeile sieht etwas seltsam aus. Ihre Bedeutung ist, dass sie die Beziehung ausfüllt parent, die im Algorithmus zur Pfadgenerierung verwendet wird. Wenn Sie diese Optimierung nicht verwenden, führt jeder Aufruf generatePatheine Anforderung aus, um den Wert der Beziehung abzurufen parent. DabeilinkNodesEs funktioniert mit einer Sammlung von Partitionen und führt keine Abfragen in der Datenbank durch. Damit dies für die unmittelbaren untergeordneten Elemente des aktuellen Abschnitts funktioniert, müssen Sie es der Sammlung hinzufügen. Fügen Sie den aktuellen Abschnitt hinzu, binden Sie alle Abschnitte aneinander und entfernen Sie ihn.

Gehen Sie am Ende alle Nachkommen durch und aktualisieren Sie ihre Pfade.

Es bleibt nur zu entscheiden, wann diese Methode aufgerufen wird. Events sind dafür toll:

  1. Vor dem Speichern des Modells prüfen wir, ob sich die Attribute geändert slugoder geändert haben parent_id. Wenn geändert, dann rufen Sie die Methode auf generatePath;

  2. Nachdem das Modell erfolgreich gespeichert wurde, prüfen wir, ob sich das Attribut nicht geändert hat path, und rufen bei Änderung die Methode auf updateDescendantsPaths.

protected static function boot()
{
    static::saving(function (self $model) {
        if ($model->isDirty('slug', 'parent_id')) {
            $model->generatePath();
        }
    });
    static::saved(function (self $model) {
        // Данная переменная нужна для того, чтобы потомки не начали вызывать 
        // метод, т.к. для них путь также изменится
        static $updating = false;
        if ( ! $updating && $model->isDirty('path')) {
            $updating = true;
            $model->updateDescendantsPaths();
            $updating = false;
        }
    });
}

Ergebnisse


Die Vorteile dieses Ansatzes:

  • Erstellen Sie sofort einen Abschnittslink
  • Holen Sie sich schnell einen Abschnitt auf dem Weg

Nachteile:

  • Die Pfade werden in der Datenbank gespeichert, wodurch sich die Größe der Tabelle geringfügig erhöht
  • Wenn Sie den Stub eines Abschnitts ändern, müssen Sie die Pfade aller Nachkommen aktualisieren

Tatsächlich überwiegen die Vorteile die Mängel bei weitem, da Sie Verknüpfungen generieren und Abschnitte viel häufiger abrufen müssen als Stubs aktualisieren. und die Überbeanspruchung von Raum in gewisser Weise ist spärlich.

Produkte


Erwägen Sie Ansätze zum Generieren von Produktverknüpfungen, die einen Abschnittspfad enthalten. Zum Beispiel: http://example.com/catalog/category/sub-catagory/product. Das Hauptproblem dabei ist, die richtige Route zu bilden.

Das Produkt verfügt wie der Abschnitt über einen Stub, der manuell angegeben oder anhand des Namens generiert werden kann. Es ist wichtig, dass dieser Stub innerhalb der Partition eindeutig ist, damit keine Konflikte auftreten. Es ist am besten, einen eindeutigen Index in der Datenbank zu erstellen (category_id, slug).

Versuchen wir die einfachste Option und betrachten die folgenden Routen:

// Маршрут до раздела
$router->get('catalog/{path}', function ($path) {
    return 'category = '.$path;
})->where('path', '[a-zA-Z0-9\-/_]+');
// Маршрут до товара
$router->get('catalog/{category}/{product}', function ($category, $product) {
    return 'category = '.$category.'
product = '.$product; })->where('category', '[a-zA-Z0-9\-/_]+');

Die erste Route sollte bereits bekannt sein - dies ist die Abschnittsausgaberoute. Die zweite Route ist fast die gleiche, nur am Ende wird ein weiterer Parameter hinzugefügt, der ein bestimmtes Produkt in diesem Abschnitt anzeigen soll. Wenn wir versuchen, das obige Beispiel in die Browserzeile einzugeben, erhalten wir Folgendes:

category = category/sub-category/product

Die erste Route hat funktioniert; nicht ganz das, was zu erwarten war. Das liegt daran, dass die erste Route für jede Zeile ausgelöst wird, die mit einem Schlüsselwort beginnt catalog. Müssen Routen tauschen. Dann bekommen wir:

category = category/sub-category
product = product

Großartig! Das ist besser, aber das ist noch nicht alles. Lassen Sie uns versuchen dies die URL: http://example.com/catalog/category/sub-category. Wir bekommen folgendes:

category = category
product = sub-category

Jetzt wird nur noch der Weg zur Ware ausgelöst. Es ist notwendig, den Produktstummel deutlich vom Stummelabschnitt zu trennen. Dazu können Sie eine Art Präfix / Postfix verwenden. Fügen Sie beispielsweise am Ende oder am Anfang des Stubs eines Produkts seine numerische Kennung hinzu: Es muss

http://example.com/catalog/category/sub-category/123-product

nur noch eine Einschränkung für den Parameter hinzugefügt werden {product}:

$router->get(...)->where('product', '[0-9]+-[a-zA-Z0-9_-]+');

In diesem Fall sieht die Generierung des Produktstubs folgendermaßen aus:

$product->slug = $product->id.'-'.str_slug($product->name);

Link-Generierung:

$url = 'catalog/'.$product->category->path.'/'.$product->slug;

Wareneingang im Controller:

public function show($categoryPath, $productSlug)
{
    // Сначала находим раздел по пути
    $category = Category::where('path', '=', $categoryPath)->firstOrFail();
    // Затем в этом разделе ищем товар с указанной заглушкой
    $product = $category->products()
                        ->where('slug', '=', $productSlug)
                        ->firstOrFail();
}

Hier tritt jedoch eine Bedingung auf: Stichabschnitte sollten nicht mit einer Zahl beginnen. Andernfalls wird anstelle der Route zum Abschnitt die Route zur Ware ausgelöst.

Sie können eine Art statisches Präfix verwenden, zum Beispiel p-:

http://example.com/catalog/category/sub-category/p-product

$router->get('catalog/{category}/p-{product}', ...);

$product->slug = str_slug($product->name);

$url = 'catalog/'.$product->category->path.'/p-'.$product->slug;

Der Controller-Code bleibt wie im vorherigen Fall erhalten.

Die letzte Option ist die schwierigste. Das Wesentliche ist, Links zu Abschnitten und Produkten in einer separaten Tabelle zu speichern.

Das Modell sieht ungefähr so ​​aus:

class Url extends Model
{
    // Полиморфное отношение
    public function model()
    {
        return $this->morphTo();
    }
}

Bei diesem Ansatz ist nur eine Route ausreichend:

$router->get('catalog/{path}', function ($path) {
    $url = Url::findOrFail($path);
    // Извлекаем модель используя отношение
    $model = $url->model;
    if ($model instanceof Product) {
        return $this->renderProduct($model);
    }
    return $this->renderCategory($model);
})
->where('path', '[a-zA-Z0-9\-/_]+');

Das Modell Urlhat eine polymorphe Beziehung zu anderen Modellen und speichert die vollständigen Pfade zu diesen. Was es gibt:

  • Für das Produkt sind keine Präfixe / Postfixes erforderlich
  • Sie können frühere Versionen von URLs speichern und zu neuen umleiten, d. H. SEO leidet nicht beim Ändern der Seitenadresse
  • Es ist nicht notwendig, sich nur auf Abschnitte / Waren zu beschränken, Sie können jede andere Ressource speichern

Dieser Ansatz wird sehr bedingt als Denkanstoß bezeichnet. Vielleicht zieht sich das ja über eine separate Nebenstelle.

Schlussfolgerungen


In diesem Artikel haben wir die wichtigsten Erweiterungsoptionen kalnoy/nestedsetsowie Ansätze zum Herstellen von Verknüpfungen zu Abschnitten und Produkten für den Fall untersucht, dass die Tiefe der Abschnitte nicht beschränkt ist.

Als Ergebnis wurde eine Methode erhalten, mit der Sie Links generieren können, ohne Abfragen in der Datenbank vornehmen zu müssen, und Abschnitte von dem Link in einer Anforderung erhalten können.

Alternativ zum Speichern von Pfaden in der Datenbank können Sie das Zwischenspeichern generierter Links verwenden. In diesem Fall müssen Sie die Links nicht aktualisieren und nur den Cache zurücksetzen.

Jetzt auch beliebt: