RouteTree: Advanced Route Management for Laravel (>=v5.5)
webflorist/routetree is a Laravel package for routetree: advanced route management for laravel (>=v5.5).
It currently has 3 GitHub stars and 5.035 downloads on Packagist (latest version v2.3.4).
Install it with composer require webflorist/routetree.
Discover more Laravel packages by webflorist
or browse all Laravel packages to compare alternatives.
Last updated
This package includes a special API for creating and accessing Laravel-routes and route-related information. It's main concept is to create a hierarchical multi-language RouteTree using an expressive syntax (mostly mimicking Laravel's own). Using that hierarchy, RouteTree can be used to easily create:
Here is a complete feature overview:
en/company/team/contact).en.company.team.contact.get).Route::group()).Payload functionality to set any custom data for your routes (e.g. page title, meta description, includeInMenu) and retrieve it anywhere in your application.en/company/team/contact for english and de/firma/team/kontakt for german).Payload (page-titles and any other custom information) - also for parameter- or resource-routes using the corresponding Eloquent Models.trans_by_route() helper.)company.team.contact) to be used for various purposes anywhere in your app. Examples using the route_node() helper function:
route_node('company.team.contact')->getUrl().route_node('company.team.contact')->getUrl()->locale('de').route_node('company.team')->getChildNodes()route_node('company.team')->getTitle()Team).Payload):route_node('company.team')->payload->get('icon/description/keywords/author/layout/last_update/whatever').en.company.news.get).HTTP_ACCEPT_LANGUAGE header sent by the client./ to the language-specific home page (e.g. /en)./company/team/contact to en/company/team/contact).artisan command.auth routes and redirects.Payload from specific routes. (requires at least Laravel 5.6!)php composer require webflorist/routetreephp artisan vendor:publish --provider="Webflorist\RouteTree\RouteTreeServiceProvider"locales inside your routetree.php config file.'locales' => ['en','de'].null to enforce a single-language app (using config app.locale).)Note that this package is configured for automatic discovery for Laravel. Thus the package's Service Provider Webflorist\RouteTree\RouteTreeServiceProvider as well as the RouteTree alias will be automatically registered with Laravel.
There are several ways to access the RouteTree service:
route_tree()\RouteTree::app('Webflorist\RouteTree\RouteTree') or app()['Webflorist\RouteTree\RouteTree']The following code examples will use the facade RouteTree::.
Just like with Laravel's own routing, your can define the RouteTree in your routes/web.php.
For better comparability of syntaxes, the following examples will correspond to the ones presented in Laravel's Routing documentation where possible. They will also assume 2 configured languages ('en','de') - if not otherwise stated.
RouteTree::node('foo', function (RouteNode $node) {
$node->get(function() {
if (\App::getLocale() === 'de') {
return 'Hallo Welt';
}
return 'Hello World';
});
});
The node() method creates a RouteNode with name (and ID) foo and is then setup using the closure in it's second parameter.
A RouteNode itself is comparable to Laravel's Route Groups. It does not per se result in any registered routes, but centralizes and shares various data (e.g. path, middleware, namespace, etc.) with it's actions and inherits them to any child nodes.
The $node->get() call adds a RouteAction named get using the HTTP request method GET and a closure as it's callback.
A RouteAction results in one generated Laravel Route per configured language.
The above code will register the following routes:
de.foo.get and path de/fooen.foo.get and path en/fooAs with Laravel's syntax you can also state the action's callback using Controller@method:
RouteTree::node('foo')->get('Controller@method');
In the above example, the RouteNode's setup closure is skipped and instead the get call is directly chained to the node call. This is an alternative way to setup (which returns a RouteNode and thus allows chaining of various fluent methods), resulting in a more readable one-liner. Once a RouteNode becomes a little more complex and has several child-nodes, using a setup-closure is recommended instead. Also be wary, that action-creating methods (such as get, post, redirect, view, etc.) return the RouteAction object instead of the RouteNode.
Both syntax variants will be used in the following examples.
RouteNodes provide public methods to register RouteActions that respond to any HTTP verb:
RouteTree::node('foo', function (RouteNode $node) {
$node->get($callback);
$node->post($callback);
$node->put($callback);
$node->patch($callback);
$node->delete($callback);
$node->options($callback);
});
You can also define redirecting nodes and state the target nodes by their ID:
RouteTree::node('here')->redirect('there');
RouteTree::node('there', function (RouteNode $node) {
$node->get(function() {
return 'You are now there';
});
});
By default, $node->redirect() returns a 302 status code.
You can customize the status code using the optional second parameter:
$node->redirect('there', 301);.
You can also use $node->permanentRedirect() to return a 301 status code.
If a RouteNode should only return a view, you can use the view method:
RouteTree::node('welcome')->view('welcome');
You can pass data to the view via the second parameter of the view method.
The RouteTree::node() method used in the above examples automatically creates nodes with the root node as parent. You can configure the root node itself using the root method instead:
RouteTree::root()->view('welcome');
You cannot state a name for the root node. It's name and ID will always be an empty string ('').
There are several ways to create a node as a child of another node:
$node->child($childName, $childCallback) within the parent's setup-callback.node method: RouteTree::node($childName, $childCallback, $parentId);RouteTree::getRoute('parent')->child('child', $childCallback)The first variant will be used in any further examples. It builds the RouteTree using nested closures, which has the benefit of representing the hierarchical RouteTree within the defining code indentation.
Child nodes will automatically receive a unique node ID representing the hierarchy of it's ancestors. For example a child with name bar of a parent called foo will have the ID foo.bar.
The same happens with path segments, resulting in for example en/foo/bar.
You can disable inheriting the segment to it's descendants by calling $node->inheritSegment(false). This is useful, if you want to use a RouteNode simply for grouping purposes without a representation in the URL-path.
As with Laravel's Route groups, middleware and (controller-)namespaces will be also inherited by default.
By default a RouteNode's name will also be it's path segment. But you can also state a different segment for a node by calling the RouteNode's segment method:
RouteTree::node('company', function (RouteNode $node) {
$node->segment('our-great-company');
$node->get($callback);
});
The above code will register the following routes:
de.company.get and path de/our-great-companyen.company.get and path en/our-great-companyYou can define localized path segments by handing a LanguageMapping object including segments for all languages:
$node->segment(
Webflorist\RouteTree\LanguageMapping::create()
->set('en', 'our-great-company')
->set('de', 'unsere-tolle-firma')
);
This will register the following routes:
de.company.get and path de/our-great-companyen.company.get and path en/unsere-tolle-firmaYou can also handle segment-translations via your language-files using the Automatic Translation functionality.
You can assign middleware to RouteNodes using:
$node->middleware('auth');
This will attach the auth middleware to all of the RouteNode's actions and inherit it to all descendant nodes.
Middleware-parameters can be stated in the second parameter of the middleware method.
Inheritance of middleware can be disabled by handing boolean false as the third parameter of the middleware method.
In case you want a descendant node to NOT use an inherited middleware, simply state the following in the descendant's callback:
$node->skipMiddleware('auth');
There might also be situations, where you want a specific action of a RouteNode to have additional middleware or skip a middleware defined on the RouteNode. You can achieve this by chaining the middleware call to the action-call. Here is an example:
RouteTree::node('user', function (RouteNode $node) {
$node->middleware('auth');
$node->get($callback)->skipMiddleware('auth');
$node->post($callback);
$node->delete($callback)->middlware('admin');
});
This will register the following routes:
GET Routes with no middleware.POST Routes with the auth middleware.DELETE Routes with both the auth and admin middleware.By default all Controller@method callback definitions will use App\Http\Controllers as the namespace.
Using a RouteNode's namespace method will append a segment to that namespace and inherit it to it's descendants. Inheritance can be overruled by simply prefixing the namespace with a backslash.
RouteTree::node('account', function (RouteNode $node) {
$node->namespace('Account');
$node->child('address' function (RouteNode $node) {
$node->get('AddressController@get');
// will point to `App\Http\Controllers\Account\AddressController`
})
$node->child('password' function (RouteNode $node) {
$node->get('\My\Other\Namespace\PasswordController@get');
// will point to `My\Other\Namespace\PasswordController`
})
});
The following code will result in the creation of the routes en/user/{id} and de/user/{id}.
RouteTree::node('user', function (RouteNode $node) {
$node->child('id', function (RouteNode $node) {
$node->parameter('id');
$node->get('id', function ($id) {
return 'User '.$id;
});
});
});
You can also set regular expression constraints for parameters:
$node->parameter('id')->regex('[0-9]+');
When using parameter or resource nodes, you might also want to be able to translate a route key (e.g. to realize a language-switching menu for a blog-article, which has different slugs for different languages.)
There are two ways to achieve this:
translation through the array-keys (0, 1, 'whatever')):$node->parameter('blog_category')->routeKeys(LanguageMapping::create()
->set('en', [
0 => 'search-engine-optimization',
1 => 'web-development'
])
->set('de', [
0 => 'suchmaschinen-optimierung',
1 => 'web-entwicklung'
])
);
Eloquent model. There are two requirements for this:Eloquent model must be stated using the model method of a RouteParameter or RouteResource:$node->resource('blog_category', 'BlogCategoryController')->model('App\BlogCategory'); or$node->parameter('blog_category')->model('App\BlogCategory');Webflorist\RouteTree\Interfaces\TranslatesRouteKey and subsequently the translateRouteKey method. Here is an example implementation:public static function translateRouteKey(string $value, string $toLocale, string $fromLocale): string
{
return BlogCategory::bySlug($value, $fromLocale)->slugs->where('locale', $toLocale)->first()->slug ?? $value;
}
Akin to Laravel's Route::resource() method, RouteTree can also register resourceful routes:
RouteTree::node('photos')->resource('photo', 'PhotoController');
This will generate a full set of resourceful routes in all languages:
HTTP-Verb | Route Name | URI | Action / Controller Method
------------|--------------------|---------------------------|--------------
GET | en.photos.index | /en/photos | index
GET | en.photos.create | /en/photos/create | create
POST | en.photos.store | /en/photos | store
GET | en.photos.show | /en/photos/{photo} | show
GET | en.photos.edit | /en/photos/{photo}/edit | edit
PUT/PATCH | en.photos.update | /en/photos/{photo} | update
DELETE | en.photos.destroy | /en/photos/{photo} | destroy
GET | de.photos.index | /de/photos | index
GET | de.photos.create | /de/photos/create | create
POST | de.photos.store | /de/photos | store
GET | de.photos.show | /de/photos/{photo} | show
GET | de.photos.edit | /de/photos/{photo}/edit | edit
PUT/PATCH | de.photos.update | /de/photos/{photo} | update
DELETE | de.photos.destroy | /de/photos/{photo} | destroy
Partial resource routes are also supported using the only or except methods:
$node->resource('photo', 'PhotoController')->only(['index', 'show']);
$node->resource('photo', 'PhotoController')->except(['create', 'store', 'update', 'destroy']);
Resource nodes can also have child-nodes. In this case call the child method on $node->resource instead of $node:
RouteTree::node('photos', function (RouteNode $node) {
$node->resource('photo', 'PhotoController')
$node->resource->child('featured', function (RouteNode $node) {
$node->get('PhotoController@featured');
});
});
The above code will additionally generate the following routes:
en.photos.featured.get with the URI en/photos/{photo}/featured.de.photos.featured.get with the URI de/photos/{photo}/featured.Now that we have defined the RouteTree, it's RouteNodes can be accessed anywhere in your application using the route_node() helper:
route_node()RouteTree::getCurrentNode() and will return the currently active RouteNode.route_node('company.team.contact')RouteTree::getNode('company.team.contact') and will return the RouteNode with ID company.team.contactIf RouteTree fails to find the current/specified node, it will throw a NodeNotFoundException, except a fallback node is set in the config routetree.fallback_node. The default config sets the fallback node to the root node, since you will probably want to inhibit NodeNotFoundExceptions in a production environment.
One of RouteTree's central use cases is to create language-agnostic links. Both RouteNodes and RouteActions have a getUrl() method, that returns a RouteUrlBuilder object, which will generate the corresponding URL when cast to a string.
(string) route_node('company.team.contact')->getUrl() will return the URL of the RouteNode's action. If a node has multiple actions, it will return the link to it's first get action (or index actions with resources).
The returned RouteUrlBuilder object has several fluent setters to modify the generated link:
->locale ( ?string $locale=null ) : RouteUrlBuilder
(string) route_node('company')->getUrl()->locale('en') will return the URL in english language (e.g. en/company). (defaults to current locale)
->absolute ( ?bool $absolute=null ) : RouteUrlBuilder
(string) route_node('company')->getUrl()->absolute(false) will return a relative path instead of an absolute URL inkl. the domain (default can be configured in routetree.absolute_urls)
->action ( string $locale ) : RouteUrlBuilder
(string) route_node('photos')->action('create') will return the URL of the resource action create (effectively appending '/create' to the URL by default in en locale). See the table at Resourceful RouteNodes for details. By default the index or the first GET action will be used. Note that with the actions show, edit, update and destroy you will also have to state the route keys to set for the url parameter(s) (see just below).
->parameters ( array $parameters ) : RouteUrlBuilder
(string) route_node('photos')->action('edit')->parameters(['photo' => 'my-slug']) would result in the URL /en/photos/my-slug/edit using the locale en. Any URL to a Route containing one or more parameters will need values to fill in for those parameters, and thus a key within the handed array. E.g. photo/{photo_id}/comments/{comment_id} would require ['photo_id' => $photoId, 'comment_id' => $commentId] to be passed. Any missing route keys (aka parameter-values aka slugs) are taken from the currently active Laravel Request - if possible.
You can define an access any information you want to a RouteNode using it's associated RoutePayload object, which is publicly accessible via a node's payload property.
You can set a payload item directly in the RoutePayload object using the following syntax-options:
RoutePayload's set method:$node->payload->set('title', 'My photos');$node->payload->title('My photos');$node->payload->title = 'My photos';The value of a payload can be any data type as well as a Closure. The Closure will receive two parameters:
route parameter => route key pairs, to retrieve the payload for (this way you can have payload depend on the current route parameters).As with path segments, any payload-item can also be multilingual using a LanguageMapping object:
$node->payload->title = LanguageMapping::create()
->set('en', 'My photos')
->set('de', 'Meine Photos')
);
You can also handle payload translations via your language-files using the Automatic Translation functionality.
If you want to have different values of a payload depending on an action, you can override a RouteNode's payload using a RouteAction's payload. Here is an example:
$node->getAction('edit')->payload->set('title', 'Edit photo');
For parameter/resource nodes there is also the possibility to fetch payload from an Eloquent model. There are two requirements for this:
Eloquent model must be stated using the model method of a RouteParameter or RouteResource:$node->resource('photos', 'PhotoController')->model('App\Photo'); or$node->parameter('photo')->model('App\Photo');Webflorist\RouteTree\Interfaces\ProvidesRoutePayload and subsequently the getRoutePayload method. Here is an example implementation:public static function getRoutePayload(string $payloadKey, array $parameters, string $locale, ?string $action)
{
if ($payloadKey === 'title' && $action === 'show')
{
return self::find($parameters['photo'])->title;
}
}
Payload can be retrieved anywhere in your app. using the get method of a RoutePayload. Example using the current RouteNode and RouteAction:
route_node()->payload->get('title');
This will look for the title payload using the following order:
There are multiple use-cases, where this payload functionality can be useful This is very useful to e.g.:
title and navTitle payloadsPage titles (for meta tags, canonical tags, title attributes of links, navigation menus, breadcrumbs, h1-tags, etc.) are probably one of the most used applications for payloads. Also quite often you might want to have a special (shorter) title for pages in navigation menus. To simplify handing of this, RouteNodes and RouteActions have special getTitle() and getNavTitle() methods, that add some additional fallback magic:
navTitle falls back to title.Photos)'Create Resource' for create actions).To utilize this magic, always use e.g. route_node()->getTitle() instead of route_node()->payload->get('title') and route_node()->getNavTitle() instead of route_node()->payload->get('navTitle').
RouteTree also includes some magic regarding automatic translation. The basic concept is to map the hierarchy of the RouteTree to a folder-structure within the localization-folder.
The config-key localization.base_folder sets the base-folder for the localization-files- and folders utilized by RouteTree. The default value is pages, which translates to the folder \resources\lang\%locale%\pages.
There are 2 seperate auto-translation-functionalities:
This provides an easy and intuitive way of configuring multi-language variants of the path-segment, page-title, or any other custom information for routes through Laravel's localization-files.
Each RouteNode is represented as a folder, and within the folder for a node resides a file, that contains all auto-translation-information. How this file is named is configured under the config-key localization.file_name. The default value is pages, which means information on all 1st-level-pages should be placed in this file: \resources\lang\%locale%\pages\pages.php
Example: Let's assume, you have defined the following RouteNodes (any actions or other options are missing for simplicity's sake):
RouteTree::node('company', function (RouteNode $node) {
$node->child('history', ...);
$node->child('team', function (RouteNode $node) {
$node->child('office', ...);
$node->child('service', ...);
});
});
RouteTree::node('contact', ...);
Please note, that no path-segment, page-titles or custom information is defined on any node. We will use auto-translation for this.
To use auto-translation, the following file- and folder-structure should be present within the defined base-folder for each locale (per default \resources\lang\%locale%\pages):
.
├── pages.php
├── company
├── pages.php
└── team
└── pages.php
Each pages.php-file includes auto-translation information for the child-nodes of the node, which corresponds to the folder it resides in. Let's see a german-language example of the contents of these files:
./pages.php:
<?php
return [
'segment' => [
'company' => 'firma',
'contact' => 'kontakt',
],
'title' => [
'company' => 'Über unsere Firma',
'contact' => 'Kontaktieren Sie uns!',
'' => 'Startseite',
],
'abstract' => [
'company' => 'Hier finden Sie allgemeine Informationen über unsere Firma.',
'contact' => 'Hier finden Sie Möglichkeiten, mit uns in Kontakt zu treten.',
]
];
Note that there is an additional entry in the title-array with an empty string as the key and "Home" as the value. This is title of the root-node (as the root-node's ID and name is always an empty string '').
./company/pages.php:
<?php
return [
'segment' => [
'history' => 'geschichte',
'team' => 'mitarbeiter',
],
'title' => [
'history' => 'Die Firmengeschichte',
'team' => 'Unsere Mitarbeiter',
],
'description' => [
'history' => 'Hier finden Sie die Entstehungsgeschichte unserer Firma.',
'team' => 'Hier sind unsere Mitarbeiter zu finden.',
]
];
./company/team/pages.php:
<?php
return [
'segment' => [
'office' => 'buero',
'service' => 'kundendienst',
],
'title' => [
'office' => 'Büro',
'service' => 'Kundendienst',
],
'description' => [
'office' => 'Hier finden Sie unsere Büro-Mitarbeiter.',
'service' => 'Hier finden Sie unsere Service-Mitarbeiter.',
]
];
With this setup, the segments defined in the language-files will automatically be used for the route-paths of their corresponding nodes.
Also the title will be fetched with each getTitle-call submitted for a specific node (e.g. route_node('company.team.service')->getTitle() would return Büro, if the current locale is de.
The same thing is possible with the description (or any other payload). (e.g. route_node('company.team.service')->payload->get('description') would return Hier finden Sie unsere Service-Mitarbeiter., if the current locale is de.
You can also set action-specific titles or navTitles via auto-translation by appending an underscore and the action to the node-name. This is very useful for resource nodes. Here is an example:
<?php
return [
'title' => [
'users' => 'Users',
'users_create' => 'Create new user',
'users_show' => 'User :userName',
'users_edit' => 'Edit user :userName',
],
];
With most websites you will want to translate page-content in your views. RouteTree includes a handy helper-function called trans_by_route(), that will use the same folder-structure but with the language-file named as the last RouteNode.
Using the example above the location of this file for the office page would be : ./company/team/office.php
If you are using Laravels route caching, RouteTree must cache it's own data too. So instead of 'artisan route:cache' use RouteTree's caching-command, which will also take care of caching Laravel's routes:
php artisan routetree:route-cache
Having an up-to-date sitemap.xml file is an important criterion for a modern search engine optimized website. RouteTree includes functionality to create such a file.
The following an artisan command will create a static XML sitemap file:
php artisan routetree:generate-sitemap
Per default the output-file will be at 'public/sitemap.xml'. You can however configure this in RouteTree's config file.
You can also enable a route to provide the sitemap dynamically (see config-options under routetree.sitemap.route).
Any URL's in the sitemap will use config('app.url') as the base url automatically. But you can also state a different value unfer the routetree.sitemap.base_url config.
Per default all routes created with RouteTree will be present in the sitemap. There are some exclusion criteria though:
GET routes will be included.routetree.sitemap.excluded_middleware will be automatically excluded (defaults to ['auth']).parameters can only be included, if RouteTree can retrieve all possible values for these parameters. There are two ways to achieve this:
routeKeys() method (see Route Parameters)Eloquent model implementing the interface Webflorist\RouteTree\Interfaces\ProvidesRouteKeyList and thus the method getRouteKeyList(). A (single-language) default implementation is included in the trait Webflorist\RouteTree\Interfaces\Traits\ProvidesRouteKeyListDefault: public static function getRouteKeyList(string $locale = null, ?array $parameters = null): array
{
return self::pluck(
(new self())->getRouteKeyName()
)->toArray();
}
Furthermore you can also explicitly exclude a RouteNode (and all it's children) from the sitemap:
$node->sitemap->exclude();
A sitemap.xml also allows the definition of additional information for search engines (see https://www.sitemaps.org/protocol.html#xmlTagDefinitions). You can state this data for a node using the following code:
$node->sitemap
->lastmod(Carbon::parse('2019-11-16T17:46:30.45+01:00'))
->changefreq('monthly')
->priority(1.0);
Furthermore you can also use payload translation (either via an Eloquent model or via language files) to automatically retrieve these values.
RouteTree also includes an API, that allows fetching information about routes registered with RouteTree. The API must be enabled via config routetree.api.enabled and has the default base-url api/routetree/ (also configurable).
At the moment there are 2 endpoints:
GET api/routetree/routes:GET api/routetree/routes/{route_name}:RouteTree dispatches events in various cases:
\Webflorist\RouteTree\Events\LocaleChanged
Is dispatched, when the locale saved in session by the RouteTreeMiddleware is changed. The old locale is available via the $oldLocale property of the event, and the new locale via $newLocale.
\Webflorist\RouteTree\Events\NodeNotFound
Is dispatched, when route_node() is called and the current or specified node could not be found. The specified RouteNode ID is available via the $nodeId property of the event, and is null in case no current node was found.
\Webflorist\RouteTree\Events\Redirected
Is dispatched, when the RouteTreeMiddleware performs an automatic redirect. The destination URI is available via the $toUri property of the event, and the source URI via $fromUri.
For the already mentioned and explained methods root(), node() please see the corresponding sections above.
Here are some other useful methods of the RouteTree-class:
route_node() as shortcut)RouteTree::doesNodeExist('company.team.office'))RouteTree::getNode('company.team.office'); use route_node('company.team.office') as shortcut)For the already mentioned and explained methods getUrl, getTitle and getNavTitle please see the corresponding sections above.
Here are some other useful methods of the RouteNode-class:
route_node('company.team.office')->getParentNode() would retrieve the node with the ID company.team.)route_node()->getParentNodes() would retrieve an array of all ancestral-nodes of the currently active node up to to the root-node. This is very useful for site-maps or breadcrumbs.)Several helper-functions are included with this package:
route_tree: Gets the RouteTree singleton from Laravel's service-container. It can be used anywhere in your application (controllers, views, etc.) to access the RouteTree service.
route_node:
route_node('company.team.contact') will return the RouteNode with ID 'company.team.contact'.route_node_url: Shortcut for route_node()->getUrl().
trans_by_route: Translates page-content using the current node's content-language-file (see section Auto-translation of regular page-content above).