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:

  1. Registers Resources: Automatically maps views/, lang/, routes/, and database/migrations/ using conventional paths.
  2. Boots Logic: Includes the plugin.php file, which serves as the primary execution entry point.
  3. Autoloads Classes: The ExtensionAutoloader loads PSR-4 classes under src/ via the Plugins\{StudlySlug} namespace without per-plugin Composer mappings.
  4. 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

  1. Create the directory: public/plugins/my-plugin.
  2. Define metadata: Create config.php to tell ValPress about your plugin.
  3. Register hooks: Use plugin.php to add menu items or modify content.
  4. 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 RefreshDatabase trait 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_settings instead of settings).
  • Use the Facades: Use ValPress::registerViews() and ValPress::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/Core or resources/views/admin. Use hooks instead.
  • Hardcoding URLs: Always use asset() or route() 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 in composer.json or registered via the PluginManager.
  • Submitting Without Tests: Uploading a Marketplace submission with no tests/ directory, or with failing tests, will delay or block approval. Run php artisan test locally 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.