Všetky príručky

Vývoj pluginov

Ako postaviť plugin pre Ecommio: štruktúra, manifest, validácia nastavení, tajomstvá a registrácia.

Pre: Vývojár

Ako postaviť plugin pre platformu Ecommio: štruktúra súborov, schéma manifestu, validácia nastavení, tajomstvá a kam sa plugin registruje.


1. Založenie pluginu

php artisan make:plugin acme-widget --name="Acme Widget" --category=marketing
composer dump-autoload -o

Toto vytvorí:

plugins/acme-widget/
  plugin.json                          # manifest — schéma nastavení, oprávnenia, metadáta
  src/AcmeWidgetServiceProvider.php    # implementuje App\Plugins\Contracts\PluginInterface

...a zapojí "Plugins\\AcmeWidget\\": "plugins/acme-widget/src/" do bloku autoload.psr-4 v composer.json (best-effort — ak regulárny výraz nenájde vhodný riadok, vypíše riadok na pridanie ručne).

Po vygenerovaní: upravte settings[] v plugin.json na skutočné polia, potom pridajte záznam MarketplacePlugin (SuperAdmin → Marketplace, alebo seeder), aby si obchodníci mohli plugin nainštalovať — make:plugin vytvorí len súbory, do marketplace plugin nezaregistruje.

2. Štruktúra

SúborÚčel
plugin.jsonJediný zdroj pravdy: zobrazované metadáta, schéma settings[] (poháňa AJ automaticky generovaný formulár nastavení v administrácii AJ validáciu — pozri §4), permissions[].
src/{Name}ServiceProvider.phpImplementuje App\Plugins\Contracts\PluginInterface. Objaví a inštancuje ho App\Plugins\Services\PluginManager (prehľadanie súborového systému plugins/*/plugin.json, vždy čerstvé — žiadny cache manifestu, ktorý treba invalidovať po úprave).

PluginInterface:

interface PluginInterface
{
    public function getName(): string;
    public function getVersion(): string;
    public function getDescription(): string;
    public function getAuthor(): string;
    public function getDependencies(): array;
    public function isEnabled(): bool;
    public function boot(): void;       // registrácia služieb/routes/observerov
    public function register(): void;   // container bindings
    public function getSettingsSchema(): array;  // zvyčajne znova číta plugin.json
    public function getPermissions(): array;
    public function install(): void;    // aktuálne no-op konvencia naprieč všetkými pluginmi
    public function uninstall(): void;  // aktuálne no-op konvencia naprieč všetkými pluginmi
    public function upgrade(string $fromVersion, string $toVersion): void;  // aktuálne no-op konvencia naprieč všetkými pluginmi
}

handleSettingsSave(array $settings): array je voliteľná (kontrolovaná cez method_exists() v PluginController::saveSettings(), nie je súčasťou rozhrania) — implementujte ju na dodatočné kroky po uložení (Stripe si vytvorí webhook endpoint) alebo na zašifrovanie tajomstva pri ukladaní (pozri §5).

Verzovanie (upgrade hook + zachovanie nastavení pri odinštalovaní)

PluginManager::checkAndApplyUpgrade($name) porovná poslednú zaznamenanú nainštalovanú verziu tenanta (PluginSetting::getValue($name, '_installed_version')) s AKTUÁLNYM reťazcom version z plugin.json. Spúšťa sa automaticky z enable() — moment, keď sa plugin stane pre tenanta živým, je zároveň prirodzený bod na dohnanie čohokoľvek, čo sa zmenilo v kóde, kým bol plugin vypnutý. Ak sa verzie líšia, zavolá sa upgrade() vášho providera s (string $fromVersion, string $toVersion) predtým, než sa uložená verzia posunie — použite to na migráciu tvaru/sémantiky nastavenia naprieč release (napr. premenovanie kľúča, alebo prešifrovanie hodnoty do nového formátu). Čerstvá inštalácia len zaznamená aktuálnu verziu ako základ; upgrade() sa pri prvej inštalácii nikdy nevolá.

Zvyšujte version v plugin.json iba vtedy, keď naozaj potrebujete, aby upgrade() prebehol pre existujúce inštalácie — každý dodaný plugin zatiaľ zostal na 1.0.0, presne preto, že nikdy nič nevyžadovalo migračný krok.

Administrátorské "Odinštalovať" akceptuje voliteľný príznak retain_settings (checkbox v potvrdzovacom dialógu, predvolene vypnutý — zodpovedá pôvodnému správaniu vždy zmazať). Keď je zapnutý, riadky plugin_settings (API kľúče, nakonfigurované voľby, sledovaná nainštalovaná verzia) prežijú odinštalovanie, takže neskorší reinštall príde už predkonfigurovaný namiesto začínania od nuly. Marketplace entitlement riadok TenantPlugin sa odstráni v oboch prípadoch — zachovanie sa týka len vlastnej konfigurácie pluginu, nie toho, či je obchod na plugin oprávnený.

3. Referencia plugin.json

{
    "name": "Acme Widget",
    "version": "1.0.0",
    "description": "...",
    "author": "Ecommio",
    "provider": "Plugins\\AcmeWidget\\AcmeWidgetServiceProvider",
    "category": "marketing",           // zoskupuje záznam v marketplace
    "dependencies": [],                 // slugy iných pluginov vyžadovaných najprv (informatívne)
    "settings": [ /* pozri §4 */ ],
    "permissions": ["analytics.view"],  // stringy oprávnení, ktoré tenant udelí pri inštalácii

    // Iba platobné pluginy — zaregistruje adaptér do PaymentGatewayRegistry
    // bez zmeny jadra. Pozri §7.
    "payment_gateway": { "code": "acme", "adapter": "Plugins\\AcmePay\\AcmeGateway" }
}

