I'm writing a class - called MenuBuilder
- in PHP 7 which is intended to build navigation menus in a web application.
The way in which the class works is by building up an output buffer which holds the markup for the menus.
It starts off by declaring an empty variable inside class MenuBuilder
:
private $output_buffer = '';
There is a method to add to it:
protected function add_to_output_buffer($input = '') {
if ($input !== '') {
$this->output_buffer .= $input;
}
}
And a method to display the menu markup:
public function render() {
echo $this->output_buffer;
}
So far quite straightforward. Here's the problem... When I add a link to the menu I use this method:
public function add_menu_item($item) {
$this->menu_items[] = $item;
}
This builds up an array ($this->menu_items
) and there is then another method which cycles through the items in it to add them to the output buffer. It's quite long but a shortened version of it is here:
public function create_links() {
foreach ($this->menu_items as $item) {
$output = '';
$output = '<a href="'.$item['link'].'">'.$item['text'].'</a>';
$this->add_to_output_buffer($output);
}
}
There is quite a lot of logic in create_links()
because various classes and other things get applied to the links, but essentially it has to work like this because it needs to loop through the full array of links.
The problem: If I want to add random HTML at any point in the menu, I have a function to do this:
public function add_misc_html($html = '') {
$this->output_buffer .= $html;
}
But in my script which uses these functions, it doesn't know the order in which things are being called. An example:
$MenuBuilder = new MenuBuilder;
$MenuBuilder->add_menu_item(['text' => 'Link 1', 'link' => '#']);
$MenuBuilder->add_menu_item(['text' => 'Link 2', 'link' => '#']);
$MenuBuilder->add_misc_html('<h1>testing add_misc_html</h1>');
$MenuBuilder->add_menu_item(['text' => 'Link 3', 'link' => '#']);
$MenuBuilder->create_links();
$MenuBuilder->render();
So the problem is that the desired output is:
<a href="#">Link 1</a>
<a href="#">Link 2</a>
<h1>testing add_misc_html</h1>
<a href="#">Link 3</a>
But this won't work, because when I call create_links()
it's not aware of add_misc_html
and where this is called in the overall structure of the calls to MenuBuilder
What ways can I achieve the desired output? Please note that add_misc_html()
needs to work anywhere it gets called, prior to render()
. There is a whole load of other stuff in the class to open the menu and close it, so it needs to be able to modify the $this->output_buffer
at any point.
This is because your add_misc_html
writes directly to your output buffer, where add_menu_item
modifies the structure of your object, you can't mix these calls. You must first write your modified object state to the buffer and only then write "misc" HTML to the buffer. This is pretty bad though.
There are many solutions you can come up with but the one I will suggest to you is to not have an output buffer at all, and possibly extend the logic of the class with a couple other classes.
class MenuBuilder {
protected $items = [];
public function add(MenuItem $item) {
$this->items[] = $item;
}
public function render() {
return join('', $this->items);
}
}
Where you will have different types of menu items, like Link
and Misc
that are used in your example.
interface MenuItem {
public function __toString();
}
class LinkMenuItem implements MenuItem {
protected $link;
protected $text;
public function __construct($link, $text) {
$this->link = $link;
$this->text = $text;
}
public function __toString() {
return '<a href="' . $this->link . '">'. $this->text .'</a>';
}
}
class MiscMenuItem implements MenuItem {
protected $html;
public function __construct($html) {
$this->html = $html;
}
public function __toString() {
return $this->html;
}
}
And now your class will be used like this.
$builder = new MenuBuilder();
$builder->add(new LinkMenuItem('#', 'Link 1'));
$builder->add(new LinkMenuItem('#', 'Link 2'));
$builder->add(new MiscMenuItem('<h1>Testing html</h1>');
$builder->add(new LinkMenuItem('#', 'Link 3'));
And when you want to render.
$builder->render();
Based on the discussion in the comments I am coming to think that you are having trouble rendering the objects as HTML hierarchically. Again there are a number of ways you can go about this, I am just proposing one
You can add a base class that your other menu items will extend
abstract class ParentMenuItem extends MenuItem {
protected $children = [];
public function addChild(MenuItem $child) {
$this->children[] = $child;
}
public function __toString() {
return '<ul>' . join(null, $this->children) . '</ul>';
}
}
Then your LinkMenuItem
will look like this
class LinkMenuItem extends ParentMenuItem {
protected $link;
protected $text;
public function __construct($link, $text) {
$this->link = $link;
$this->text = $text;
}
public function __toString() {
return '<a href="' . $this->link . '">'. $this->text .'</a>' . parent::__toString();
}
}
When you want to add children you will add them to the menu item they belong to (duh?)
$builder = new MenuBuilder();
$link1 = new LinkMenuItem('#', 'Link 1');
$link1->addChild(new LinkMenuItem('#', 'Child1'));
$link1->addChild(new LinkMenuItem('#', 'Child2'));
$builder->add($link1);
$builder->add(new LinkMenuItem('#', 'Link 2'));
$builder->add(new MiscMenuItem('<h1>Testing html</h1>'));
$builder->add(new LinkMenuItem('#', 'Link 3'));
You can also enable chaining to avoid using variables and all kinds of other good stuff like factories etc.