Cookbook

Glueful Extensions

Learn how to create, configure, and optimize Glueful extensions using the ServiceProvider pattern, Composer discovery, and production caching.

Glueful extensions let you add modular domain features (routes, services, migrations, commands, assets) through a familiar ServiceProvider pattern. This guide walks you through discovery via Composer, local development scaffolding, configuration management, performance caching, and migration from older manifest-based systems—so you can ship extensible APIs with confidence.

Overview

The Glueful Extensions System provides a modern architecture for extending the framework's functionality. Built on the industry-standard ServiceProvider pattern, it delivers high performance with a lean, focused core design.

Table of Contents

Quick Start

1. Check System Status

# List every discovered extension with its state (enabled ✓ / available ○ / enabled-but-missing ⚠)
php glueful extensions:list

# Show detailed extension information (by package name, provider FQCN, or slug)
php glueful extensions:info blog

# Diagnose discovery + resolver errors (and, in production, whether the cache is present)
php glueful extensions:diagnose

# Show system summary with performance metrics
php glueful extensions:summary

2. Create Your First Extension

# Create new local extension
php glueful create:extension my-extension

# The extension will be created in extensions/my-extension/
# with a basic ServiceProvider and composer.json

3. Install a Composer Extension

# 1. Install via Composer
composer require vendor/my-extension

# 2. Enable it — installing does NOT auto-load an extension; it must be added to
#    config/extensions.php's `enabled` allow-list. This command does that for you
#    (and recompiles the cache). Accepts the package name, provider FQCN, or slug.
php glueful extensions:enable my-extension

# 3. In production, (re)build the compiled manifest as part of your deploy step
php glueful extensions:cache

Installed ≠ enabled. A glueful-extension package is discovered by Composer but does nothing until its provider FQCN is in config/extensions.phpenabled. Use extensions:enable (or edit the list by hand). Run extensions:list to see which installed extensions are enabled () vs available ().

Architecture

Core Principles

  1. Composer-native discovery - Leverages existing package manager
  2. ServiceProvider pattern - Familiar to all PHP developers
  3. PHP config files - Type-safe and IDE-friendly
  4. Standard interfaces - Reuses Symfony/PSR interfaces
  5. Convention over configuration - Minimal boilerplate

Extension Types

// composer.json
{
    "name": "vendor/my-extension",
    "type": "glueful-extension",
    "autoload": {
        "psr-4": {
            "Vendor\\MyExtension\\": "src/"
        }
    },
    "extra": {
        "glueful": {
            "provider": "Vendor\\MyExtension\\MyExtensionServiceProvider"
        }
    }
}

Local Development Extension

extensions/
└── my-extension/
    ├── composer.json
    ├── src/
    │   └── MyExtensionServiceProvider.php
    ├── routes/
    │   └── routes.php
    ├── config/
    │   └── my-extension.php
    └── database/
        └── migrations/

ServiceProvider Pattern

Each extension has a single ServiceProvider that registers all functionality:

<?php

namespace MyExtension;

use Glueful\Extensions\ServiceProvider;

class MyExtensionServiceProvider extends ServiceProvider
{
    /**
     * Define services for container compilation (production-ready)
     */
    public static function services(): array
    {
        return [
            MyService::class => [
                'class' => MyService::class,
                'shared' => true,
                'arguments' => ['@db'],
                // Optional: add tag hints for lazy warmup, etc.
                'tags' => [['name' => 'lazy.background', 'priority' => 5]],
            ]
        ];
    }
    
    /**
     * Register runtime services and config defaults
     */
    public function register(): void
    {
        // Note: Service registration happens via static services() method
        // This method is for configuration merging and other runtime setup
        
        // Merge default configuration
        $this->mergeConfig('my-extension', require __DIR__.'/../config/config.php');
    }
    
    /**
     * Boot after all providers are registered
     */
    public function boot(): void
    {
        // Load routes
        $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
        
        // Load migrations
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        
        // Register console commands
        if ($this->runningInConsole()) {
            $this->commands([
                Commands\MyCommand::class,
            ]);
        }
        
        // Register extension metadata (if container has ExtensionManager)
        if ($this->app->has(\Glueful\Extensions\ExtensionManager::class)) {
            $this->app->get(\Glueful\Extensions\ExtensionManager::class)->registerMeta(self::class, [
                'slug' => 'my-extension',
                'name' => 'My Extension',
                'version' => '1.0.0',
                'description' => 'My awesome extension',
            ]);
        }
    }
}

> See also:
> - docs/implementation_plans/EXTENSIONS_CONTAINER_INTEGRATION.md (new container + extensions overview)
> - docs/implementation_plans/SERVICES_LOADER.md (DSL schema and ServicesLoader interface)