name, version a provider sú jediné povinné kľúče (chýbajúci kľúč sa zaloguje a plugin sa aj tak načíta). Manifest, ktorý nie je platný JSON, sa preskočí úplne.

4. Schéma nastavení + validácia (riadená manifestom)

Každá položka settings[]:

{
    "id": "measurement_id",             // povinné — kľúč nastavenia
    "type": "text",                     // povinné — poháňa AJ admin formulár AJ predvolené validačné pravidlo
    "label": "GA4 Measurement ID",      // povinné — popisok v admin formulári
    "description": "...",               // voliteľný pomocný text
    "placeholder": "G-XXXXXXXXXX",      // voliteľné
    "default": "",                      // voliteľné
    "options": [{"value": "a", "label": "A"}],  // povinné pre type:"select"
    "validation": ["nullable", "string", "max:50", "regex:/^G-[A-Z0-9]+$/i"],  // voliteľné — pozri nižšie
    "validation_message": "Measurement ID musí byť v tvare G-XXXXXXXXXX."      // voliteľné
}

App\Http\Requests\Admin\SavePluginSettingsRequest zostaví Laravel validačné pravidlá pre každé pole automaticky cez App\Plugins\Services\PluginSettingsValidationRules — žiadny kód ani match-arm na plugin netreba písať ani si pamätať. Pole dostane, v poradí:

  1. Vlastné pole "validation" doslovne (Laravel rule stringy/objekty), ak je prítomné.
  2. Inak predvolené pravidlo podľa type:
typePredvolené pravidlo
text (alebo neznámy)nullable, string, max:500
textareanullable, string, max:5000
passwordnullable, string, max:1000
numbernullable, numeric
checkboxnullable
select (s options)nullable, Rule::in(<hodnoty options>)
select (bez options)nullable, string, max:100
webhook_status(len na zobrazenie — bez pravidla; nikdy sa neposiela pri ukladaní)

Nastavte explicitné pole "validation" vždy, keď je predvolené pravidlo príliš voľné pre dané pole (číselný rozsah, formát regexom, email). "validation_message" sa mapuje na kľúče {id}.regex / {id}.in Laravel správ pre priateľskejšiu chybu než generickú.

