Alright, I know the title might be a bit strange. But I don't know how else to put it. Basically it comes down to something simple, but sometimes being a single developer makes you question things and make you want to discuss it with fellow developers.
A quick intro to the problem: I'm about to re-do my CMS based on a popular PHP Framework (Fyi: Laravel is my choice). The CMS will be modular (Blog, Users, Pages, Forms, etc.), but I also want the website navigation to be manageable by the website owners. This is where I started thinking about the best practice to do so.
So far, what I was thinking about is to create an interface "MenuBuilder" that defines the methods that need to be known to generate menu links for a given module. So, let's say my BlogModule has a class BlogMenuBuilder.
The MenuBuilder classes would have a method to build a link based on none or some parameters (like a record ID), a method to generate all links, a method to generate an array of options for a dropdown and so on.
Then the MenuModule would save the classname and optional paramaters to a table, together with the menu location (top, footer, sidebar, etc) and I could call MenuModule::build('top')
, this function would find all links, sorted ofcourse, and build the links by calling BlogMenuBuilder::link('type', $optional_parameters);
. Type would in this case be a named route from the Laravel framework. Eventually ,the MenuModule::build() method would cache instead of having to build it again every time.
Am I anywhere near a best practice? I feel like I'm heading in the right direction but since I have no other developers to think this thing thru with I was hoping to get some useable feedback from SO.
Thanks for your time!
I think you're pretty much there as far a structure. Based on what I understand from your question (and being that you're using Laravel) I'd approach it like this:
Name: menus
Columns: id | builder | created_at | updated_at | deleted_at
---------------------------------------------------------------------
eg. 1 | NULL | 1985-10-26 01:20:00 | 1985-10-26 01:21:00 | NULL
2 | blog | 1985-10-26 01:20:00 | 1985-10-26 01:21:00 | NULL
Name: links
Columns: id | menu_id | type | resource_type | resource_id | url | created_at | updated_at | deleted_at
-------------------------------------------------------------------------------------------------------------------------------
eg. 1 | 1 | url | NULL | NULL | http://example.com/blog/1 | 1985-10-26 01:20:00 | 1985-10-26 01:21:00 | NULL
1 | 2 | route | blog | 1 | NULL | 1985-10-26 01:20:00 | 1985-10-26 01:21:00 | NULL
Name: meta (or options)
Columns: id | resource_type | resource_id | name | value
---------------------------------------------------
eg. 1 | menu | 1 | bg_color | 000000
2 | link | 1 | target | _blank
With this table setup you can store menus, links (either a route defined in the relevant builder or a url) and meta/option data for either menus or links.
Note: If you wanted to re-use links in multiple menus you could remove the menu_id
from the links
table add a menu_links
table for a ManyToMany relationship between menus and links
Simple enough, you'd want a model for each table ie. Menu
, Link
and Meta
Within the app
directory of your project I'd place a Builders
folder and potentially a MenuBuilders
folder inside that. Using that structure you'd namespace each Builder
class under App\Builders\MenuBuilders
.
With each Builder
class you could then either extend a main MenuBuilder
class or, depending how much you'll change for each Builder
, you could implement
an interface
. Here's a quick example using an interface
(if you're not familier with interfaces I'd suggest a look at Laracasts PHP Bootcamp):
MenuBuilderInterface.php
namespace App\Builders\MenuBuilders;
interface MenuBuilderInterface{
/*
* Build function
*
* @param object $menu
*/
public build($menu);
}
BlogMenuBuilder.php
namespace App\Builders\MenuBuilders;
class BlogMenuBuilder implements MenuBuilderInterface {
/*
* Build function
*
* @param object $menu
*/
public build($menu)
{
// Here you can now build your menu using
// the menu instance passed through
}
}
If you didn't want to have to create all the methods contracted in the interface
then maybe extending a main MenuBuilder
class works better for you, it really depends on how different you see the menus being and whether you can re-use a lot of methods in a main MenuBuilder
.
So how would you use the above setup? Like you stated in your question, each menu
defines it's builder in the database so we could create a simple build function in our Menu
model to call the correct Builder
class:
Menu.php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Menu extends Model
{
/**
* Get the user's first name.
*
* @param string $value
* @return string
*/
public function build()
{
// First we define our namespace and get the builder based on the menu data
$namespace = 'App\\Builders\\MenuBuilders\\';
$builder = !empty($this->builder)? $namespace.ucfirst($this->builder).'MenuBuilder' : $namespace.'MenuBuilder';
// Then check the builder class defined definitely exists
// If no, replace with main menu builder class
$builder = class_exists($builder)? $builder : $namespace.'MenuBuilder';
// Lastly we call the build method on a new instance of our builder
// passing in our menu instance
return new $builder()->build($this);
}
}
Now all we need do in our view is call the build function on our menu instance:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Example Site</title>
</head>
<body>
<div id="menu">
<?php
// Get our blog menu
// - you would obviously more likely do this in your controller
$menu = Menu::find(2);
?>
{!! $menu->build() !!}
</div>
</body>
</html>
There are always a few different ways you can structure this kind of thing. Inevitably you will figure out what's best for you based on the specific requirements of each outcome you're after, but I hope that helps!
Well as everyone said, do what other big cms'es have done.
So on the UI: - have a select box of existing menus - have a button to create a new menu
this part could be complex as how your site is: - contain static pages: / , /about , /contact - contain dynamic pages (just like wordpress) - contain dynamic posts (just like wordpress) - contain dynamic categories, tags whatever (just like wordpress) - free input box
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With