Available Helper Methods

  • loadRoutesFrom(string $path) - Load route definitions
  • loadMigrationsFrom(string $dir) - Register migration directory
  • mergeConfig(string $key, array $defaults) - Merge default configuration
  • loadMessageCatalogs(string $dir, string $domain = 'messages') - Load translation catalogs
  • mountStatic(string $mount, string $dir) - Serve static assets (dev/admin UIs)
  • commands(array $commands) - Register console commands
  • runningInConsole() - Check if running in CLI

CLI Metadata and Slugs

CLI commands (extensions:info|enable|disable) resolve an extension by its Composer package name (vendor/my-extension), its provider FQCN, or its slug — the last path segment of the package name (my-extension), matched case-insensitively. This comes from Composer discovery and needs no metadata registration.

The optional metadata registry is purely for display: a friendly name and description shown by extensions:info/list. (The version column comes from the installed Composer package version, falling back to registered metadata.)

  • Why register metadata
    • extensions:info shows your name/description instead of just the provider class.
    • It's optional — resolution and the version column work without it.
  • How to register
    • Call ExtensionManager::registerMeta(self::class, [...]) from your ServiceProvider’s boot():
if ($this->app->has(\Glueful\Extensions\ExtensionManager::class)) {
    $this->app->get(\Glueful\Extensions\ExtensionManager::class)->registerMeta(self::class, [
        'slug' => 'my-extension',            // required for CLI slug lookups
        'name' => 'My Extension',            // shown in lists/info
        'version' => '1.0.0',                // shown in lists/info
        'description' => 'What this does',   // shown in info
    ]);
}
  • Tips
    • Pick a short, URL‑safe slug (lowercase, hyphens OK). Users will type this in CLI commands.
    • Keep name human‑friendly; keep version in sync with your package’s extra.glueful.version.
    • You can include extra keys (e.g., categories, homepage, publisher)—the CLI ignores unknown keys safely.
    • As of framework 1.7.1, the framework discovers providers before booting them, so your registerMeta() call in boot() is available to the CLI immediately after startup.

Optional Interfaces

OrderedProvider

Control provider boot order:

use Glueful\Extensions\OrderedProvider;

class MyServiceProvider extends ServiceProvider implements OrderedProvider
{
    public function priority(): int
    {
        return 10; // Lower number boots first
    }
    
    public function bootAfter(): array
    {
        return [
            DatabaseServiceProvider::class,
            CacheServiceProvider::class,
        ];
    }
}

DeferrableProvider

Declare provided services (for future lazy loading):

use Glueful\Extensions\DeferrableProvider;

class MyServiceProvider extends ServiceProvider implements DeferrableProvider
{
    public function provides(): array
    {
        return [
            MyService::class,
            MyRepository::class,
        ];
    }
}

Configuration

Discovery is Composer-only, and config/extensions.php is a single enabled allow-list: an installed glueful-extension package does nothing until its provider FQCN appears here. There is no only / dev_only / disabled / local_path / scan_composer — the enabled list is the allow-list (empty = nothing loads).

config/extensions.php

<?php

return [
    'enabled' => [
        // Plain string FQCNs (no ::class) so `extensions:enable|disable` can edit
        // this list safely. Order is preserved; dependencies are reordered for you.
        'Glueful\\Blog\\BlogServiceProvider',
        'Vendor\\Analytics\\AnalyticsServiceProvider',
    ],
];

Notes:

  • String FQCNs, not ::class. The CLI edits this file as text, so entries must be plain strings. (A stray Foo::class literal simply won't match — it'll show as enabled-but-missing in extensions:list.)
  • Dev-only extensions: require them as Composer dev dependencies (require-dev) so they're absent in production, and list them in enabled. Resolution is environment-independent — there's no separate dev_only key.
  • Disabling: remove the entry from enabled (or run extensions:disable). There's no blacklist.
  • App's own providers (e.g. AppServiceProvider) live in config/serviceproviders.php under the same single enabled key; they're always loaded and are not gated by extensions.enabled.

Declaring requirements (extension authors)

{
    "type": "glueful-extension",
    "extra": {
        "glueful": {
            "provider": "Vendor\\MyExtension\\MyExtensionServiceProvider",
            "requires": {
                "glueful": ">=1.46.0",
                "extensions": ["Vendor\\Base\\BaseServiceProvider"]
            }
        }
    }
}
  • requires.glueful is a Composer version constraint (matched with composer/semver); a mismatch is a resolver error and the extension won't load.
  • requires.extensions lists provider FQCNs that must also be enabled — they are not auto-enabled; enabling an extension whose dependency isn't enabled is refused.

Command Reference

Discovery & Information

