Plugins
Overview
ValPress Plugins are modular extensions that enhance the functionality of the CMS without modifying the core application files. Built on Laravel's powerful architecture and an event-driven hook system, plugins allow developers to register new routes, controllers, views, database migrations, and admin interfaces seamlessly.
Why It Matters
- Decoupling: Keep custom logic separate from the core engine to ensure easy updates and maintenance.
- Portability: Share functionality across different ValPress installations by packaging it as a single directory.
- Extensibility: Hook into any part of the request lifecycle to modify behavior or inject content.
- Standardization: Use familiar Laravel patterns (Controllers, Migrations, Blade) within a CMS context.
How It Works
ValPress manages plugins through the App\Core\PluginManager. During the application boot process, the manager scans the public/plugins directory and
identifies active plugins stored in the options table.
For each active plugin, the manager:
- Registers Resources: Automatically maps
views/,lang/,routes/, anddatabase/migrations/using conventional paths. - Boots Logic: Includes the
plugin.phpfile, which serves as the primary execution entry point. - Autoloads Classes: The
ExtensionAutoloaderloads PSR-4 classes undersrc/via thePlugins\{StudlySlug}namespace without per-plugin Composer mappings. - Lifecycle Management: Triggers specific actions during activation, deactivation, and uninstallation to handle setup or cleanup (e.g., running migrations).
Usage
Plugin Directory Structure
A standard ValPress plugin is located in public/plugins/{slug}/. While only plugin.php and config.php are strictly required for metadata, a
production-ready plugin typically follows this layout:
public/plugins/my-custom-plugin/
├── config.php # Metadata and configuration
├── plugin.php # Main entry point (hooks, logic)
├── screenshot.png # Admin UI preview (1200x900)
├── thumbnail.png # Admin UI list icon (300x300)
├── routes/
│ └── web.php # Plugin-specific routes
├── src/
│ ├── Plugin.php # Optional: ValidatesUpdate for update pipeline
│ ├── Controllers/ # Logic for your routes
│ ├── Models/ # Database entities
│ └── Middleware/ # Request filtering
├── views/ # Blade templates
├── database/
│ └── migrations/ # Schema changes
├── lang/ # Localization files
├── res/ # Static assets (CSS, JS, Images)
└── tests/ # PHPUnit tests (required for Marketplace publication)
Creating Your First Plugin
- Create the directory:
public/plugins/my-plugin. - Define metadata: Create
config.phpto tell ValPress about your plugin. - Register hooks: Use
plugin.phpto add menu items or modify content. - Activate: Navigate to Admin > Plugins and click Activate.
Code Examples
1. The Configuration File (config.php)
This file defines the identity and requirements of your plugin.
<?php
return [
'plugin_slug' => 'my-plugin',
'name' => 'My Custom Plugin',
'plugin_url' => 'https://example.com/plugins/my-plugin',
'version' => '1.0.0',
'short_description' => 'A brief overview of what this plugin does.',
'long_description' => 'A detailed explanation for the plugin details screen.',
'author' => 'Your Name',
'author_url' => 'https://example.com',
'compat_min' => '1.0.0', // Minimum ValPress version required
'php_min' => '8.2', // Minimum PHP version required
'screenshot' => 'screenshot.png',
'thumbnail' => 'thumbnail.png',
];
2. The Plugin Entry Point (plugin.php)
The plugin.php file is where you register your hooks and initialize your logic.
<?php
use App\Core\Facades\AdminMenu;
use App\Core\Facades\ScriptManager;
// 1. Register a menu item in the Admin Dashboard
add_filter('valpress_admin_menu_items', function ($items) {
$items['my-plugin-settings'] = [
'id' => 'my-plugin-settings',
'title' => __('Plugin Settings'),
'url' => fn() => route('admin.my_plugin.settings'),
'icon' => 'bi-gear-fill',
'order' => 100,
'parent' => 'settings', // Nest under the Settings menu
];
return $items;
});
// 2. Enqueue custom assets for the admin area
add_action('admin_enqueue_scripts', function () {
ScriptManager::register('my-plugin-js', asset('plugins/my-plugin/res/admin.js'));
ScriptManager::enqueue('my-plugin-js');
});
// 3. Filter post content
add_filter('the_content', function ($content) {
if (is_single()) {
$content .= view('my-plugin::cta-banner')->render();
}
return $content;
});
3. Registering Routes and Controllers
ValPress automatically loads routes from routes/web.php and routes/api.php.
routes/web.php:
use Illuminate\Support\Facades\Route;
use MyPlugin\Controllers\SettingsController;
Route::middleware(['web', 'auth', 'admin'])
->prefix('admin/my-plugin')
->group(function () {
Route::get('/settings', [SettingsController::class, 'index'])->name('admin.my_plugin.settings');
});
Advanced Usage
Update validation
Plugins can participate in the Update Center pipeline by implementing ValidatesUpdate on src/Plugin.php. This lets your plugin block or warn on core updates and self-updates when prerequisites are not met.
php artisan vp:make-plugin-validation my-plugin
See Plugin Update Validation, Repair Actions, and Update Pipeline. A reference implementation is available in the CMS repo at docs/1.x/examples/valpress-update-example/.
Plugin Lifecycle Hooks
Handle activation and cleanup logic using dynamic hook tags based on your plugin's slug.
// Triggered when the user clicks 'Activate'
add_action('activate_my-plugin', function () {
// Run migrations, set default options, etc.
Artisan::call('migrate', ['--force' => true]);
});
// Triggered when the user clicks 'Deactivate'
add_action('deactivate_my-plugin', function () {
// Flush caches or disable specific features
});
// Triggered when the user clicks 'Uninstall' (and confirms deletion)
add_action('uninstall_my-plugin', function () {
// DELETE database tables and options
Schema::dropIfExists('my_plugin_table');
delete_option('my_plugin_settings');
});
Registering Migrations Manually
While the PluginManager handles conventional paths, you can manually register migrations if you use a custom structure:
add_action('valpress_init', function() {
ValPress::registerMigrations(__DIR__ . '/custom-database-path');
});
Marketplace Publication Requirements
Plugins submitted to the ValPress Marketplace must include an automated test suite. During editorial review, editors verify that your package contains PHPUnit tests and that they pass against a ValPress installation. Submissions without tests, or with failing tests, will not be approved for publication.
Where to Place Tests
Place your plugin tests under:
public/plugins/{slug}/tests/
ValPress discovers these directories automatically via PSR-4 mappings in composer.json. Use namespaces that match your plugin slug (for example,
Plugins\MyPlugin\Tests\).
What to Cover
At minimum, your test suite should demonstrate that the plugin works as described and does not introduce regressions. Prioritize coverage for:
- Core functionality: Routes, controllers, services, and any custom business logic.
- Hooks: Actions and filters your plugin registers (for example,
the_content,valpress_admin_menu_items). - Database changes: Migrations, models, and data persistence (use the
RefreshDatabasetrait where appropriate). - Authorization: Admin endpoints and settings screens must reject unauthenticated or unauthorized requests.
Verify Before You Submit
Run your plugin tests from the ValPress project root before packaging your ZIP for upload:
php artisan test public/plugins/my-custom-plugin/tests
You can also filter by namespace or test name:
php artisan test --filter MyPlugin
All tests must pass. For test database setup, PHPUnit configuration, and additional examples, see Testing & Test Database Setup.
Example Plugin Test
<?php // public/plugins/my-custom-plugin/tests/SettingsRouteTest.php
namespace Plugins\MyCustomPlugin\Tests;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;
class SettingsRouteTest extends TestCase
{
use RefreshDatabase;
public function test_admin_can_view_plugin_settings_page(): void
{
$admin = User::factory()->create(['role' => 'admin']);
$this->actingAs($admin);
$response = $this->get(route('admin.my_plugin.settings'));
$response->assertStatus(200);
}
public function test_guest_cannot_view_plugin_settings_page(): void
{
$response = $this->get(route('admin.my_plugin.settings'));
$response->assertRedirect();
}
}
Include the tests/ directory in your submission ZIP. Marketplace reviewers expect tests to be part of the distributed package, not maintained separately.
Best Practices
- Ship Tests with Your Plugin: Treat automated tests as a required deliverable, especially if you plan to publish on the Marketplace.
- Namespace Your Assets: Always prefix your asset handles and option keys (e.g.,
my_plugin_settingsinstead ofsettings). - Use the Facades: Use
ValPress::registerViews()andValPress::registerTranslations()to ensure your resources are correctly namespaced (e.g.,view('my-plugin::template')). - Respect Priorities: Use appropriate priorities (default is 10) to ensure your hooks run in the correct order relative to other plugins.
- Sanitize and Validate: Since plugins run within the main application, always validate user input and use Laravel's CSRF protection.
Common Mistakes
- Directly Modifying Core: Never edit files in
app/Coreorresources/views/admin. Use hooks instead. - Hardcoding URLs: Always use
asset()orroute()helpers to generate URLs, ensuring compatibility with different server configurations. - Missing Permissions Checks: When adding admin routes or menu items, always ensure the user has the required capability (e.g.,
manage_options). - Forgetting the Namespace: If your plugin includes custom classes in
src/, ensure they follow PSR-4 and are correctly namespaced incomposer.jsonor registered via thePluginManager. - Submitting Without Tests: Uploading a Marketplace submission with no
tests/directory, or with failing tests, will delay or block approval. Runphp artisan testlocally before every upload.
Summary
Plugins are the lifeblood of ValPress extensibility. By leveraging the PluginManager and the hook system, you can build complex, integrated features while
maintaining a clean, updateable core. Whether you're adding a simple content filter or a full-featured e-commerce module, the plugin architecture provides the
tools needed for professional Laravel-based development. If you intend to distribute your plugin via the Marketplace, include a passing PHPUnit test suite from
the start — tests are a publication requirement, not an optional extra.