Pole, ktoré potrebuje skutočnú PHP logiku (nevyjadriteľnú ako reťazec pravidla — napr. vlastnú kontrolu povolených schém) nemôže žiť v JSON. Pridajte ju ako closure override v SavePluginSettingsRequest::applyClosureOverrides(), zlúčenú nad pravidlami odvodenými z manifestu pre dané jedno pole (vzor nájdete pri cookie-consent → privacy_url kontrole XSS schémy).

Neznáme hodnoty type stále dostanú generické predvolené pravidlo pre text — každý plugin má reálnu validáciu, známy aj nový.

5. Tajomstvá (polia transient)

Pole typu password obsahujúce skutočné tajomstvo (API kľúč, webhook secret), ktoré si váš provider šifruje sám, musí byť označené "transient": true v manifeste (nielen v getSettingsSchema() — generický ukladač číta množinu zapisovateľných polí z manifestu):

{ "id": "api_key", "type": "password", "label": "API Key", "transient": true }

PluginController::saveSettings() odstráni transient polia z generického zápisu PluginSetting::savePluginSettings() úplne — uloženie plaintextu, hoci len na okamih pred zašifrovaním vo vašom hooku, by ho nechalo čitateľné v databáze, ak by hook zlyhal. Implementujte handleSettingsSave() na zašifrovanie + uloženie pod {field}_encrypted:

public function handleSettingsSave(array $settings): array
{
    $value = $settings['api_key'] ?? '';
    if (is_string($value) && trim($value) !== '') {
        PluginSetting::savePluginSettings('acme-widget', [
            'api_key_encrypted' => Crypt::encryptString(trim($value)),
            'api_key' => '',
        ]);
    }

    return ['success' => true];
}

PluginManager::loadSettingsIntoConfig() dešifruje {field}_encrypted späť do config('plugins.acme-widget.api_key'), aby si to váš provider vedel prečítať. Pre celý vzor sa inšpirujte pluginom coinbase-commerce alebo gpwebpay (prázdne opätovné odoslanie zachová existujúci šifrotext).

6. Injekcia skriptov/widgetov do storefrontu

Konfigurácia zapnutého pluginu sa dostane na storefront cez App\View\Components\PluginScripts (Blade) a globálne premenné window.__{PLUGIN}_CONFIG konzumované React komponentom zaregistrovaným v ClientExtras (resources/js/app.tsx).

Widgety s config globálom (bežný prípad) sú bezmanifestové a nevyžadujú ŽIADNU zmenu v `PluginScripts.php` ani `plugin-scripts.blade.php`. Implementujte voliteľnú metódu widgetConfig(array $settings): ?array na vašom providerovi — duck-typed cez method_exists(), rovnaká konvencia ako handleSettingsSave() — vracajúcu tvarovanú konfiguráciu na zverejnenie, alebo null, ak ešte nie je pripravená. PluginScripts::loadConfigGlobalPluginData() ju automaticky vyzdvihne pre každý zapnutý plugin, podmienené tým, že plugin má aspoň jeden skutočný (nie podčiarkovníkom začínajúci) uložený riadok nastavenia — plugin práve zapnutý bez navštívených nastavení nič nevykreslí, zhodne s pôvodným správaním. JS globál sa predvolene volá podľa slugu v SCREAMING_SNAKE_CASE (cookie-consent → window.__COOKIE_CONSENT_CONFIG); prepíšte ho voliteľnou metódou widgetGlobalName(): string, ak potrebujete iný názov (pozri product-comparison, ktorý si drží svoj pôvodný kratší __COMPARISON_CONFIG). Pre vzor implementácie widgetConfig() sa inšpirujte cookie-consent, social-proof alebo product-comparison.

Zostávajúci manuálny krok je ClientExtras v app.tsx — importujte svoj React widget komponent a pridajte jeden riadok do JSX fragmentu; samotný komponent by mal čítať svoj window.__X_CONFIG globál a vykresliť null, keď chýba.