php glueful extensions:list              # Every discovered extension + state (✓ / ○ / ⚠); folds in the old `why`
php glueful extensions:info <name>       # Detail by package name, provider FQCN, or slug (e.g. `blog`)
php glueful extensions:diagnose          # Resolver errors, load order, and (prod) cache presence
php glueful extensions:summary           # System summary with performance metrics

Cache Management

php glueful extensions:cache             # Strict: compile the manifest; fails on any resolver error
php glueful extensions:clear             # Clear the compiled cache

Development Tools

php glueful create:extension <name>      # Scaffold a new extension (composer package + path repo)
php glueful extensions:enable <name>     # Add provider to `enabled` + recompile (dev only)
php glueful extensions:disable <name>    # Remove provider from `enabled` + recompile (dev only)

Note: enable/disable actually edit config/extensions.php and recompile the cache. They validate before writing — enabling an extension whose dependency isn't enabled (or disabling one another enabled extension depends on) is refused, leaving the config untouched. Both accept the package name, provider FQCN, or slug (slug is case-insensitive), and support --dry-run and --backup. They're disabled in production — there, edit config/extensions.php and run extensions:cache in your deploy step.

Example CLI Output

$ php glueful extensions:list
St  Package                Provider                                 Requires     Version
----------------------------------------------------------------------------------------------------
 glueful/blog            Glueful\Blog\BlogServiceProvider          >=1.46.0     v1.2.0
 vendor/analytics        Vendor\Analytics\AnalyticsServiceProvider *            v0.4.1
 (not installed)         Glueful\Shop\ShopServiceProvider          -            n/a

Summary:
Enabled:             1
Available (off):     1
Enabled-but-missing: 1
Cache used:          no

Real-World Example

Blog Extension

composer.json

{
    "name": "glueful/blog",
    "description": "Blog functionality for Glueful",
    "type": "glueful-extension",
    "require": {
        "php": "^8.3",
        "glueful/framework": "^1.0"
    },
    "autoload": {
        "psr-4": {
            "Glueful\\Blog\\": "src/"
        }
    },
    "extra": {
        "glueful": {
            "provider": "Glueful\\Blog\\BlogServiceProvider",
            "requires": {
                "glueful": ">=1.46.0",
                "extensions": []
            }
        }
    }
}

BlogServiceProvider.php

<?php

namespace Glueful\Blog;

use Glueful\Extensions\ServiceProvider;
use Glueful\Blog\Services\BlogService;
use Glueful\Blog\Controllers\BlogController;

class BlogServiceProvider extends ServiceProvider
{
    public static function services(): array
    {
        return [
            BlogService::class => [
                'class' => BlogService::class,
                'shared' => true,
                'arguments' => ['@db', '@cache']
            ]
        ];
    }
    
    public function register(): void
    {
        // Note: Service registration happens via static services() method
        // This method handles configuration merging and other setup
        
        // Merge default config
        $this->mergeConfig('blog', require __DIR__.'/../config/blog.php');
    }
    
    public function boot(): void
    {
        // Load routes
        $this->loadRoutesFrom(__DIR__.'/../routes/blog.php');
        
        // Load migrations
        $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
        
        // Register middleware
        $this->app->get('middleware.registry')->register([
            'blog.auth' => Middleware\BlogAuthMiddleware::class,
        ]);
        
        // Register console commands
        if ($this->runningInConsole()) {
            $this->commands([
                Commands\BlogInstallCommand::class,
            ]);
        }
        
        // Register metadata (if container has ExtensionManager)
        if ($this->app->has(\Glueful\Extensions\ExtensionManager::class)) {
            $this->app->get(\Glueful\Extensions\ExtensionManager::class)->registerMeta(self::class, [
                'slug' => 'blog',
                'name' => 'Blog Extension',
                'version' => '1.0.0',
                'description' => 'Blogging functionality for Glueful',
            ]);
        }
    }
}

routes/blog.php

<?php

use Glueful\Blog\Controllers\BlogController;
use Glueful\Routing\Router;

// $router is available from the container
$router->group(['prefix' => 'blog'], function (Router $router) {
    // Public routes
    $router->get('/', [BlogController::class, 'index'])->name('blog.index');
    $router->get('/posts/{slug}', [BlogController::class, 'show'])
           ->where('slug', '[a-z0-9\-]+')
           ->name('blog.show');
    
    // Admin routes
    $router->group(['middleware' => ['auth:api', 'role:admin']], function (Router $router) {
        $router->post('/posts', [BlogController::class, 'store'])->name('blog.store');
        $router->put('/posts/{id}', [BlogController::class, 'update'])->name('blog.update');
        $router->delete('/posts/{id}', [BlogController::class, 'destroy'])->name('blog.destroy');
    });
});

Migration Guide

