Glueful Extensions
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
- Architecture
- Creating Extensions
- ServiceProvider Pattern
- Configuration
- Command Reference
- Real-World Example
- Migration Guide
- Performance
- Troubleshooting
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-extensionpackage is discovered by Composer but does nothing until its provider FQCN is inconfig/extensions.php→enabled. Useextensions:enable(or edit the list by hand). Runextensions:listto see which installed extensions are enabled (✓) vs available (○).
Architecture
Core Principles
- Composer-native discovery - Leverages existing package manager
- ServiceProvider pattern - Familiar to all PHP developers
- PHP config files - Type-safe and IDE-friendly
- Standard interfaces - Reuses Symfony/PSR interfaces
- Convention over configuration - Minimal boilerplate
Extension Types
Composer Package (Recommended)
// 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 definitionsloadMigrationsFrom(string $dir)- Register migration directorymergeConfig(string $key, array $defaults)- Merge default configurationloadMessageCatalogs(string $dir, string $domain = 'messages')- Load translation catalogsmountStatic(string $mount, string $dir)- Serve static assets (dev/admin UIs)commands(array $commands)- Register console commandsrunningInConsole()- 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:infoshows yourname/descriptioninstead 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’sboot():
- Call
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
namehuman‑friendly; keepversionin sync with your package’sextra.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 inboot()is available to the CLI immediately after startup.
- Pick a short, URL‑safe
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 strayFoo::classliteral simply won't match — it'll show asenabled-but-missinginextensions:list.) - Dev-only extensions: require them as Composer dev dependencies (
require-dev) so they're absent in production, and list them inenabled. Resolution is environment-independent — there's no separatedev_onlykey. - Disabling: remove the entry from
enabled(or runextensions:disable). There's no blacklist. - App's own providers (e.g.
AppServiceProvider) live inconfig/serviceproviders.phpunder the same singleenabledkey; they're always loaded and are not gated byextensions.enabled.
Declaring requirements (extension authors)
{
"type": "glueful-extension",
"extra": {
"glueful": {
"provider": "Vendor\\MyExtension\\MyExtensionServiceProvider",
"requires": {
"glueful": ">=1.46.0",
"extensions": ["Vendor\\Base\\BaseServiceProvider"]
}
}
}
}
requires.gluefulis a Composer version constraint (matched withcomposer/semver); a mismatch is a resolver error and the extension won't load.requires.extensionslists 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 System | New System |
|---|---|
manifest.json | composer.json with extra.glueful.provider |
BaseExtension class | ServiceProvider class |
Extension.php | ServiceProvider.php |
| Routes in manifest | loadRoutesFrom() in provider |
| Migrations in manifest | loadMigrationsFrom() in provider |
| Config publishing | mergeConfig() in register() |
| Runtime service registration | Static services() method + runtime fallback |
Migration Steps
- Create a
composer.jsonwith typeglueful-extension - Convert your main extension class to extend
ServiceProvider - Add static
services()method for production compilation - Move initialization logic to
register()andboot()methods - Update route and migration loading to use helper methods
- Remove old
manifest.jsonfile
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
| Aspect | Performance |
|---|---|
| Discovery Time | < 5ms (cached) |
| Boot Time | < 10ms for 10 extensions |
| Memory Usage | ~400KB per extension |
| Cache Hit | 100% 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
enabledlist 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:disableedit theenabledlist and recompile.- Production differs: it boots only from the compiled manifest and fails fast if it's missing — always run
extensions:cachein 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-write —
enable/disablerefuse 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.