Skip to content

Plugin/Theme Framework for Wordpress Applications

License

Notifications You must be signed in to change notification settings

vafFriedrich/wpapp

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

WPApp

Modern WordPress framework with Symfony Dependency Injection and attribute-based hooks.

Features

  • Symfony DI Container - Full-featured dependency injection with autowiring
  • Attribute-Based Hooks - Define WordPress hooks using PHP 8.4 attributes
  • Attribute-Based Shortcodes - Clean shortcode definitions with automatic parameter mapping
  • Templating System - PHTML and Twig template engines with theme override support
  • Template Functions - Define template helpers with dependency injection using attributes
  • Service Discovery - Automatic service registration from configured directories
  • Container Caching - Compiled container with file-based cache invalidation
  • Bundle System - Support for parent/child themes and plugin bundles
  • PHP & YAML Config - Flexible configuration options
  • Type-Safe - Full PHP 8.4 type safety

Requirements

  • PHP 8.4 or higher
  • WordPress 6.0 or higher
  • Composer

Installation

Install via Composer:

composer require saij/wpapp

Quick Start

Plugin Example

1. Create your plugin structure:

my-plugin/
├── src/
│   ├── Plugin.php
│   ├── Services/
│   │   ├── ExampleService.php
│   │   └── TemplateHelpers.php
│   └── Templates/
│       └── Welcome.phtml
├── config/
│   └── services.yaml
├── composer.json
└── my-plugin.php

2. Define your plugin class (src/Plugin.php):

<?php
namespace MyPlugin;

final class Plugin extends \WPApp\Application
{
}

3. Boot your plugin (my-plugin.php):

<?php
/*
Plugin Name: My Plugin
*/

require_once __DIR__ . '/vendor/autoload.php';

MyPlugin\Plugin::boot(__DIR__);

4. Configure services (config/services.yaml):

services:
  _defaults:
    autowire: true
    autoconfigure: true

  MyPlugin\:
    resource: '../src/*'

5. Create a service with template rendering (src/Services/ExampleService.php):

<?php
namespace MyPlugin\Services;

use WPApp\Hook\Hook;
use WPApp\Template\TemplateRenderer;

class ExampleService
{
    public function __construct(
        private readonly TemplateRenderer $renderer,
    ) {}

    #[Hook('the_content')]
    public function addWelcomeMessage(string $content): string
    {
        $welcome = $this->renderer->render('@MyPlugin/Templates/Welcome', [
            'username' => wp_get_current_user()->display_name,
        ]);

        return $welcome . $content;
    }
}

6. Create template helpers (src/Services/TemplateHelpers.php):

<?php
namespace MyPlugin\Services;

use WPApp\Template\TemplateFunction;

class TemplateHelpers
{
    #[TemplateFunction('greeting')]
    public function greeting(string $name): string
    {
        $hour = (int) date('G');
        $timeOfDay = match (true) {
            $hour < 12 => 'morning',
            $hour < 18 => 'afternoon',
            default => 'evening',
        };

        return "Good {$timeOfDay}, {$name}!";
    }
}

7. Create a template (src/Templates/Welcome.phtml):

<div class="welcome-message">
    <h3><?= $this->greeting($this->username) ?></h3>
    <p>Welcome back to our site!</p>
</div>

Theme Example

Theme structure:

my-theme/
├── src/
│   ├── Theme.php
│   └── Services/
│       └── ThemeService.php
├── config/
│   └── services.yaml
├── composer.json
└── functions.php

Define your theme class (src/Theme.php):

<?php
namespace MyTheme;

final class Theme extends \WPApp\Application
{
}

Boot your theme (functions.php):

<?php
require_once __DIR__ . '/vendor/autoload.php';

MyTheme\Theme::boot(__DIR__);

Using Hooks

Define hooks using the #[Hook] attribute:

<?php
namespace MyPlugin\Services;

use WPApp\Hook\Hook;

class ExampleService
{
    // Simple action hook
    #[Hook('init')]
    public function onInit(): void
    {
        // Your code here
    }

    // Filter hook with WordPress parameters
    #[Hook('the_content', priority: 20)]
    public function filterContent(string $content): string
    {
        return $content . '<p>Added by my plugin!</p>';
    }

