Laravel 13 组合模式深度解析
组合模式是一种结构型设计模式,它允许将对象组合成树形结构来表示”部分-整体”的层次结构。本文将深入探讨 Laravel 13 中组合模式的高级用法。
组合模式基础
什么是组合模式
组合模式使客户端可以统一对待单个对象和组合对象,无需关心处理的是单个对象还是组合对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
namespace App\Contracts;
interface ComponentInterface { public function operation(): string;
public function add(ComponentInterface $component): void;
public function remove(ComponentInterface $component): void;
public function getChild(int $index): ?ComponentInterface; }
|
文件系统组合
文件系统组件接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\Contracts\Filesystem;
interface FilesystemComponentInterface { public function getName(): string;
public function getPath(): string;
public function getSize(): int;
public function isDirectory(): bool;
public function display(int $indent = 0): string; }
|
文件类(叶子节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
| <?php
namespace App\Filesystem\Components;
use App\Contracts\Filesystem\FilesystemComponentInterface;
class File implements FilesystemComponentInterface { protected string $name; protected string $path; protected int $size;
public function __construct(string $name, string $path, int $size = 0) { $this->name = $name; $this->path = $path; $this->size = $size; }
public function getName(): string { return $this->name; }
public function getPath(): string { return $this->path; }
public function getSize(): int { return $this->size; }
public function isDirectory(): bool { return false; }
public function display(int $indent = 0): string { $prefix = str_repeat(' ', $indent); return "{$prefix}📄 {$this->name} ({$this->formatSize()})\n"; }
protected function formatSize(): string { $units = ['B', 'KB', 'MB', 'GB']; $size = $this->size;
for ($i = 0; $size > 1024 && $i < count($units) - 1; $i++) { $size /= 1024; }
return round($size, 2) . ' ' . $units[$i]; } }
|
目录类(组合节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111
| <?php
namespace App\Filesystem\Components;
use App\Contracts\Filesystem\FilesystemComponentInterface; use Illuminate\Support\Collection;
class Directory implements FilesystemComponentInterface { protected string $name; protected string $path; protected Collection $children;
public function __construct(string $name, string $path = '') { $this->name = $name; $this->path = $path; $this->children = collect(); }
public function getName(): string { return $this->name; }
public function getPath(): string { return $this->path; }
public function getSize(): int { return $this->children->sum(fn($child) => $child->getSize()); }
public function isDirectory(): bool { return true; }
public function add(FilesystemComponentInterface $component): self { $this->children->push($component); return $this; }
public function remove(FilesystemComponentInterface $component): self { $this->children = $this->children->reject( fn($child) => $child === $component ); return $this; }
public function getChild(string $name): ?FilesystemComponentInterface { return $this->children->first(fn($child) => $child->getName() === $name); }
public function getChildren(): Collection { return $this->children; }
public function display(int $indent = 0): string { $prefix = str_repeat(' ', $indent); $output = "{$prefix}📁 {$this->name}/\n";
foreach ($this->children as $child) { $output .= $child->display($indent + 1); }
return $output; }
public function findByName(string $name): ?FilesystemComponentInterface { if ($this->name === $name) { return $this; }
foreach ($this->children as $child) { if ($child->isDirectory()) { $found = $child->findByName($name); if ($found) { return $found; } } elseif ($child->getName() === $name) { return $child; } }
return null; }
public function flatten(): Collection { $items = collect([$this]);
foreach ($this->children as $child) { if ($child->isDirectory()) { $items = $items->merge($child->flatten()); } else { $items->push($child); } }
return $items; } }
|
菜单系统组合
菜单组件接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\Contracts\Menu;
interface MenuComponentInterface { public function render(): string;
public function getUrl(): string;
public function getLabel(): string;
public function isActive(): bool;
public function hasChildren(): bool; }
|
菜单项(叶子节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| <?php
namespace App\Menu\Components;
use App\Contracts\Menu\MenuComponentInterface;
class MenuItem implements MenuComponentInterface { protected string $label; protected string $url; protected string $icon; protected bool $active = false;
public function __construct(string $label, string $url, string $icon = '') { $this->label = $label; $this->url = $url; $this->icon = $icon; }
public function render(): string { $iconHtml = $this->icon ? "<i class=\"{$this->icon}\"></i> " : ''; $activeClass = $this->active ? ' class="active"' : '';
return "<li{$activeClass}><a href=\"{$this->url}\">{$iconHtml}{$this->label}</a></li>"; }
public function getUrl(): string { return $this->url; }
public function getLabel(): string { return $this->label; }
public function isActive(): bool { return $this->active; }
public function setActive(bool $active): self { $this->active = $active; return $this; }
public function hasChildren(): bool { return false; } }
|
菜单组(组合节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100
| <?php
namespace App\Menu\Components;
use App\Contracts\Menu\MenuComponentInterface; use Illuminate\Support\Collection;
class MenuGroup implements MenuComponentInterface { protected string $label; protected string $icon; protected Collection $children; protected bool $active = false;
public function __construct(string $label, string $icon = '') { $this->label = $label; $this->icon = $icon; $this->children = collect(); }
public function add(MenuComponentInterface $item): self { $this->children->push($item); return $this; }
public function remove(MenuComponentInterface $item): self { $this->children = $this->children->reject(fn($child) => $child === $item); return $this; }
public function render(): string { $iconHtml = $this->icon ? "<i class=\"{$this->icon}\"></i> " : ''; $activeClass = $this->active ? ' active' : '';
$output = "<li class=\"dropdown{$activeClass}\">"; $output .= "<a href=\"#\" class=\"dropdown-toggle\">{$iconHtml}{$this->label}</a>"; $output .= "<ul class=\"dropdown-menu\">";
foreach ($this->children as $child) { $output .= $child->render(); }
$output .= "</ul></li>";
return $output; }
public function getUrl(): string { return '#'; }
public function getLabel(): string { return $this->label; }
public function isActive(): bool { return $this->active || $this->children->contains(fn($child) => $child->isActive()); }
public function setActive(bool $active): self { $this->active = $active; return $this; }
public function hasChildren(): bool { return $this->children->isNotEmpty(); }
public function getChildren(): Collection { return $this->children; }
public function findByUrl(string $url): ?MenuComponentInterface { foreach ($this->children as $child) { if ($child->getUrl() === $url) { return $child; }
if ($child->hasChildren()) { $found = $child->findByUrl($url); if ($found) { return $found; } } }
return null; } }
|
菜单构建器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| <?php
namespace App\Menu;
use App\Menu\Components\{MenuItem, MenuGroup};
class MenuBuilder { protected MenuGroup $root;
public function __construct(string $label = 'Main Menu') { $this->root = new MenuGroup($label); }
public function addItem(string $label, string $url, string $icon = ''): self { $this->root->add(new MenuItem($label, $url, $icon)); return $this; }
public function addGroup(string $label, callable $callback, string $icon = ''): self { $group = new MenuGroup($label, $icon); $callback($group); $this->root->add($group); return $this; }
public function setActiveByUrl(string $url): self { $item = $this->root->findByUrl($url);
if ($item) { $item->setActive(true); }
return $this; }
public function build(): MenuGroup { return $this->root; }
public function render(): string { return '<ul class="menu">' . $this->root->render() . '</ul>'; } }
|
权限系统组合
权限组件接口
1 2 3 4 5 6 7 8 9 10 11 12
| <?php
namespace App\Contracts\Permission;
interface PermissionComponentInterface { public function getName(): string;
public function check($user): bool;
public function getDescription(): string; }
|
单个权限(叶子节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
namespace App\Permission\Components;
use App\Contracts\Permission\PermissionComponentInterface;
class Permission implements PermissionComponentInterface { protected string $name; protected string $description; protected $checker;
public function __construct(string $name, string $description = '', ?callable $checker = null) { $this->name = $name; $this->description = $description; $this->checker = $checker ?? fn($user) => $user->can($name); }
public function getName(): string { return $this->name; }
public function check($user): bool { return ($this->checker)($user); }
public function getDescription(): string { return $this->description; } }
|
权限组(组合节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82
| <?php
namespace App\Permission\Components;
use App\Contracts\Permission\PermissionComponentInterface; use Illuminate\Support\Collection;
class PermissionGroup implements PermissionComponentInterface { protected string $name; protected string $description; protected Collection $permissions; protected string $mode;
public function __construct(string $name, string $description = '', string $mode = 'any') { $this->name = $name; $this->description = $description; $this->permissions = collect(); $this->mode = $mode; }
public function getName(): string { return $this->name; }
public function add(PermissionComponentInterface $permission): self { $this->permissions->push($permission); return $this; }
public function remove(PermissionComponentInterface $permission): self { $this->permissions = $this->permissions->reject(fn($p) => $p === $permission); return $this; }
public function check($user): bool { if ($this->permissions->isEmpty()) { return false; }
if ($this->mode === 'all') { return $this->permissions->every(fn($p) => $p->check($user)); }
return $this->permissions->some(fn($p) => $p->check($user)); }
public function getDescription(): string { return $this->description; }
public function getPermissions(): Collection { return $this->permissions; }
public function flatten(): Collection { $items = collect();
foreach ($this->permissions as $permission) { if ($permission instanceof self) { $items = $items->merge($permission->flatten()); } else { $items->push($permission); } }
return $items; }
public function getAllNames(): array { return $this->flatten()->map(fn($p) => $p->getName())->toArray(); } }
|
权限构建器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| <?php
namespace App\Permission;
use App\Permission\Components\{Permission, PermissionGroup};
class PermissionBuilder { protected PermissionGroup $root;
public function __construct(string $name = 'root') { $this->root = new PermissionGroup($name); }
public function addPermission(string $name, string $description = ''): self { $this->root->add(new Permission($name, $description)); return $this; }
public function addGroup(string $name, callable $callback, string $mode = 'any'): self { $group = new PermissionGroup($name, '', $mode); $callback($group); $this->root->add($group); return $this; }
public function build(): PermissionGroup { return $this->root; } }
|
表单组件组合
表单组件接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| <?php
namespace App\Contracts\Form;
interface FormComponentInterface { public function render(): string;
public function getName(): string;
public function validate(): array;
public function fill(array $data): void;
public function getData(): array; }
|
表单字段(叶子节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| <?php
namespace App\Form\Components;
use App\Contracts\Form\FormComponentInterface;
class FormField implements FormComponentInterface { protected string $name; protected string $type; protected string $label; protected mixed $value = null; protected array $rules = []; protected array $errors = [];
public function __construct(string $name, string $type, string $label = '') { $this->name = $name; $this->type = $type; $this->label = $label; }
public function rules(array $rules): self { $this->rules = $rules; return $this; }
public function render(): string { $labelHtml = $this->label ? "<label for=\"{$this->name}\">{$this->label}</label>" : ''; $errorHtml = $this->errors ? "<span class=\"error\">" . implode(', ', $this->errors) . "</span>" : '';
$inputHtml = match ($this->type) { 'textarea' => "<textarea name=\"{$this->name}\" id=\"{$this->name}\">{$this->value}</textarea>", 'select' => $this->renderSelect(), default => "<input type=\"{$this->type}\" name=\"{$this->name}\" id=\"{$this->name}\" value=\"{$this->value}\">", };
return "<div class=\"form-field\">{$labelHtml}{$inputHtml}{$errorHtml}</div>"; }
protected function renderSelect(): string { $options = collect($this->options ?? []) ->map(fn($label, $value) => "<option value=\"{$value}\"" . ($this->value == $value ? ' selected' : '') . ">{$label}</option>") ->implode('');
return "<select name=\"{$this->name}\" id=\"{$this->name}\">{$options}</select>"; }
public function getName(): string { return $this->name; }
public function validate(): array { $validator = validator([$this->name => $this->value], [$this->name => $this->rules]);
if ($validator->fails()) { $this->errors = $validator->errors()->get($this->name); return $this->errors; }
$this->errors = []; return []; }
public function fill(array $data): void { $this->value = $data[$this->name] ?? null; }
public function getData(): array { return [$this->name => $this->value]; } }
|
表单组(组合节点)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73
| <?php
namespace App\Form\Components;
use App\Contracts\Form\FormComponentInterface; use Illuminate\Support\Collection;
class FormGroup implements FormComponentInterface { protected string $name; protected string $legend; protected Collection $components;
public function __construct(string $name, string $legend = '') { $this->name = $name; $this->legend = $legend; $this->components = collect(); }
public function add(FormComponentInterface $component): self { $this->components->push($component); return $this; }
public function render(): string { $legendHtml = $this->legend ? "<legend>{$this->legend}</legend>" : ''; $fieldsHtml = $this->components->map(fn($c) => $c->render())->implode('');
return "<fieldset name=\"{$this->name}\">{$legendHtml}{$fieldsHtml}</fieldset>"; }
public function getName(): string { return $this->name; }
public function validate(): array { $errors = [];
foreach ($this->components as $component) { $errors = array_merge($errors, $component->validate()); }
return $errors; }
public function fill(array $data): void { foreach ($this->components as $component) { $component->fill($data); } }
public function getData(): array { $data = [];
foreach ($this->components as $component) { $data = array_merge($data, $component->getData()); }
return $data; }
public function getComponent(string $name): ?FormComponentInterface { return $this->components->first(fn($c) => $c->getName() === $name); } }
|
测试组合模式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
| <?php
namespace Tests\Unit\Composite;
use Tests\TestCase; use App\Filesystem\Components\{File, Directory}; use App\Menu\Components\{MenuItem, MenuGroup}; use App\Permission\Components\{Permission, PermissionGroup};
class CompositeTest extends TestCase { public function test_filesystem_composite(): void { $root = new Directory('root'); $root->add(new File('file1.txt', '/root/file1.txt', 100)); $root->add(new File('file2.txt', '/root/file2.txt', 200));
$subdir = new Directory('subdir'); $subdir->add(new File('file3.txt', '/root/subdir/file3.txt', 300)); $root->add($subdir);
$this->assertEquals(600, $root->getSize()); $this->assertTrue($root->isDirectory()); $this->assertFalse($root->getChild('file1.txt')->isDirectory()); }
public function test_menu_composite(): void { $menu = new MenuGroup('Main'); $menu->add(new MenuItem('Home', '/')); $menu->add(new MenuItem('About', '/about'));
$submenu = new MenuGroup('Services'); $submenu->add(new MenuItem('Web', '/services/web')); $submenu->add(new MenuItem('Mobile', '/services/mobile')); $menu->add($submenu);
$this->assertTrue($menu->hasChildren()); $this->assertCount(3, $menu->getChildren()); }
public function test_permission_composite(): void { $user = new class { public function can($permission) { return in_array($permission, ['edit', 'delete']); } };
$group = new PermissionGroup('content', '', 'any'); $group->add(new Permission('edit')); $group->add(new Permission('delete')); $group->add(new Permission('publish'));
$this->assertTrue($group->check($user));
$allGroup = new PermissionGroup('admin', '', 'all'); $allGroup->add(new Permission('edit')); $allGroup->add(new Permission('admin'));
$this->assertFalse($allGroup->check($user)); } }
|
最佳实践
1. 统一接口
1 2 3 4 5 6
| <?php
interface ComponentInterface { public function operation(): mixed; }
|
2. 叶子和组合实现相同接口
1 2 3 4 5 6 7 8 9 10 11
| <?php
class Leaf implements ComponentInterface { public function operation(): mixed { } }
class Composite implements ComponentInterface { public function operation(): mixed { } }
|
3. 提供默认实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <?php
abstract class BaseComponent implements ComponentInterface { public function add(ComponentInterface $component): void { throw new \BadMethodCallException('Cannot add to a leaf'); }
public function remove(ComponentInterface $component): void { throw new \BadMethodCallException('Cannot remove from a leaf'); } }
|
总结
Laravel 13 的组合模式提供了一种优雅的方式来处理树形结构数据。通过合理使用组合模式,可以创建灵活、可扩展的层次结构,同时简化客户端代码的复杂度。