google-analytics/google-tag-manager/facebook-pixel sú výnimka: načítavajú externý trackovací skript (nielen config blob), takže si držia svoje existujúce natvrdo zapojené @include-ované Blade partialy v plugin-scripts.blade.php namiesto widgetConfig() — ak váš plugin potrebuje načítať externý <script src> tretej strany, inšpirujte sa niektorým z týchto troch.

7. Platobné pluginy

Platobné brány implementujú App\Domains\Shipping\Contracts\PaymentGatewayInterface (code(), pluginSlug(), isConfigured(), isRedirectGateway(), supportsHeadlessCheckout(), supportsRefund(), refund(), zobrazovacie metadáta) a riešia sa cez App\Domains\Shipping\Services\PaymentGatewayRegistry — pozrite StripeGateway/ CoinbaseGateway/GpWebpayGateway na vzor adaptéra (každý obaľuje vlastnú *Service triedu pluginu; samotná iniciácia checkoutu zostáva per-gateway, keďže Stripe PaymentIntent, GPwebpay podpísaný redirect a Coinbase hostovaný charge sú naozaj odlišné flowy).

Deklarujte adaptér vo vlastnom `plugin.json` — registry si postaví svoju mapu z manifestov, takže toto nevyžaduje žiadnu zmenu jadra platformy:

"payment_gateway": { "code": "acme", "adapter": "Plugins\\AcmePay\\AcmeGateway" }

Manifesty sa čítajú bez ohľadu na stav zapnutia/inštalácie, zámerne: brána musí zostať adresovateľná aj keď je vypnutá, aby isConfigured() mohla pre ňu vrátiť false, namiesto toho, aby sa jej kód javil ako neznáma platobná metóda. Manifest odkazujúci na chýbajúcu triedu, alebo takú, ktorá nie je PaymentGatewayInterface, sa zaloguje a preskočí — jeden pokazený plugin tretej strany nikdy nezhodí pokladňu pre ostatné.

PaymentMethodService aj refundačný flow v OrderController riešia brány cez registry, takže na to, aby sa brána objavila v pokladni, netreba meniť nič iné.

`supportsHeadlessCheckout()` deklaruje, či API klient vie spustiť vašu bránu — teda či vystavujete iniciačný endpoint pod /api/v1. Vráťte false a brána sa skryje z GET /api/v1/checkout/methods a POST /checkout ju odmietne s 422 PAYMENT_METHOD_NOT_AVAILABLE, namiesto toho, aby headless zákazník zostal s objednávkou uviaznutou v pending bez možnosti zaplatiť. Toto je odlišné od isRedirectGateway(), ktorá len popisuje, ako vás vykresľujú naše vlastné témy.

Stále na strane jadra: vaše HTTP endpointy. Routy žijú v routes/shop.php / routes/api.php, aby prežili route:cache, a webhook cesta potrebuje CSRF výnimku v bootstrap/app.php. Brána, ktorá komunikuje s poskytovateľom, teda stále potrebuje tieto dva riadky pridané nami — manifest odstraňuje tretí kontaktný bod (registry), nie všetky.

8. Testovanie

Feature testy pre nastavenia pluginov žijú v tests/Feature/Admin/PluginTest.php (Tests\AdminTestCase, actingAsAdmin() + lokálny pomocník installPluginForTenant($directory), ktorý založí MarketplacePlugin + aktívny TenantPlugin riadok, aby prešla entitlement kontrola). Testy objavovania súborového systému (bez DB) žijú v tests/Feature/Plugins/PluginWhitelistTest.php (čistý Tests\TestCase). Samotný make:plugin je pokrytý v tests/Feature/Console/MakePluginCommandTest.php.

php artisan test --compact tests/Feature/Admin/PluginTest.php
php artisan test --compact --filter=your_plugin_test_name

Bola táto stránka užitočná?