    // Hook with service injection
    #[Hook('wp_enqueue_scripts')]
    public function enqueueScripts(AssetService $assets): void
    {
        $assets->enqueueStyle('my-style');
    }

    // Mix service injection with WordPress parameters
    #[Hook('the_title', priority: 10)]
    public function filterTitle(LoggerService $logger, string $title, int $postId): string
    {
        $logger->log("Processing title for post {$postId}");
        return strtoupper($title);
    }
}

Multiple hooks on same method:

#[Hook('save_post')]
#[Hook('publish_post')]
public function onPostSave(int $postId): void
{
    // Runs on both hooks
}

Using Shortcodes

Define shortcodes using the #[Shortcode] attribute:

<?php
namespace MyPlugin\Services;

use WPApp\Shortcode\Shortcode;

class GalleryShortcode
{
    #[Shortcode('gallery')]
    public function render(
        ImageService $images,           // Service injection
        string $content = '',            // Shortcode content
        int $columns = 3,                // Attribute: columns="3"
        bool $showTitles = false,        // Attribute: show_titles="true"
        string $size = 'medium'          // Attribute: size="medium"
    ): string {
        $gallery = $images->createGallery($columns, $size, $showTitles);
        return $gallery->render($content);
    }
}

Usage in content:

[gallery columns="4" show_titles="true" size="large"]
  Gallery content here
[/gallery]

Using Templates

WPApp provides a powerful templating system with PHTML and Twig engines, dependency injection for template functions, and theme override support.

Rendering Templates

Inject TemplateRenderer into any service to render templates:

<?php
namespace MyPlugin\Services;

use WPApp\Template\TemplateRenderer;

class ProductService
{
    public function __construct(
        private readonly TemplateRenderer $renderer,
    ) {}

    public function renderProduct(int $productId): string
    {
        $product = $this->getProduct($productId);

        return $this->renderer->render(
            '@MyPlugin/Products/Single',
            ['product' => $product]
        );
    }
}

Template Functions

Define reusable template functions with dependency injection using the #[TemplateFunction] attribute:

<?php
namespace MyPlugin\Services;

use WPApp\Template\TemplateFunction;

class TemplateHelpers
{
    #[TemplateFunction('format_price')]
    public function formatPrice(
        CurrencyService $currency,  // Service injection
        float $amount               // Template argument
    ): string {
        return $currency->format($amount);
    }

    #[TemplateFunction('user_avatar')]
    public function renderAvatar(
        AvatarService $avatarService,
        int $userId,
        int $size = 48
    ): string {
        return $avatarService->render($userId, $size);
    }
}

PHTML Templates

PHTML templates use plain PHP with $this context for accessing variables and functions:

Template: src/Products/Single.phtml

<div class="product">
    <h2><?= $this->title ?></h2>
    <div class="price">
        <?= $this->format_price($this->price) ?>
    </div>
    <div class="description">
        <?= $this->description ?>
    </div>

    <!-- Render sub-template using built-in partial() -->
    <?= $this->partial('@MyPlugin/Products/Reviews', [
        'productId' => $this->id
    ]) ?>
</div>

Twig Templates

Twig templates use Twig syntax with automatic template function registration:

Template: src/Products/Single.twig

