- The composite: This object represent nodes that have other children nodes also called (subtree).
- The leaf node: which represent nodes that have no children or called terminal nodes.
If we think about the composite pattern as a recursive function, in recursive functions we must have a stop point, otherwise the function will execute infinitely. This stop point in the context of the composite pattern is the leaf node, so the leaf node mark the termination point in the composite pattern.
Now let’s think about building the composite structure. We will need an interface with a single method for example called “execute” which the composite and the leaf classes must implement as we will see in the below example.
- IMenu interface.
- MenuItem (leaf)
- MenuGroup (composite): contains children of MenuItem or MenuGroup.
<?php /** * Interface IMenu */ interface IMenu { public function render(); } /** * Class MenuItem (Leaf) */ class MenuItem implements IMenu { private $text; private $url; public function __construct($text, $url) { $this->text = $text; $this->url = $url; } public function render() { return '<a href="'.$this->url.'">'.$this->text.'</a>'; } } /** * Class MenuGroup (Composite) */ class MenuGroup implements IMenu { private $items = []; private $text; private $url; public function __construct($text = null, $url = null) { $this->text = $text; $this->url = $url; } public function addChildren(IMenu $item) { array_push($this->items, $item); } public function render() { $html = ""; if($this->text && $this->url) { $html .= '<a href="'.$this->url.'">'.$this->text.'</a>'; } $html .= "<ul class='menu'>"; foreach($this->items as $item) { $html .= '<li>' . $item->render() . '</li>'; } $html .= "</ul>"; return $html; } public function getItems() { return $this->items; } }
In this code both the MenuItem and MenuGroup classes implement the Menu interface with different implementations for the render() method and this is the core goal of the composite pattern. The render() method in the MenuItem class just outputs an html anchor tag with a text and link properties that we inject through the constructor.
The MenuGroup class also implement the render() method and above that it contains the addChildren() method which when called it will add an item of type IMenu, which can be a MenuItem or another MenuGroup. Finally in the render() method we iterates over the items array and call each item corresponding render() method.
Basic Usage:
$menu = new MenuGroup(); $menu->addChildren(new MenuItem("Home", "#")); $menu->addChildren(new MenuItem("About", "#")); $menu->addChildren(new MenuItem("Contact US", "#")); echo $menu->render();
Usage With Nested Menu Items:
$menu = new MenuGroup(); $menu->addChildren(new MenuItem("Home", "#")); $menu->addChildren(new MenuItem("About", "#")); $subMenu = new MenuGroup("Services", "#"); $subMenu->addChildren(new MenuItem("Web Development", "#")); $subMenu->addChildren(new MenuItem("Mobile App Development", "#")); $subMenu->addChildren(new MenuItem("Artificial Intelligence", "#")); $menu->addChildren($subMenu); $subMenu2 = new MenuGroup("Products", "#"); $subMenu2->addChildren(new MenuItem("WebSites", "#")); $subMenu3 = new MenuGroup("Mobile Apps", "#"); $subMenu3->addChildren(new MenuItem("Android", "#")); $subMenu3->addChildren(new MenuItem("IOS", "#")); $subMenu2->addChildren($subMenu3); $menu->addChildren($subMenu2); $menu->addChildren(new MenuItem("Contact", "#")); echo $menu->render();
Using an Array of Items:
$items = [ new MenuItem("Home", "#"), new MenuItem("About", "#"), [ new MenuGroup("Services", "#"), 'children' => [ new MenuItem("Web Development", "#"), new MenuItem("Mobile App Development", "#") ] ], [ new MenuGroup("Products", "#"), 'children' => [ new MenuItem("WebSites", "#"), [ new MenuGroup("Mobile Apps", "#"), 'children' => [ new MenuItem("Android", "#"), new MenuItem("IOS", "#") ] ] ] ], new MenuItem("Contact us", "#") ]; /** * recursive function to build the menu array */ function buildMenu(MenuGroup $menu, $items) { foreach ($items as $item) { if(!is_array($item)) { $menu->addChildren($item); } else { $submenu = buildMenu($item[0], $item['children']); $menu->addChildren($submenu); } } return $menu; } $menu = buildMenu(new MenuGroup(), $items); echo $menu->render();
Styling The Menu
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>Navbar Rendered With Composite Pattern</title> <style> ul.menu { list-style: none; } ul.menu li { display: inline; margin: 5px; position: relative; } ul.menu li a { color: #fff; text-decoration: none; font-family: Arial, Helvetica, serif; background: gray; padding: 10px; } ul.menu li ul.menu { display: none; position: absolute; width: 250px; top: 35px; left: -45px; } ul.menu li:hover > ul { display: inline; } ul.menu li ul.menu li { margin-top: 6px; margin-bottom: 22px; display: block; } ul.menu li { } </style> </head> <body> <?php require_once("composite_pattern.php") ?> </body> </html>