### Example Plugin Initialization Source: https://acellemail.com/developers/getting-started This is an example of running the `plugin:init` command and the expected success output. It confirms the plugin creation and indicates the location of its source files. ```bash $ php artisan plugin:init acmecorp/loyalty Plugin acmecorp/loyalty created & loaded! You can find its source files in the ./storage/app/plugins/acmecorp/loyalty folder ``` -------------------------------- ### PHPUnit Style Plugin Test Setup Source: https://acellemail.com/developers/testing Illustrates how to extend `PluginTestCase` in a PHPUnit test class for class-level setup. ```php // PHPUnit style — class-level class MyTest extends \Acelle\Ai\Tests\PluginTestCase { public function test_something(): void { /* ... */ } } ``` -------------------------------- ### Example Plugin Index JSON Source: https://acellemail.com/developers/plugin-architecture This JSON file lists plugins and their statuses. It's used for boot-time discovery. The `error` field indicates recent loading failures. ```json { "acelle/ai": { "status": "active" }, "acmecorp/loyalty": { "status": "inactive" }, "broken/sample": { "status": "active", "error": "Class \"Broken\\Sample\\ServiceProvider\" not found" } } ``` -------------------------------- ### Pest Style Plugin Test Setup Source: https://acellemail.com/developers/testing Demonstrates how to use the `PluginTestCase` in a Pest test file using the `uses` function. This approach is suitable for file-level setup. ```php // Pest style — file-level uses(\Acelle\Ai\Tests\PluginTestCase::class, RefreshDatabase::class); it('renders the chatbox bubble on app pages', function () { $this->get('/dashboard')->assertSee('data-ai-chatbox'); }); ``` -------------------------------- ### Sending Driver Service Provider Example Source: https://acellemail.com/developers/sending-drivers This is a full skeleton service provider for a sending-driver plugin, demonstrating registration and boot logic. ```php namespace MyVendor\Sending; use App\Library\Facades\Hook; use Illuminate\Support\ServiceProvider as Base; class ServiceProvider extends Base { public const PLUGIN_NAME = 'myvendor/sending'; // MUST match composer.json#name public function register(): void { // Translation file registration — see /developers/translations for // the full contract. MUST be in register(), never in boot(), or the // host's collect loop misses it. Hook::add('add_translation_file', fn () => [ 'id' => '#myvendor/sending_translation_file', 'plugin_name' => self::PLUGIN_NAME, 'file_title' => 'Translation for myvendor/sending plugin', 'translation_folder' => storage_path('app/data/plugins/myvendor/sending/lang/'), 'translation_prefix' => 'myvendor', 'file_name' => 'messages.php', 'master_translation_file' => realpath(__DIR__ . '/../resources/lang/en/messages.php'), ]); } public function boot(): void { // (1) View namespace + plugin's own routes. $this->loadViewsFrom(__DIR__ . '/../resources/views', 'myvendor'); $this->loadRoutesFrom(__DIR__ . '/../routes.php'); // (2) The single REGISTRY hook that the host's SendingServerServiceProvider // collects in its app->booted() phase. The closure body runs lazily // at collect time — route() resolves correctly because all providers // have already booted. Hook::add('register_sending_server_driver', fn () => [ 'type' => MyVendorDriver::TYPE, // slug -> DriverRegistry 'driver' => MyVendorDriver::class, 'config_keys' => ['my_api_key', 'my_region'], // -> JSON config column 'name' => 'My Vendor', 'description' => 'Send via My Vendor API', 'icon_url' => route('plugin.myvendor.sending.icon'), // create_url omitted -> main app derives from `type` ]); // (3) Lifecycle — only if the plugin needs cleanup on uninstall. Hook::on('delete_plugin_' . self::PLUGIN_NAME, function () { \App\Model\SendingServer::where('type', MyVendorDriver::TYPE)->forceDelete(); }); } } ``` -------------------------------- ### Page-level Slot Example Source: https://acellemail.com/developers/ui-injection Demonstrates how to collect and render fragments for a specific page slot. ```PHP @foreach (array_filter("App\Library\Facades\Hook"::collect('page.maillist.show.body', [$list])) as $html) {!! $html !!} @endforeach ``` -------------------------------- ### Testing Event Hooks: Awarding Welcome Points Source: https://acellemail.com/developers/testing This example demonstrates testing an 'customer_added' event hook. It creates a customer, fires the event, and asserts that the correct number of loyalty points are awarded. ```php it('awards welcome points when a customer is added', function () { $customer = Customer::factory()->create(); Hook::fire('customer_added', [$customer]); expect(LoyaltyAccount::where('customer_id', $customer->id)->first()->points) ->toBe(100); }); ``` -------------------------------- ### Plugin Development: File Structure Setup Source: https://acellemail.com/developers/sending-drivers This sequence of commands outlines the initial steps for creating a new sending driver plugin by copying and renaming an existing plugin's directory structure. It includes editing the composer.json file, renaming the driver class, and updating the ServiceProvider. ```bash cp -r storage/app/plugins/rencontru/postal storage/app/plugins// cd storage/app/plugins// # 1. Edit composer.json — name, namespace, autoload.psr-4 # 2. Rename src/PostalDriver.php → Driver.php, update class name + TYPE # 3. Update src/ServiceProvider.php — PLUGIN_NAME, view namespace, hook payload # 4. Adjust resources/views/sending-servers/_fields_connection.blade.php # 5. Adjust resources/lang/en/messages.php # 6. Replace the Postal HTTP client under src/Postal/* with your vendor client ``` -------------------------------- ### Plugin Registration Process Source: https://acellemail.com/developers/plugin-architecture The `Plugin::register($name)` method handles the initial setup of a plugin, including reading composer.json, updating the database and index.json, loading service providers, and materializing translation files. ```php Plugin::register($name); ``` -------------------------------- ### Plugin Registration Method Source: https://acellemail.com/developers/lifecycle The `register` method in `app/Model/Plugin.php` handles the initial installation of a plugin. It reads composer.json, inserts a DB row, writes to the master file, loads the service provider, and materializes translations and assets. ```php public static function register($name) { // ... implementation details ... } ``` -------------------------------- ### Publish Plugin Assets Source: https://acellemail.com/developers/plugin-architecture This method is used within a plugin's service provider to copy bundled assets to the host's public directory upon installation. ```php $this->publishes([...], 'plugin') ``` -------------------------------- ### PluginTestCase for AcelleMail AI Plugin Source: https://acellemail.com/developers/testing This class extends the base `TestCase` to provide setup for the 'acelle/ai' plugin. It ensures the plugin is seeded as active before each test and resets the plugin gate cache to prevent stale data issues. ```php namespace Acelle\Ai\Tests; use App\Model\Plugin; use Tests\TestCase as BaseTestCase; class PluginTestCase extends BaseTestCase { protected function setUp(): void { parent::setUp(); $this->seedAcellAiPluginActive(); // Bust the per-request memoisation in PluginGate so each test // observes the actual DB state set above (or set inline by the // test). Without this the second test onward sees a stale // cached value from the first test's boot. if (class_exists(\Acelle\Ai\Support\PluginGate::class)) { \Acelle\Ai\Support\PluginGate::resetCache(); } } protected function seedAcellAiPluginActive(): void { $plugin = Plugin::query()->where('name', 'acelle/ai')->first(); if (! $plugin) { $plugin = new Plugin(); $plugin->name = 'acelle/ai'; $plugin->title = 'AcelleMail AI'; $plugin->version = '1.0.0'; } $plugin->status = 'active'; $plugin->save(); } } ``` -------------------------------- ### Initialize a New Plugin Source: https://acellemail.com/developers/getting-started Run this command from the application root to create a new plugin scaffold. Replace `{vendor}/{name}` with your desired vendor and plugin name. ```bash php artisan plugin:init {vendor}/{name} ``` -------------------------------- ### Plugin Boot-and-Load Flow Source: https://acellemail.com/developers/plugin-architecture Illustrates the sequence of events when the application boots and loads plugins. This flow includes service provider registration and translation loading. ```text application boots └─ AppServiceProvider::boot() └─ Plugin::autoloadWithoutDbQuery() └─ reads storage/app/plugins/index.json └─ for each entry: └─ Plugin::loadPluginByName($name) ├─ reads plugin's composer.json ├─ registers PSR-4 prefixes via Composer\Autoload\ClassLoader └─ App::register() ├─ ServiceProvider::register() (early — translations registered here) └─ ServiceProvider::boot() (late — routes, hooks, views, publishes) └─ AppServiceProvider then collects every add_translation_file hook and calls $this->loadTranslationsFrom() once per plugin. ``` -------------------------------- ### Get Errored Plugin Names Source: https://acellemail.com/developers/lifecycle The `getErroredPluginNames()` method reads the master file and returns a list of plugin names that have an associated error. ```php getErroredPluginNames() ``` -------------------------------- ### Loading Routes and Views in `boot()` Source: https://acellemail.com/developers/plugin-architecture In the `boot()` method, load plugin-specific routes and views using `$this->loadRoutesFrom()` and `$this->loadViewsFrom()` respectively. ```php $this->loadRoutesFrom(__DIR__ . '/../routes/web.php'); $this->loadViewsFrom(__DIR__ . '/../resources/views', 'myplugin'); ``` -------------------------------- ### Conditional Maillist Redirect Source: https://acellemail.com/developers/hook-system This example shows a plugin intercepting a maillist redirect. If specific conditions are met, it returns a custom redirect; otherwise, it allows the default redirect (or no redirect if null). ```php Hook::modify('page.maillist.show.redirect', function ($redirect, $list, $request) { if (MyPlugin::shouldRedirect($list, $request->user())) { return redirect()->route('my_plugin.custom_page', $list->uid); } return $redirect; // null means "do not redirect" }); ``` ```php $redirect = Hook::filter('page.maillist.show.redirect', null, [$list, $request]); if ($redirect) { return $redirect; } // continue rendering ``` -------------------------------- ### Logging Vendor Calls with Intent UID Source: https://acellemail.com/developers/payment-gateways Log errors from payment gateway calls, including the intent UID and the error message. This is crucial for debugging. Adding a 'gateway' tag is recommended for multi-gateway setups. ```PHP \Log::error('Paddle checkout: createTransaction failed', ['intent_uid' => $intent->uid, 'error' => $e->getMessage()]); ``` -------------------------------- ### Plugin-Isolated Factory Definition Source: https://acellemail.com/developers/testing Example of a plugin-specific Eloquent factory located within the plugin's test fixtures directory. This factory is namespaced under the plugin's PSR-4 prefix and autoloaded by the host application. ```php namespace Acmecorp\Loyalty\Tests\Fixtures; use Acmecorp\Loyalty\Models\LoyaltyAccount; use Illuminate\Database\Eloquent\Factories\Factory; class LoyaltyAccountFactory extends Factory { protected $model = LoyaltyAccount::class; public function definition(): array { return [ 'customer_id' => \App\Model\Customer::factory(), 'points' => $this->faker->numberBetween(0, 10_000), ]; } } ``` -------------------------------- ### Publishing Assets in `boot()` Source: https://acellemail.com/developers/plugin-architecture Use `$this->publishes()` within the `boot()` method to make plugin assets available for publishing, typically tagged with 'plugin'. ```php $this->publishes([ __DIR__.'/../public' => public_path('vendor/myplugin'), ], 'plugin'); ``` -------------------------------- ### Registering Body Assets with Layout and Context Source: https://acellemail.com/developers/ui-injection This snippet demonstrates adding assets just before the closing body tag. It includes a check for plugin availability and uses layout and context arguments to render body assets. ```php // In acelle/ai's ServiceProvider::boot() Hook::add('layout.body.before_close', function ($layout = 'app', array $context = []) { if (! $this->aiPluginAvailable()) { return null; } return view('ai::partials.body_assets', [ 'layout' => $layout, 'context' => $context, ])->render(); }); ``` -------------------------------- ### Asset Publishing for Plugins Source: https://acellemail.com/developers/ui-injection Demonstrates how to publish plugin assets like CSS, JS, and images using Laravel's standard API. ```PHP // In ServiceProvider::boot() $this->publishes([ __DIR__ . '/../resources/assets' => public_path('plugins/acmecorp/loyalty'), ], 'plugin'); ``` -------------------------------- ### Explicitly Pass Currency to Vendor Source: https://acellemail.com/developers/payment-gateways When initializing a payment, always pass the currency explicitly. This prevents silent failures due to default currency interpretations by the vendor. The example shows how to validate and pass the ISO4217 numeric code. ```php // ❌ Silent default $payload = ['Amount' => $amountMinor, 'OrderId' => $intentUid]; // ✅ Explicit, fail-loud on unknown currency private const ISO4217_NUMERIC = ['RUB' => '643', 'USD' => '840', 'EUR' => '978', /* ... */]; if (!isset(self::ISO4217_NUMERIC[$iso = strtoupper($currency)])) { throw new \InvalidArgumentException( "Unsupported currency '{$currency}'. Supported: " . implode(', ', array_keys(self::ISO4217_NUMERIC)) ); } $payload['Currency'] = self::ISO4217_NUMERIC[$iso]; ``` -------------------------------- ### Registering a Plugin Source: https://acellemail.com/developers/lifecycle Programmatically register a plugin by name. This action writes a database row, creates a master file entry, loads the service provider, and performs language dumping and vendor publishing. ```php Plugin::register($name) ``` -------------------------------- ### Fire Hook Source: https://acellemail.com/developers/plugin-architecture This demonstrates firing a hook with a specific name and optional parameters. Listeners registered to this hook will be executed. ```php Hook::fire('activate_plugin_'.$name) ``` -------------------------------- ### Generated Plugin Directory Structure Source: https://acellemail.com/developers/getting-started This tree shows the files and directories generated by the `plugin:init` command for the `acmecorp/loyalty` plugin. It includes configuration, routes, migrations, resources, and source files. ```text storage/app/plugins/acmecorp/loyalty/ ├── build.sh ├── composer.json ├── icon.svg ├── routes.php ├── database/ │ └── migrations/ │ └── 2000_01_01_000000_create_acmecorp_loyalty_settings_table.php ├── resources/ │ ├── lang/ │ │ └── en/ │ │ └── messages.php │ └── views/ │ └── index.blade.php └── src/ ├── Controllers/ │ └── DashboardController.php ├── Models/ │ └── Setting.php └── ServiceProvider.php ``` -------------------------------- ### Master File Entry - After Boot Failure Source: https://acellemail.com/developers/lifecycle If a plugin fails to boot, an 'error' field is added to its entry in the master file, indicating the failure. This state persists until cleared by a successful activation or a disable operation. ```json { "acmecorp/loyalty": { "status": "active", "error": "Class \"…\" not found" } } ``` -------------------------------- ### Run Plugin Lifecycle Test Source: https://acellemail.com/developers/testing This snippet demonstrates a full integration test for a plugin, covering activation, feature execution, and cleanup/rollback on deletion. It verifies database table creation and deletion. ```php it('runs migration on activate and rolls back on delete', function () { $plugin = Plugin::register('acmecorp/loyalty'); expect($plugin->status)->toBe('inactive'); expect(Schema::hasTable('acmecorp_loyalty_accounts'))->toBeFalse(); $plugin->activate(); expect($plugin->status)->toBe('active'); expect(Schema::hasTable('acmecorp_loyalty_accounts'))->toBeTrue(); $plugin->deleteAndCleanup(); expect(Schema::hasTable('acmecorp_loyalty_accounts'))->toBeFalse(); expect(Plugin::where('name', 'acmecorp/loyalty')->exists())->toBeFalse(); }); ``` -------------------------------- ### Setting Plugin Icon URL in `boot()` Source: https://acellemail.com/developers/plugin-architecture Set the icon URL for the plugin using `Hook::set()` in the `boot()` method. ```php Hook::set('icon_url_{vendor}/{name}', $this->iconPath()); ``` -------------------------------- ### Master File Entry - After Activate Source: https://acellemail.com/developers/lifecycle Upon activation, the plugin's status in the master file is updated to 'active'. ```json { "acmecorp/loyalty": { "status": "active" } } ``` -------------------------------- ### Registering Lifecycle Event Listeners in `boot()` Source: https://acellemail.com/developers/plugin-architecture Register listeners for plugin lifecycle events like activation and deletion using `Hook::on()` in the `boot()` method. ```php Hook::on('activate_plugin_{name}', function () { // Activation logic }); Hook::on('delete_plugin_{name}', function () { // Deletion logic }); ``` -------------------------------- ### Testing Driver Class Loading Source: https://acellemail.com/developers/sending-drivers This command tests if the driver class can be instantiated without syntax errors. It requires the Artisan tinker command and a mock SendingServer configuration. ```php php artisan tinker --execute="new MyVendor\Sending\MyVendorDriver(new App\Model\SendingServer(['type' => 'myvendor-api']))" ``` -------------------------------- ### Plugin Migration File Structure Source: https://acellemail.com/developers/database-models Illustrates the directory structure for plugin-specific database migrations. Migrations are stored within the plugin's folder, separate from the host application's migrations. ```text storage/app/plugins/acmecorp/loyalty/ └── database/ └── migrations/ └── 2000_01_01_000000_create_acmecorp_loyalty_settings_table.php ``` -------------------------------- ### Performing a BEHAVIOR Source: https://acellemail.com/developers/hook-system The core application calls `perform` to execute the currently registered behavior for a given name. This will execute either the plugin's override or the core's default. ```php $currentJob = Hook::perform('dispatch_list_import_job', [$list, $filepath]); dispatch($currentJob); ``` -------------------------------- ### Registering Translation Files in `register()` Source: https://acellemail.com/developers/translations Use the `add_translation_file` hook within the `register()` method of your service provider to ensure translations are collected by the host application. Registering in `boot()` will cause translations to be ignored. ```php Hook::add('add_translation_file', function () { return [ 'id' => 'my_plugin_messages', 'plugin_name' => 'my_vendor/my_plugin', 'file_title' => 'My Plugin Messages', 'translation_folder' => __DIR__ . '/../resources/lang', 'file_name' => 'messages.php', 'master_translation_file' => __DIR__ . '/../resources/lang/en/messages.php', ]; }); ``` -------------------------------- ### Register Plugin via Tinker Source: https://acellemail.com/developers/sending-drivers Registers a new plugin in the database. This is the first step in activating a custom sending driver. ```php php artisan tinker --execute="App\Model\Plugin::register('vendor/name')" ``` -------------------------------- ### Activate Plugin Programmatically Source: https://acellemail.com/developers/getting-started Use this snippet within Tinker to activate a plugin and run its migration. It updates the plugin's status and the master file. ```php $p = \App\Model\Plugin::getByName('acmecorp/loyalty'); >>> $p->activate(); => ✓ status: active, migration ran, master file updated ``` -------------------------------- ### Registering Head Assets with Layout and Context Source: https://acellemail.com/developers/ui-injection This snippet shows how to add assets to the head of a layout. It checks for plugin availability and conditionally renders head assets based on the provided layout and context. ```php // Plugin (in ServiceProvider::boot()) Hook::add('layout.head.assets', function ($layout = 'app', array $context = []) { if (! $this->aiPluginAvailable()) { return null; } return view('ai::partials.head_assets', [ 'layout' => $layout, 'context' => $context, ])->render(); }); ``` -------------------------------- ### Master File Entry - Before Register Source: https://acellemail.com/developers/lifecycle Before a plugin is registered, its entry is absent from the master file. ```json {} ``` -------------------------------- ### Verify Config Keys Auto-Routing via Tinker Source: https://acellemail.com/developers/sending-drivers Ensures that configuration keys defined by the plugin are correctly recognized and routed by the host system. ```php php artisan tinker --execute=" $keys = App\Model\SendingServer::getVendorConfigKeys(); echo in_array('my_api_key', $keys, true) ? 'YES' : 'NO'; " ``` -------------------------------- ### Artisan Commands for Plugin Testing Source: https://acellemail.com/developers/testing Common Artisan commands for running plugin tests locally. This includes commands for running all tests, specific plugin test suites, individual test files, and filtering tests by description. ```shell # Run every plugin's testsuite plus host Unit + Feature php artisan test # Run just one plugin's tests php artisan test --testsuite="Plugin: acmecorp/loyalty" # Run a single test by file path php artisan test storage/app/plugins/acmecorp/loyalty/tests/Feature/AwardsTest.php # Run a single Pest test by description php artisan test --filter "awards welcome points" ``` -------------------------------- ### Register Hook Listener (Registry) Source: https://acellemail.com/developers/plugin-architecture This demonstrates registering a listener for a REGISTRY hook. Multiple listeners can be registered for the same hook name, and they will be iterated over. ```php Hook::listen('admin.sidebar.groups', ...) ``` -------------------------------- ### Register Sending Server Driver (REGISTRY) Source: https://acellemail.com/developers/hook-system Plugins can register new sending server drivers by adding metadata to a named list. The core application collects all registered drivers to populate available options. ```php Hook::add('register_sending_server_driver', fn() => [ 'type' => 'postal', 'driver' => '\AcmeCorp\Postal\Driver', 'name' => 'Postal MTA', ]); ``` ```php foreach (Hook::collect('register_sending_server_driver') as $meta) { $drivers[$meta['type']] = $meta['driver']; } ``` -------------------------------- ### Referencing Published Plugin Assets Source: https://acellemail.com/developers/ui-injection Shows how to reference published plugin assets within the plugin's partials. ```HTML ``` -------------------------------- ### Master File Entry - After Register Source: https://acellemail.com/developers/lifecycle After registration, a plugin entry appears in the master file with its status set to 'inactive'. ```json { "acmecorp/loyalty": { "status": "inactive" } } ``` -------------------------------- ### Plugin State Transitions via Tinker Source: https://acellemail.com/developers/lifecycle Manage plugin states programmatically using Artisan Tinker. Access plugin models and invoke methods like activate, disable, and delete. ```php >>> $p = \App\Model\Plugin::getByName('acmecorp/loyalty'); >>> $p->activate(); // → active, runs migration via activate hook >>> $p->disable(); // → inactive, status flip only >>> $p->deleteAndCleanup($keepData = true); // → preserve customer-facing tables ``` -------------------------------- ### Registering an EVENT listener Source: https://acellemail.com/developers/hook-system Plugins can listen for specific events fired by the core application and execute custom logic. This is useful for side effects like sending notifications or awarding points. ```php Hook::on('customer_added', function ($customer) { LoyaltyPoints::award($customer, 100, 'welcome_bonus'); }); ``` -------------------------------- ### Activate Plugin via Tinker Source: https://acellemail.com/developers/sending-drivers Activates a registered plugin, triggering the 'activate_plugin_' hook. This makes the driver available for use. ```php php artisan tinker --execute="App\Model\Plugin::where('name','vendor/name')->first()->activate()" ``` -------------------------------- ### Running AcelleMail AI Plugin Tests with Pest Source: https://acellemail.com/developers/showcase The host's phpunit.xml registers the plugin's testsuite. Use the provided command to run the full suite in parallel with the host's own tests. ```bash ./vendor/bin/pest --testsuite="Plugin: acelle/ai" ``` -------------------------------- ### Publishing Plugin Assets Source: https://acellemail.com/developers/showcase Assets like CSS and JavaScript are published to the public directory using the 'vendor:publish' command. This command ensures that plugin-specific assets are accessible by the web server. ```bash vendor:publish --tag=plugin --force ``` -------------------------------- ### Setting a BEHAVIOR if not already set Source: https://acellemail.com/developers/hook-system The core application can register a default behavior using `setIfEmpty`, which only takes effect if no plugin has already overridden it using `set`. ```php Hook::setIfEmpty('dispatch_list_import_job', function ($list, $filepath) use ($request) { return new ImportJob($list, $filepath, $request); }); ``` -------------------------------- ### Run Plugin Migrations on Activation Source: https://acellemail.com/developers/database-models This code snippet shows how to hook into the plugin activation event to run the plugin's database migrations. It uses `Artisan::call` to execute the `migrate` command specifically for the plugin's migration path. ```php // Run plugin migrations when the plugin is activated. Hook::on('activate_plugin_acmecorp/loyalty', function () { \Artisan::call('migrate', [ '--path' => 'storage/app/plugins/acmecorp/loyalty/database/migrations', '--force' => true, ]); }); ``` -------------------------------- ### Register Hook Listener (Behavior) Source: https://acellemail.com/developers/plugin-architecture This demonstrates registering a listener for a BEHAVIOR hook. BEHAVIOR hooks are exclusive, and attempting to set the same hook name twice will result in an error. ```php Hook::set('layout.head.assets', ...) ```