View Categories

PropEngine Plugin Development Guide

5 min read

PropEngine includes a first-party plugin architecture for extending the platform without editing core application files. Plugins can load service providers, register hooks and filters, publish Blade views, load translations, add routes, and integrate with admin workflows.

This Markdown file is the source documentation for the packaged product. A standalone offline HTML version is available at plugin-development.html. A BetterDocs-ready HTML fragment is available at betterdocs-plugin-development.html.

When to Build a Plugin #

Use a plugin when the feature should remain independent from the core product:

  • Optional tools such as calculators, AI helpers, importers, or marketing widgets
  • Integrations with external services
  • Custom property detail widgets
  • Custom admin pages or settings
  • Hooks that add frontend or backend UI without changing PropEngine core files

Do not use a plugin for changes that alter core database contracts, authentication, installer flow, license checks, or shared models unless the product owner explicitly approves that architecture.

Plugin Directory Structure #

plugins/
  my-plugin/
    plugin.json
    src/
      Providers/
        MyPluginServiceProvider.php
    config/
      config.php
    resources/
      lang/
        en/
          messages.php
        tr/
          messages.php
      views/
        widget.blade.php
    routes/
      web.php

Only plugin.json and the service provider declared in entry are required. Other folders are optional.

Plugin Manifest #

Every plugin must include plugin.json in its plugin root.

{
    "name": "My Plugin",
    "slug": "my-plugin",
    "version": "1.0.0",
    "description": "Adds a custom feature to PropEngine.",
    "author": "Your Name",
    "author_url": "https://example.com",
    "entry": "Plugins\\MyPlugin\\Providers\\MyPluginServiceProvider",
    "keywords": ["example", "property"],
    "requires": [],
    "settings": {}
}

Required fields:

  • name
  • slug
  • version
  • entry

The entry class should point to a Laravel service provider that can be autoloaded by the application.

Service Provider #

<?php

namespace Plugins\MyPlugin\Providers;

use App\Services\Plugin\HookManager;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;

class MyPluginServiceProvider extends ServiceProvider
{
    public function register(): void
    {
        $this->mergeConfigFrom(
            plugins_path('my-plugin/config/config.php'),
            'my-plugin'
        );
    }

    public function boot(): void
    {
        $this->loadViewsFrom(
            plugins_path('my-plugin/resources/views'),
            'my-plugin'
        );

        $this->loadTranslationsFrom(
            plugins_path('my-plugin/resources/lang'),
            'my-plugin'
        );

        $hookManager = app(HookManager::class);

        $hookManager->addAction('property.actions', function (array $data = []) {
            if (! isset($data['property'])) {
                return;
            }

            echo Blade::render(
                '<x-my-plugin::widget :property="$property" />',
                ['property' => $data['property']]
            );
        }, 20);
    }
}

Hook System #

Actions inject output into a hook point.

$hookManager->addAction('property.actions', function (array $data = []) {
    echo '<button type="button">Custom action</button>';
}, 20);

Blade templates execute action hooks with:

@hookaction('property.actions', ['property' => $property])

Filters modify a value and return it.

$hookManager->addFilter('property.price_display', function ($price, $currency) {
    if ($currency === 'TRY') {
        return number_format((float) $price, 0, ',', '.') . ' TRY';
    }

    return $price;
}, 10);

Blade templates can apply filters with:

@hookfilter('property.price_display', [$property->price, $property->price_currency])

Lower priority values run earlier. Use a higher priority when your plugin should run after other plugins.

Common Hook Points #

Hook Type Description
global.head Action Inside the page <head>
global.body_start Action Immediately after <body>
global.body_end Action Before </body>
property.actions Action Property detail action area near share/print controls
property.sidebar.after_form Action After the property inquiry/contact area
property.card.after Action After a property card
property.price_display Filter Property price formatting
footer.before Action Before the footer
admin.dashboard.widgets Action Admin dashboard widget area

Custom hook points can be added in Blade templates:

@hookaction('my-plugin.custom_point', ['data' => $variable])

Views #

After calling loadViewsFrom, plugin views can be rendered with the plugin namespace:

<x-my-plugin::widget :property="$property" />

or:

view('my-plugin::widget', ['property' => $property]);

Keep plugin views self-contained. Avoid depending on unpublished core partials unless the product owner documents them as stable.

Translations #

Plugins should keep their translations inside the plugin directory instead of writing to core language files.

Example file:

<?php

return [
    'button' => 'Open calculator',
    'title' => 'Mortgage Calculator',
];

Use namespaced translation keys:

{{ __('my-plugin::messages.button') }}

Add at least English translations. Add other languages when the plugin ships user-facing UI for those locales.

Configuration #

Plugin config should be namespaced by plugin slug.

<?php

return [
    'enabled' => env('MY_PLUGIN_ENABLED', true),
    'api_key' => env('MY_PLUGIN_API_KEY'),
];

Access config values with:

config('my-plugin.enabled')

Use unique environment variable prefixes. Do not reuse PropEngine core environment keys.

Admin Integration #

Plugins may add Filament pages or actions when needed. Keep admin navigation labels, groups, icons, and permissions consistent with the core admin panel.

Example:

use Filament\Facades\Filament;

public function boot(): void
{
    Filament::registerPages([
        \Plugins\MyPlugin\Filament\Pages\MyPluginSettings::class,
    ]);
}

For settings that buyers should manage, prefer a Filament settings page over requiring manual .env edits after installation.

Routes #

If a plugin needs web routes, load them from the service provider:

use Illuminate\Support\Facades\Route;

public function boot(): void
{
    Route::middleware(['web'])
        ->group(plugins_path('my-plugin/routes/web.php'));
}

Prefix plugin route names and URLs to avoid collisions.

External APIs #

Plugins that integrate external services must make API keys configurable. Third-party accounts, billing, quotas, and provider-side setup belong to the buyer unless a separate service agreement says otherwise.

Do not hard-code private keys, tokens, account IDs, or production credentials in plugin files.

Best Practices #

  • Keep plugins independent from core files.
  • Use the Plugins\YourPlugin\ namespace.
  • Prefix config, translation, route, event, and view names with the plugin slug.
  • Load translations from the plugin directory.
  • Escape output in Blade unless HTML is intentional and trusted.
  • Check that optional dependencies exist before using them.
  • Fail gracefully when a service is disabled or an API key is missing.
  • Avoid database schema changes unless the plugin ships clear migrations and rollback behavior.
  • Document every environment variable and admin setting.
  • Include tests for complex services or calculations.

Reference Plugins #

See these first-party plugins for working examples:

  • plugins/mortgage-calculator/
  • plugins/ai-assistant/

The Mortgage Calculator plugin demonstrates property detail hook integration, plugin translations, views, and admin-managed defaults.

Scroll to Top