<div class="product">
    <h2>{{ title }}</h2>
    <div class="price">
        {{ format_price(price) }}
    </div>
    <div class="description">
        {{ description }}
    </div>

    {# Render sub-template using built-in partial() #}
    {{ partial('@MyPlugin/Products/Reviews', {
        productId: id
    }) }}
</div>

Twig features:

  • Automatic caching in wp-content/cache/wpapp-twig/
  • All template functions auto-registered
  • Full Twig syntax support (filters, tests, etc.)

Template Namespace Resolution

Templates use @Namespace/Path/To/Template syntax where:

  • Namespace is your primary namespace from composer.json (e.g., MyPlugin)
  • Path is the relative path to the template file
  • File extension (.phtml or .twig) is detected automatically

Example:

For plugin with primary namespace MyPlugin\:

  • Template name: @MyPlugin/Products/Single
  • Resolves to: src/Products/Single.phtml (or .twig)

For theme with primary namespace MyTheme\:

  • Template name: @MyTheme/Components/Header
  • Resolves to: src/Components/Header.phtml (or .twig)

Theme Override Hierarchy

Templates can be overridden by themes, searched in this order:

  1. Active theme: wp-content/themes/child/templates/MyPlugin/Products/Single.phtml
  2. Parent theme: wp-content/themes/parent/templates/MyPlugin/Products/Single.phtml
  3. Plugin/theme: wp-content/plugins/my-plugin/src/Products/Single.phtml

This allows theme developers to customize plugin templates without modifying plugin files.

Example override structure:

wp-content/
├── themes/
│   └── my-theme/
│       └── templates/
│           └── MyPlugin/
│               └── Products/
│                   └── Single.phtml    # Theme overrides plugin template
└── plugins/
    └── my-plugin/
        └── src/
            └── Products/
                └── Single.phtml        # Original plugin template

Built-in Template Functions

partial(template, context) - Render sub-templates:

// PHTML
<?= $this->partial('@MyPlugin/Components/Button', [
    'text' => 'Click Me',
    'url' => '/action'
]) ?>

// Twig
{{ partial('@MyPlugin/Components/Button', {
    text: 'Click Me',
    url: '/action'
}) }}

Creating Custom Template Engines

Extend the templating system by implementing EngineInterface:

<?php
namespace MyPlugin\Template;

use WPApp\Template\EngineInterface;

class MarkdownEngine implements EngineInterface
{
    public function getSupportedExtensions(): array
    {
        return ['md', 'markdown'];
    }

    public function render(string $path, array $context): string
    {
        $markdown = file_get_contents($path);
        // Your markdown rendering logic here
        return $this->parseMarkdown($markdown, $context);
    }
}

Place the file in your auto-discovered directory (e.g., src/Template/MarkdownEngine.php) and it's automatically discovered and registered. No manual configuration needed!

Service Inheritance

When a child theme or bundle service extends a parent service, the framework automatically handles inheritance at the service level to prevent duplicate registrations.

How it works:

  • Parent services are automatically skipped during attribute discovery when a child service extends them
  • Only the leaf service (most specific in the hierarchy) has its attributes analyzed
  • Inherited methods with #[Hook] or #[Shortcode] are discovered only once on the child service
  • Prevents duplicate hook execution and shortcode conflicts

Example:

// Parent theme
namespace ParentTheme\Services;
class WelcomeService {
    #[Shortcode('welcome')]
    public function render(string $name = 'Guest'): string {
        return "Welcome {$name}!";
    }

    #[Hook('wp_footer')]
    public function addFooter(): void {
        echo '<p>Parent footer</p>';
    }
}

// Child theme extends parent
namespace ChildTheme\Services;
class WelcomeService extends \ParentTheme\Services\WelcomeService {
    // Inherits both render() and addFooter() methods
    // Parent service is skipped during attribute discovery
    // Only child service methods are analyzed
}

Result: Parent service ParentTheme\Services\WelcomeService is completely skipped. The [welcome] shortcode and wp_footer hook are registered only once from the child service.

Important:

  • Child service MUST extend parent service class using PHP extends keyword
  • Applies to parent/child themes, bundles, and any service inheritance
  • Supports multi-level inheritance (grandparent → parent → child)
  • With WP_DEBUG enabled, framework logs skipped services

Service Configuration

Using YAML (config/services.yaml)

services:
  _defaults:
    autowire: true
    autoconfigure: true

  MyPlugin\:
    resource: '../src/*'
    exclude: '../src/{Kernel.php}'

  # Explicit service definition
  MyPlugin\Services\ApiClient:
    arguments:
      $apiKey: '%env(API_KEY)%'
      $timeout: 30

parameters:
  app.version: '1.0.0'
  app.debug: false

Using PHP (config/services.php)

<?php
return [
    'services' => [
        '_defaults' => [
            'autowire' => true,
            'autoconfigure' => true,
        ],

        'MyPlugin\\' => [
            'resource' => '../src/*',
            'exclude' => '../src/{Kernel.php}',
        ],

        'MyPlugin\\Services\\ApiClient' => [
            'arguments' => [
                '$apiKey' => '%env(API_KEY)%',
                '$timeout' => 30,
            ],
        ],
    ],

    'parameters' => [
        'app.version' => '1.0.0',
        'app.debug' => false,
    ],
];

Parent/Child Themes

WPApp automatically detects and supports parent/child theme relationships.

Parent theme:

// parent-theme/functions.php
ParentTheme\Theme::boot(__DIR__);

Child theme:

// child-theme/functions.php
ChildTheme\Theme::boot(__DIR__);

The child theme automatically:

  • Loads parent theme services
  • Merges parent and child configurations
  • Can override parent services by using the same service class name

Service override example:

// Parent theme
namespace ParentTheme\Services;
class HeaderService {
    public function render(): string {
        return '<header>Parent Header</header>';
    }
}

// Child theme (must extend parent)
namespace ChildTheme\Services;
use ParentTheme\Services\HeaderService as ParentHeaderService;

class HeaderService extends ParentHeaderService {
    public function render(): string {
        return '<header>Child Header</header>';
    }
}

Bundle System

Bundles provide modular architecture by packaging related services and configuration together. Each bundle is a class implementing BundleInterface.

Creating a bundle:

namespace MyPlugin\Bundles;

use WPApp\BundleInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AdminBundle implements BundleInterface
{
    public function getPath(): string
    {
        return __DIR__;
    }

    public function configureContainer(ContainerBuilder $builder): void
    {
        // Optional: Register custom services, parameters, or compiler passes
        $builder->setParameter('admin.menu_position', 50);
    }
}

Bundle directory structure:

bundles/admin/
├── config/
│   └── services.yaml    # Bundle-specific services
├── Services/
│   ├── MenuService.php
│   └── SettingsService.php
└── AdminBundle.php      # Bundle class

Registering bundles in config/services.yaml:

wpapp:
  bundles:
    - MyPlugin\Bundles\AdminBundle
    - MyPlugin\Bundles\ApiBundle

services:
  _defaults:
    autowire: true
    autoconfigure: true

  MyPlugin\:
    resource: '../src/*'

Or in config/services.php:

return [
    'wpapp' => [
        'bundles' => [
            MyPlugin\Bundles\AdminBundle::class,
            MyPlugin\Bundles\ApiBundle::class,
        ],
    ],
    'services' => [
        // ... service configuration
    ],
];

Bundles are automatically loaded during container compilation, and their services and configurations are merged into the main container.

Container Access

Access the container from anywhere:

// Get container
$container = MyPlugin\Plugin::getContainer();

// Get service
$service = $container->get(MyService::class);

Advanced Features

Parameter Injection

class MyService
{
    public function __construct(
        private string $apiKey,
        private int $timeout = 30
    ) {}
}
services:
  MyPlugin\Services\MyService:
    arguments:
      $apiKey: '%env(API_KEY)%'
      $timeout: 60

Service Decoration

services:
  MyPlugin\Services\EnhancedLogger:
    decorates: Psr\Log\LoggerInterface
    arguments:
      $inner: '@.inner'

Performance

Container Caching

The container is automatically compiled and cached in container/ directory:

my-plugin/
├── container/
│   ├── container.php      # Compiled container
│   └── container.meta     # Cache validation data

Cache is automatically invalidated when:

  • Any source file is modified
  • Configuration files change
  • Services are added/removed

Pre-compilation

For production, pre-compile the container:

# Add to composer.json
{
  "scripts": {
    "compile-container": "WPApp\\Container\\Compiler::compile"
  }
}

# Then run
composer compile-container

Best Practices

  1. Use Constructor Injection - Inject dependencies via constructor
  2. Type Everything - Leverage PHP 8.4's type system
  3. Keep Services Small - Single responsibility principle
  4. Avoid Static State - Use dependency injection instead
  5. Use Interfaces - Program to interfaces, not implementations
  6. Cache in Production - Pre-compile containers for production

Troubleshooting

Container Not Updating

Delete the cache directory:

rm -rf container/

Service Not Found

Check that:

  1. Class is in configured resource path
  2. Class is instantiable (not abstract/interface)
  3. Namespace matches directory structure
  4. Composer autoloader is up to date (composer dump-autoload)

Hook Not Firing

Ensure:

  1. Method is public
  2. Hook name is correct
  3. WordPress hook exists
  4. Plugin/theme is booted before hook fires

License

GPL-3.0-or-later. See LICENSE file.

About

Plugin/Theme Framework for Wordpress Applications

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages