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

0.1.1 2024-06-01 00:30 UTC

This package is auto-updated.

Last update: 2025-10-22 09:42:17 UTC


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 defer or async for 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

  1. Asset Deduplication: AssetManager prevents loading same library twice
  2. Lazy Rendering: Widgets only render when render() is called
  3. Priority Sorting: Critical assets load first
  4. Version Caching: Versioned URLs enable long-term caching
  5. Minimal Overhead: Value objects and enums have zero runtime cost

Future Enhancements

Potential additions for future versions:

  1. Event System: Widget-to-widget communication
  2. Template Engine: Integration with Twig/Plates
  3. Cache Layer: Render caching