malyusha / path-history
Laravel package that allows you to create nested/human-readable paths(urls) for your eloquent models from attributes.
Installs: 1 593
Dependents: 0
Suggesters: 0
Security: 0
Stars: 4
Watchers: 1
Forks: 0
Open Issues: 0
Requires
- php: ^7.0
- illuminate/database: ^5.4 || ^5.5 || ^5.6 || ^5.7
- illuminate/routing: ^5.4 || ^5.5 || ^5.6 || ^5.7
- illuminate/support: ^5.4 || ^5.5 || ^5.6 || ^5.7
Requires (Dev)
- mockery/mockery: ^1.2
- orchestra/testbench: 3.5.*
- phpunit/phpunit: ^6.3|^7.0
README
Path History
Laravel package that allows you to create nested/human-readable paths(urls) for your eloquent models.
Installation
run composer require malyusha/path-history
inside project directory.
Why?
Sometimes you may want to create categorized news/articles/products pages and to see final URL to be like
http(s)://your-domain.com/{category}/{product}
. So, what are steps to do routes, resolving such patterns?
- Create routes for each entity. So for each change in base route parameters you will change it's dependencies.
- Create route for category(ies):
Route::get('{category_slug}')->name('products_category')->uses('ProductCategoryController@showCategory');
- Then create route for product:
Route::get('{category_slug}/{product_identifier}')->name('product')->uses('ProductsController@showProduct');
But what if you have nested categories for each entity? Ok, we can handle it, just add where
condition with pattern on routes:
->where('category_slug', '[\w\-/_]+')
. So, now your router will know that category_slug
can contain slashes, but
how can we determine where category_slug
ends and where product_identifier
starts? That's it, we need to prefix
our product/article route to tell resolver how it works.
Modifying last route:
Route::get('{category_slug}/p-{product_identifier}')->name('product')->uses('ProductsController@showProduct');
will give us what we wanted.
So what are the main problems of this method?
- Each entity must have it's own route;
- Can't redirect from old URL to new automatically. Imagine having 2 categories (
black
,white
) with 20 subcategories each. Each subcategory has ~20-40 products/articles. Now, you decided to change categoryblack
toblack-color
and you'll have to add redirects from (20 * ~20-40) URLs to new ones. - Prefix for entity with 2-nd level and upper. Not critical but still;
This package solves problem with that kind of routes and model's URLs
How it works?
- Package creates table where all URLs from all "slugable" entities will be stored;
- You add trait to all entities that are "slugable" and determine a little logic for relative entities;
- You define routes for your system and include package router registrar;
- Define controller and it's logic for each type of resolved from URL entity;
- Done!
Getting started
- Publish vendors running
vendor:publish --tag=migrations --tag=config
. This will copy configuration file and generate migration file with new table creation. - Run
php artisan migrate
to run migration for generated file; - For detailed explanation of configuration parameters check out configuration section;
Set up model URL generation
- Include
\Malyusha\PathHistory\HasPathHistory
trait in your model;
<?php namespace App\Entities; use App\DescendantRetrievers\Shop\NestedSetDescendants; use App\DescendantRetrievers\Shop\ProductsOfCategory; class ProductCategory extends \Illuminate\Database\Eloquent\Model { use \Malyusha\PathHistory\HasPathHistory; protected $updatePathOnChangeAttributes = ['parent_id']; protected $parentPathRelation = 'parent'; protected $descendantRetrievers = [ NestedSetDescendants::class, ProductsOfCategory::class, ]; public function parent() { return $this->belongsTo(static::class, 'parent_id'); } public function shouldUseParentPaths(): bool { return ! $this->isRoot(); } public function isRoot(): bool { return $this->parent_id === null; } public function products(): \Illuminate\Database\Eloquent\Relations\HasMany { return $this->hasMany(Product::class); } }
As you can see, we define category model that uses out trait. That trait will setup model event listeners to update all paths of current and nested categories and their products. You may have noticed that we've defined additional properties of our model, let's look at them more detailed:
- Place route inside your routes file:
<?php // web.php Route::get('/')->name('index')->uses('HomeController@showIndex'); // ... Other routes definitions ... // And here comes shop section Route::prefix('shop')->name('shop.')->group(function () { Route::get('/')->name('index')->uses('ShopController@index'); // Call dynamic router registration app('ph.router')->register(); });
- Add
shop
topaths
parameter inside configuration file. More detailed see here.
That's it! You've defined model, controller and route responsible for url generation and handling!
Retrieving URLs for models
As you've set up models to use path history you'll want to retrieve it's generated urls.
Every model, that uses PathHistoryTrait has getter for path
attribute. It'll check whether relation currentPath
was loaded to model and return it's link. So if you want to use full urls you'll need to preload this relation:
// IndexController.php
public function showIndex() { $products = App\Entities\Product::with('currentPath')->get(); return view('index_page', ['products' => $products]); }
// index_page.blade.php
@extends('layouts.default') @section('content') <h1>Products</h1> @foreach($products as $product) <img src="{{ $product->getImage() }}}"> <a href="{{ route('shop.resolve', ['path' => $product->path]) }}">{{ $product->name }}</a> @endforeach @endsection
When path relation for product is loaded it's link will contain full URL to product page.
Configuration
You can find configuration of package in path_history.php
config file after publishing vendors inside your project.
This file contains comments and description of each parameter, but here you can find more detailed info about each of them:
table
- table where all urls for all models that are using trait will be stored.
model
- instance of PathHistoryContract
. This is the main model for manage path-history package logic. Redefine
it to your model if you want to customize something.
redurect_status
- Redirect status for paths, that are not current anymore. Default is 302, but you can change it to
301, for example, when you are sure, that you'll never restore previous URL of entity. Note: it works only if you add
redirect middleware in Http\Kernel list.
paths
- array of paths. These are main setting, showing how your application should manage urls of configured types of models.
To simplify let's look at sample configuration of paths
parameter:
<?php return [ // other params here... 'paths' => [ [ 'prefix' => 'news', 'types' => [ \App\Entities\Content\NewsCategory::class => \App\Http\Controllers\News\CategoryController::class, \App\Entities\Content\News::class => \App\Http\Controllers\News\NewsController::class, ] ], [ 'prefix' => 'products', 'types' => [ \App\Entities\Shop\Product::class => \Admin\Http\Controllers\Shop\ProductsController::class, \App\Entities\Shop\ProductCategory::class => \Admin\Http\Controllers\Shop\CategoryController::class, ], ], ], ];
As you can see we defined configuration for 2 sections: news
and products
. Let's see each path
array config more
detailed:
prefix
- used to show package what is the root prefix used to handle concrete model type and call it's controller
when prefix for model matched;
types
- here you need to define array of types to handle as array of [model => controller]
. Also, if you have controller
to handle all types of entities for prefix you can define it as separated parameter in path array - [controller => your_controller]
and make types
parameter indexed array with models as values.
Automatic redirects
There are 2 options for automatic redirects from old urls from path history:
Redirects from old model's url
Add Malyusha\PathHistory\Middleware\RedirectsFromOld
middleware into your web
middleware group inside App\Http\Kernel
.
This will redirect users from old created urls for models to changed if they are exist.
So, imagine you have code structure, described here. You want your shop section to be
located visiting /shop
. You've created category with slug men
, so your category page will be
rendered on /shop/men
. Now, you've created nested category with slug boots
, so users will see men's boots page
on /shop/men/boots
. You created first product for these categories - "Awesome Black Boots" with code awesome-bb
or just
vendor code of boots, doesn't really matter. This product was shown on this page for 2 months and you've decided to change
code of category to, for example, high-shoes
, but robots (google bot, yahoo, etc.) have already indexed your page old
url. Usually you can have separated table for redirects from old-to-new URLs and add them manually, but this package can
handle it for you. So, when you change category's slug to something new all descendant's URLs of category (sub categories, products, etc.)
will now be redirected to new URLs;
Global redirects for all system urls
Install spatie/laravel-missing-page-redirector
by running composer require spatie/laravel-missing-page-redirector
.
This kind of redirects needed when you need to redirect from old pages that haven't ever existed in your project. For instance, when you're migration from old version of website to new, you need to save all indexed pages and redirect them to new ones. Documentation in progress...