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:
nameslugversionentry
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.