douglasgreen / pagemaker
A project to build webpages in OOP style with a plug-in architecture
Installs: 6
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 1
Forks: 0
Open Issues: 0
pkg:composer/douglasgreen/pagemaker
Requires
- php: >=8.1
Requires (Dev)
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.5
- rector/rector: ^1.0
- symplify/easy-coding-standard: ^12.2
README
A project to build webpages in OOP style with a plug-in architecture.
Overview
This is a complete redesign of a modular web page layout system using PHP 8.1+ features. The engine provides a robust, extensible architecture for building pages from independent, reusable widgets.
Key Improvements Over Original Design
1. Flexible Architecture
- Original: Hard-coded sections (header, main, footer)
- New: Arbitrary slots/regions defined at runtime
- Benefit: Can create any layout structure (sidebars, grids, nested regions)
2. Composition Pattern
- Original: Flat widget structure
- New: Nested containers with parent-child relationships
- Benefit: Build complex UIs from simple components
3. Interface-Driven Design
- Original: Single abstract class
- New: Multiple interfaces for different capabilities
- Benefit: Mix and match features, better testability
4. Asset Management
- Original: Assets registered during render
- New: Centralized AssetManager with deduplication
- Benefit: No duplicate assets, priority control, proper ordering
5. Type Safety
- Original: Array-based configuration
- New: Enums, value objects, and typed properties
- Benefit: Better IDE support, fewer runtime errors
6. Configuration System
- Original: Limited widget configuration
- New: Full key-value config with type safety
- Benefit: Pass data to widgets easily
Core Concepts
Interfaces
RenderableInterface
Base interface for anything that can produce HTML output.
interface RenderableInterface { public function render(): string; public function getId(): string; }
ContainerInterface
For components that can contain child components with named slots.
interface ContainerInterface extends RenderableInterface { public function addChild(RenderableInterface $child, ?string $slot = null): self; public function getChildren(?string $slot = null): array; public function removeChild(string $id): bool; }
AssetAwareInterface
For components that require CSS/JS assets.
interface AssetAwareInterface { public function getStylesheets(): array; public function getScripts(): array; public function getDependencies(): array; }
ConfigurableInterface
For components that accept configuration.
interface ConfigurableInterface { public function setConfig(string $key, mixed $value): self; public function getConfig(string $key, mixed $default = null): mixed; }
Value Objects
Asset
Immutable representation of a CSS or JS file with:
- Name and version
- URL and type
- Priority for ordering
- Custom attributes (async, defer, integrity, etc.)
- Automatic version parameter appending
MetaTag
Represents HTML meta tags with type safety (name vs http-equiv vs property).
Core Classes
AssetManager
- Deduplicates assets by name+version
- Sorts by priority
- Generates versioned URLs
- Produces HTML tags
AbstractWidget
Base class providing:
- Unique ID generation
- Attribute management (class, data-*, etc.)
- Configuration storage
- Asset registration
- Template method pattern for rendering
Container
Extends AbstractWidget to support child widgets with:
- Named slots (regions)
- Slot ordering
- Recursive asset collection
- Child management (add/remove)
Page
Top-level class that:
- Manages document metadata
- Collects all assets from widget tree
- Renders complete HTML5 document
- Provides clean API for page construction
Usage Patterns
Basic Page with Simple Widgets
$page = new Page(); $page->setTitle('My Website') ->setLang('en') ->setFavicon('/favicon.ico'); $header = new HtmlWidget('header', 'header'); $header->setHtml('<h1>Welcome</h1>'); $page->addWidget($header); echo $page->render();
Complex Layout with Multiple Regions
// Create custom layout $layout = new Layout(['header', 'main', 'sidebar', 'footer']); // Add widgets to specific regions $layout->addChild($navigationWidget, 'header'); $layout->addChild($contentWidget, 'main'); $layout->addChild($adsWidget, 'sidebar'); $layout->addChild($footerWidget, 'footer'); $page->addWidget($layout);
Widget with Assets
class MyCustomWidget extends AbstractWidget { protected function initialize(): void { // Register CSS $this->addStylesheet(new Asset( name: 'my-widget-style', url: '/widgets/my-widget/style.css', version: '1.2.0', type: AssetType::STYLESHEET, priority: 50 )); // Register JS $this->addScript(new Asset( name: 'my-widget-script', url: '/widgets/my-widget/script.js', version: '1.2.0', type: AssetType::SCRIPT, attributes: ['defer' => true], priority: 50 )); // Set dependencies $this->dependencies = ['jQuery']; } protected function renderContent(): string { return '<div class="my-widget-content">Content here</div>'; } }
Nested Containers
$page = new Container('page', 'div'); $sidebar = new Container('sidebar', 'aside'); $sidebar->addChild(new SearchWidget()); $sidebar->addChild(new CategoriesWidget()); $main = new Container('main', 'main'); $main->addChild(new ArticleWidget()); $page->addChild($sidebar); $page->addChild($main);
Configurable Widgets
class DataTableWidget extends AbstractWidget { protected function renderContent(): string { $data = $this->getConfig('data', []); $columns = $this->getConfig('columns', []); // Render table with data and columns return $this->renderTable($data, $columns); } } $table = new DataTableWidget(); $table->setConfig('data', $users) ->setConfig('columns', ['Name', 'Email', 'Role']);
Extension Points
Custom Widget Types
Create specialized widgets by extending AbstractWidget:
class CarouselWidget extends AbstractWidget { protected function initialize(): void { $this->tagName = 'div'; $this->addClass('carousel'); // Register carousel JS/CSS } public function addSlide(string $html): void { // Implementation } protected function renderContent(): string { // Render carousel HTML } }
Custom Containers
Create specialized layouts by extending Container:
class GridLayout extends Container { public function __construct(int $columns = 3) { parent::__construct('grid', 'div'); $this->addClass('grid'); $this->setAttribute('style', "grid-template-columns: repeat($columns, 1fr)"); } }
Custom Asset Types
While the current system uses AssetType enum for CSS and JS, you can extend it:
enum AssetType { case STYLESHEET; case SCRIPT; case FONT; case PRELOAD; }
Best Practices
1. Widget Isolation
Each widget should:
- Use BEM or scoped CSS class names
- Prefix all CSS classes with widget name
- Not rely on global styles
- Declare all asset dependencies
2. Asset Management
- Use semantic versioning for all assets
- Set appropriate priorities (0-10 for critical, 90-100 for optional)
- Include integrity hashes for CDN resources
- Use
deferorasyncfor non-critical JS
3. Configuration
- Use config for variable data, not hardcoded values
- Validate config in widget constructor
- Provide sensible defaults
- Document expected config keys
4. Container Slots
- Name slots semantically (not "div1", "div2")
- Define slot order in constructor
- Document available slots for custom containers
5. Error Handling
- Validate widget state before rendering
- Catch and log render errors
- Provide fallback content for failed widgets
- Use type hints and return types throughout
Migration from Original Design
Replacing Old Code
Old:
$page = new Page('My Title'); $page->addHeaderWidget($widget); $page->addMainWidget($widget); $page->addFooterWidget($widget);
New:
$page = new Page(); $page->setTitle('My Title'); $layout = new Layout(['header', 'main', 'footer']); $layout->addChild($widget, 'header'); $layout->addChild($widget, 'main'); $layout->addChild($widget, 'footer'); $page->addWidget($layout);
Widget Migration
Old:
class MyWidget extends AbstractWidget { public function render(): string { return '<div>Content</div>'; } }
New:
class MyWidget extends AbstractWidget { protected function renderContent(): string { return 'Content'; // Tag wrapper handled automatically } }
Testing Strategies
Unit Testing Widgets
public function testWidgetRendersCorrectly(): void { $widget = new MyWidget('test-id'); $widget->setConfig('title', 'Test'); $html = $widget->render(); $this->assertStringContainsString('Test', $html); $this->assertStringContainsString('id="test-id"', $html); }
Integration Testing
public function testPageCollectsAllAssets(): void { $page = new Page(); $widget1 = new WidgetWithCSS(); $widget2 = new WidgetWithJS(); $page->addWidget($widget1); $page->addWidget($widget2); $html = $page->render(); $this->assertStringContainsString('widget1.css', $html); $this->assertStringContainsString('widget2.js', $html); }
Performance Considerations
- Asset Deduplication: AssetManager prevents loading same library twice
- Lazy Rendering: Widgets only render when
render()is called - Priority Sorting: Critical assets load first
- Version Caching: Versioned URLs enable long-term caching
- Minimal Overhead: Value objects and enums have zero runtime cost
Future Enhancements
Potential additions for future versions:
- Event System: Widget-to-widget communication
- Template Engine: Integration with Twig/Plates
- Cache Layer: Render caching