From Old Extension System

If migrating from the old manifest.json-based system:

Old SystemNew System
manifest.jsoncomposer.json with extra.glueful.provider
BaseExtension classServiceProvider class
Extension.phpServiceProvider.php
Routes in manifestloadRoutesFrom() in provider
Migrations in manifestloadMigrationsFrom() in provider
Config publishingmergeConfig() in register()
Runtime service registrationStatic services() method + runtime fallback

Migration Steps

  1. Create a composer.json with type glueful-extension
  2. Convert your main extension class to extend ServiceProvider
  3. Add static services() method for production compilation
  4. Move initialization logic to register() and boot() methods
  5. Update route and migration loading to use helper methods
  6. Remove old manifest.json file

Service Registration Patterns

Extensions use static services definition for container compilation:

// Static services definition for container compilation
public static function services(): array 
{
    return [
        MyService::class => [
            'class' => MyService::class,
            'shared' => true,
            'arguments' => ['@db']
        ]
    ];
}

// Runtime configuration and setup
public function register(): void 
{
    // Handle configuration merging and other non-service setup
    $this->mergeConfig('my-extension', require __DIR__.'/../config/config.php');
}

public function boot(): void
{
    // Load routes, migrations, commands, etc.
    $this->loadRoutesFrom(__DIR__.'/../routes/routes.php');
    $this->loadMigrationsFrom(__DIR__.'/../database/migrations');
}

Performance

Metrics

AspectPerformance
Discovery Time< 5ms (cached)
Boot Time< 10ms for 10 extensions
Memory Usage~400KB per extension
Cache Hit100% in production

Optimization

Production Deployment

For optimal production performance, follow this deployment sequence:

# 1. Clear any existing cache
php glueful extensions:clear

# 2. Build extensions cache
php glueful extensions:cache

# 3. Compile DI container with extension services
php glueful di:container:compile

# 4. Clear application cache if needed
php glueful cache:clear

Production Caching

# Build cache for production
php glueful extensions:cache

# Cache location: bootstrap/cache/extensions.php
# Cache persists until explicitly cleared

Development Mode

  • Resolves live from Composer + the enabled list on each boot (no cache required).
  • The compiled cache, if present, expires after 5 seconds by default (configurable via EXTENSIONS_CACHE_TTL_DEV).
  • extensions:enable / extensions:disable edit the enabled list and recompile.
  • Production differs: it boots only from the compiled manifest and fails fast if it's missing — always run extensions:cache in your deploy step.

Troubleshooting

Common Issues

Extension Not Found

[Extensions] Extension provider not found ['provider' => 'MyExtension\\Provider']

Solution: Ensure the provider class exists and is properly autoloaded.

Invalid composer.json

[Extensions] Invalid composer.json in /path/to/extension/composer.json: Syntax error

Solution: Validate JSON syntax and ensure file is valid.

Provider Boot Failure

[Extensions] Provider failed during boot() ['provider' => 'MyExtension\\Provider', 'error' => '...']

Solution: Check provider's boot() method for errors. System continues with other providers.

Circular Dependencies

[Extensions] Circular dependency detected in provider bootAfter(), using priority fallback

Solution: Review bootAfter() dependencies to remove cycles.

Debug Commands

# Show all registered providers
php glueful extensions:list

# Check specific extension
php glueful extensions:info my-extension

# View system summary
php glueful extensions:summary

# Clear cache if having issues
php glueful extensions:clear

API-First Philosophy

Glueful follows an API-first approach:

  • No runtime views: Extensions don't use view templating
  • No asset publishing: No file copying into the app
  • Prebuilt frontends: SPAs are compiled and served as static assets
  • Configuration via files: No in-app configuration UIs

For frontend assets, use mountStatic() in development or serve via CDN in production.

Security

The extension system includes multiple security features:

  • Explicit allow-list — nothing loads unless its provider FQCN is in enabled; installing a package does not auto-activate it.
  • Strict production manifest — production boots only from the compiled cache (extensions:cache), so what loads is reviewed and deterministic.
  • Validate-before-writeenable/disable refuse to leave the config in a broken state (missing dependency, version mismatch, cycle).
  • Path traversal protection in mountStatic().
  • Graceful error handling — a provider failing to boot is logged, not fatal.
  • PSR-3 logging for security events.

Summary

The Glueful Extensions System provides:

  • Simple architecture - Just 3 core files, ~400 lines
  • Familiar patterns - ServiceProvider from Laravel/Symfony
  • Modern tooling - Composer packages, PSR-4 autoloading
  • High performance - Production caching, lazy loading
  • Developer friendly - Clear structure, good IDE support
  • Production ready - Error handling, logging, security

For more examples and advanced usage, see the framework documentation.