This package allows to create dynamic routes right from database, just like WordPress and other CMS do.
This package is currently being developed and tested. Would love feedback ❤️.
- Resources for visual SEO management (in progress)
- Installation
- Getting started
- Usage
- Route names
- Getting the route for a resource
- Routes and route groups
- Nesting routes
- Creating/updating permalinks manually
- Overriding the default action
- Support for morphMap & actionMap
- Automatic SEO generation
composer require devio/permalink
php artisan migrate
This package handles dynamic routing directly from our database. It also supports slug inheritance so we can easily create routes like this jobs/frontend-web-developer
.
Most of the solutions out there are totally bound to models with polymorphic relationships, however that's not flexible at all when dealing with routes without models. This package supports both, model bound routes and plain routes.
Basically, the package stores routes in a permalinks
table which contains information about every route:
- Slug
- Parent slug
- Model (if any)
- Action
- SEO
Let's review a very basic example to understand how it works:
id | slug | parent_id | parent_for | permalinkable_type | permalinkable_id | action |
---|---|---|---|---|---|---|
1 | users | NULL | App\User | NULL | NULL | UserController@index |
2 | israel-ortuno | 1 | NULL | App\User | 1 | NULL |
It will run the following (this example tries to be as explicit as possible, internally it uses eager loading and some other performance optimizations):
$router->get('users', 'UserController@index');
$router->group(['prefix' => 'users'], function() {
$user->get('israel-ortuno', User::find(1)->permalinkAction())
});
// Which will produce:
// /users UserController@index
// /users/israelOrtuno Whatever action configured into the permalinkAction method
Call the permalink route loader from the boot
method from any of your application service providers: AppServiceProvider
or RouteServiceProviderp
are good examples:
class AppServiceProvider extends ServiceProvider {
public function boot()
{
$this->app->make('permalink')->load();
}
}
The configuration in the models is pretty simple, simply use the HasPermalinks
trait and implement the Permalinkable
interface in the models you want to provide the permalink functionality and implement the following methods:
class User extends Model implements \Devio\Permalink\Contracts\Permalinkable {
use \Devio\Permalink\HasPermalinks;
/**
* Get the model action.
*
* @return string
*/
public function permalinkAction()
{
return UserController::class . '@show';
}
/**
* Get the options for the sluggable package.
*
* @return array
*/
public function slugSource(): array
{
return ['source' => 'permalinkable.name'];
}
}
This model is now fully ready to work with.
The package uses cviebrock/eloquent-sluggable for the automatic slug generation, so the slugSource
method should return an array of options compatible with the eloquent-sluggable
options. By just providing the source
key should be enough for most cases, but in case you want to update other options, here you can do so. Basically we are pointing that the slug will be generated from the name
field of the permalinkable
relationship of the permalink model, which will be the current model.
The permalinkAction
method should return the default action hanlder for this model, just like if we were setting a route here: Controller@action
. You could even return a Closure
.
NOTE: Be aware that Laravel cannot cache Closure
based routes.
We are now ready to create a new User
and the permalink will be automatically generated for us.
$user = User::create(['name' => 'Israel Ortuño']);
$route = $user->route; // 'http://localhost/israel-ortuno
// Permalink (
// slug: israel-ortuno
// parent_id: NULL
// permalinkable_type: App\User
// permalinkable_id: 1
// action: NULL
// )
Whenever we visit /israel-ortuno
we will be executing UserController@show
action. This action will receive the binded model as parameter:
<?php
namespace App\Http\Controllers;
use App\Product;
use Illuminate\Http\Request;
class UserController
{
public function show(Request $request, User $user)
{
return $user;
}
}
When routes are loaded, they will be named based on their related model or action.
If the route is linked to a model, the route name will be generated by appending the permalink id
to the string provided by the permalinkRouteName()
method included in the HasPermalinks
trait. By default this method returns permalink
so all routes will be named as peramlink.{id}
. Feel free to override this method and provide any other name you like.
Static permalinks which are not linked to models will receive their names by extracting the controller name and the action from their fully qualified action names: UserController@index
will be transformed into user.index
.
However, if the permalink action is an "alias" (see Support for morphMap & actionMap), the action name itself will be used to name the route.
Routes can be resolved as any other Laravel route but just taking into account that they will cannot be resolved by route parameters, they have to be appended nevertheless.
route('permalink.1'); // Get the route of the permalink with id = 1
route('user.5'); // Get the route of the permalink with id = 1
NOTE: The id
of the resource is appended to the route name route.{id}
(note the .
), be careful to NOT pass the key as parameter route('permalink', $id)
.
When we are manipulating our model, we do not really want to care about the key of its permalink. Despite we could resolve routes just like in the previous example, HasPermalinks
trait includes a route
accessor which will resolve the fully qualified route for the current entity permalink:
$route = $user->route;
// this is just an alias for:
route('permalink.' . $user->permalink->id);
Cool and neat!
If a route is not binded to a model and its action is also NULL
, it will be threated as a route group but won't be registered:
id | slug | parent_id | parent_for | permalinkable_type | permalinkable_id | action |
---|---|---|---|---|---|---|
1 | users | NULL | App\User | NULL | NULL | NULL |
2 | israel-ortuno | 1 | NULL | App\User | 1 | NULL |
The example above will not generate a /users
route, users
will only act as parent of other routes but won't be registered.
At this point, you may be wondering why do we need a parent_for
column if there's already a parent_id
being used as foreign key for parent child nesting.
parent_for
is used in order to automatically discover which route will be the parent of a new stored permalink. Using the table above this section, whenever we store a permalink for a App\User
model, it will be automatically linked to the permalink 1
.
The parent_for
will be NULL
in most cases.
By default, this package comes with an observer class which is linked to the saved
event of your model. Whenever a model is saved, this package will create/update accordingly.
NOTE: By default, slugs are only set when creating. They won't be modified when updating unless you explicitly configured the slugSource
options to do so.
To disable the automatic permalink management you can simply set the value of the property managePermalinks
of your model to false
:
class User ... {
public $managePermalinks = false;
}
This will disable any permalink creation/update and you will be then responisble of doing this manually. As the Permalink
model is actually a polymorphic relationship, you can just create or update the permalink as any other relationship:
$user = User::create(['name' => 'Israel Ortuño']);
$user->permalink()->create(); // This is enough unless you want to manually specify an slug or other options
// or if the permalink exists...
$user->permalink->update([...]);
The values for the newly created or updated permalink will be extracted from:
- A
permalink
key passed into the creation array. - A
permalink
key from the current request.
When a permalink is binded to a model, we will guess which action it points to the permalinkAction
method defined in our Permalinkable
model. However, we can override this action for a certain model by just specifiying a value into the action
column of the permalink record:
id | slug | permalinkable_type | permalinkable_id | action |
---|---|---|---|---|
1 | madrid | App\City | 1 | App\Http\Controllers\CityController@show |
You could update your model via code as any other normal relationship, for example:
$city = City::find(1);
$city->permalink->update(['action' => 'App\Http\Controllers\CityController@show']);
NOTE: The action namespace should always be a fully qualified name unless you are using the actionMap
explained below.
This package provides support for morphMap
. As you may know, Laravel ships with a morphMap
method to where you can define a relationship "morph map" to instruct Eloquent to use a custom name for each model instead of the class name.
In adition to the morphMap
method, this package includes an actionMap
static method under the Permalink
model where you can also define a relationship "action map" just like the "morph map" but for the permalink actions:
use Illuminate\Database\Eloquent\Relations\Relation;
Relation::morphMap([
'users' => 'App\User',
'posts' => 'App\Post',
]);
use Devio\Permalink\Permalink;
Permalink::actionMap([
'user.index' => 'App\Http\Controllers\UserController@index',
'user.show' => 'App\Http\Controllers\UserController@show',
]);
You can register these maps in the boot method of your AppServiceProvider
. The example above will make the permalinkable_type
and action
columns look much more readable than showing fully qualified class names.
id | ... | permalinkable_type | ... | action |
---|---|---|---|---|
1 | user | user.show |
For SEO tags generation ARCANDEV/SEO-Helper is being used. This package offers a powerful set of tools to manage your SEO meta tags.
Permalink package provides content for ARCANDEV/SEO-Helper form a specific seo
column in the permalinks table. This column is supposed to store all the SEO related data for a given permalink in a JSON format:
{
"meta": {
"title": "Specific title", // The <title>
"description": "The meta description", // The page meta description
"robots": "noindex,nofollow" // Robots control
},
"opengraph":{
"title": "Specific OG title", // The og:title tag
"description": "The og description", // The og:description tag
"image": "path/to/og-image.jpg" // The og:image tag
},
"twitter":{
"title": "Specific Twitter title", // The twitter:title tag
"description": "The twitter description", // The twitter:description tag
"image": "path/to/og-image.jpg" // The twitter:image tag
}
}
In order to have all this content rendered in your HTML you should add the following you your <meta>
:
<head>
{!! seo_helper()->render() !!}
</head>
<head>
{{ seo_helper()->renderHtml() }}
</head>
Plase visit SEO-Helper – Laravel Usage to know more about what and how to render.
Under the hood, this JSON structure is calling to the different SEO helpers (meta, opengraph and twitter). Let's understand:
{
"title": "Generic title",
"image": "path/to/image.jpg",
"description": "Generic description",
"meta": {
"title": "Default title",
},
"opengraph": {
"image": "path/to/og-image.jpg"
}
}
This structure will allow you to set a base value for the title
in all the builders plus changing exclusively the title for the Meta builder. Same with the image, Twitter and OpenGraph will inherit the parent image but OpenGraph will replace its for the one on its builder.
This will call setTitle from the SeoMeta
helper and setImage from the SeoOpenGraph
helper. Same would happen with Twitter. Take some time to review these three contracts in order to know all the methods available:
In order to match any of the helper methods, every JSON option will be transformed to studly_case
prefixed by set
and add
, so title
will be converted to setTitle
and google_analytics
to setGoogleAnalytics
. How cool is that?
All methods are called via call_user_func_array
, so if an option contains an array, every key will be pased as parameter to the helper method. See setTitle
or addWebmaster
which allows multiple parameters.
To provide even more flexibility, the method calls are piped through 3 classes (one for each helper) called Builders. These builders are responsible for calling the right method from the ARCANDEV/SEO-Helper package.
If there is a method in this builders matching any of the JSON options, the package will execute that method instead of the default behaviour, which would be calling the method (if exists) from the SEO-Helper package.
Review the MetaBuilder as example. This builder contains a setCanonical
method which is basically used as an alias for setUrl
(just to be more explicit).
In order to modify the behaviour of any of these builders, you can create your own Builder which should extend the Devio\Permalink\Contracts\SeoBuilder
interface or inherit the Devio\Permalink\Builders\Builder
class.
Once you have created your own Builder, just replace the default one in the Container. Add the following to the register
method of any Service Provider in your application:
// Singleton or not, whatever you require
$this->app->singleton("permalink.meta", function ($app) use ($builder) { // meta, opengraph, twitter or base
return new MyCustomBuilder;
// Or if you are inheriting the default builder class
return (new MyCustomBuilder($app->make(SeoHelper::class)));
});
If you wish to prevent the rendering of any of the three Builders (meta, OpenGraph or Twitter), just set its JSON option to false:
{
"meta": { },
"opengraph": false,
"twitter": false
}
This will disable the execution of the OpenGraph and Twitter builders.