myCRM/docs/PLUGIN_SYSTEM.md
olli 42e7bc7e10 feat(billing-module): Implementieren der Modulstruktur und Lizenzverwaltung
- Hinzufügen der DependencyInjection-Konfiguration für das Billing-Modul.
- Erstellen der Invoice-Entity mit API-Ressourcen und Berechtigungen.
- Konfigurieren der Services in services.yaml für das Billing-Modul.
- Implementieren von CLI-Commands zur Verwaltung von Modul-Lizenzen und zur Auflistung installierter Module.
- Erstellen eines API-Controllers zur Verwaltung von Modulen und Lizenzen.
- Hinzufügen eines EventListeners für das Booten von Modulen.
- Definieren von Interfaces für Lizenzvalidierung und Modul-Plugins.
- Implementieren der ModuleRegistry zur Verwaltung und Booten von Modulen.
- Erstellen eines LicenseValidator-Services zur Validierung und Registrierung von Lizenzen.
2025-12-03 15:14:07 +01:00

14 KiB

Plugin-System für lizenzierbare Module

Übersicht

Das myCRM Plugin-System ermöglicht die Installation und Verwaltung von optionalen, lizenzierbaren Modulen als separate Composer-Packages. Module können unabhängig vom Core entwickelt, installiert und lizenziert werden.

Architektur-Prinzipien

1. Lose Kopplung

  • Core kennt nur Interfaces, keine konkreten Module
  • Module registrieren sich selbst über Symfony's Service-Tagging
  • Keine Hard-Dependencies im Core-Code

2. Lizenzbasierte Aktivierung

  • Module werden nur gebootet, wenn eine gültige Lizenz vorhanden ist
  • Online-Validierung mit Offline-Fallback (Grace Period: 7 Tage)
  • Caching zur Performance-Optimierung

3. Plugin als Symfony Bundle

  • Jedes Modul ist ein vollständiges Symfony Bundle
  • Eigene Entities, Controller, Services, Routes
  • Optionale Frontend-Komponenten (Vue.js)

4. Composer-basierte Installation

  • Module sind private Composer-Packages
  • composer require mycrm/modul-name installiert das Modul
  • Automatische Service-Registrierung über Symfony Flex

Komponenten

Core-Komponenten (im Hauptprojekt)

src/Plugin/
├── ModulePluginInterface.php      # Interface für alle Module
├── LicenseValidatorInterface.php  # Interface für Lizenzvalidierung
├── ModuleRegistry.php              # Registry für alle installierten Module

src/Service/
└── LicenseValidator.php            # Standard-Implementierung

src/EventListener/
└── ModuleBootListener.php          # Bootet Module beim Request

src/Command/
├── ModuleListCommand.php           # CLI: Module auflisten
└── ModuleLicenseCommand.php        # CLI: Lizenzen verwalten

src/Controller/Api/
└── ModuleManagementController.php  # API für Module-Verwaltung

config/
└── services_plugin.yaml            # Service-Konfiguration

Modul-Struktur (externes Package)

mycrm-modulname/
├── composer.json
├── src/
│   ├── ModulnameModulePlugin.php   # Implementiert ModulePluginInterface
│   ├── ModulnameBundle.php         # Symfony Bundle
│   ├── DependencyInjection/
│   │   ├── ModulnameExtension.php
│   │   └── Configuration.php
│   ├── Entity/                     # Modul-spezifische Entities
│   ├── Controller/                 # API-Controller
│   ├── Service/                    # Business Logic
│   ├── Security/Voter/             # Permission-Voter
│   └── EventListener/
├── config/
│   ├── services.yaml
│   ├── routes.yaml
│   └── packages/
│       └── doctrine.yaml
├── assets/                          # Frontend (optional)
│   └── js/
│       └── components/
└── migrations/                      # Modul-spezifische Migrations

Installation und Konfiguration

1. Core-System vorbereiten

Die Core-Komponenten sind bereits implementiert. Konfiguration aktivieren:

# services_plugin.yaml in services.yaml importieren
# config/services.yaml
imports:
    - { resource: services_plugin.yaml }

Environment-Variablen setzen:

# .env.local
LICENSE_SERVER_URL=https://license.mycrm.local
INSTANCE_ID=unique-instance-identifier-here

# Beispiel-Lizenz für ein Modul
LICENSE_BILLING=your-license-key-here

2. Modul installieren

# Private Repository konfigurieren
composer config repositories.mycrm-billing vcs https://github.com/your-org/mycrm-billing-module

# Modul installieren
composer require mycrm/billing-module

# Bundle registrieren (falls nicht automatisch via Flex)
# config/bundles.php
return [
    // ...
    MyCRM\BillingModule\BillingBundle::class => ['all' => true],
];

# Migrations ausführen
php bin/console doctrine:migrations:migrate

3. Lizenz registrieren

Via CLI:

# Interaktiv
php bin/console app:module:license billing

# Direkt mit Key
php bin/console app:module:license billing YOUR_LICENSE_KEY

# Lizenz validieren
php bin/console app:module:license billing --validate

# Lizenz widerrufen
php bin/console app:module:license billing --revoke

# Alle Module auflisten
php bin/console app:module:list

Via API (für Admin-UI):

# Module auflisten
curl -X GET http://localhost:8000/api/modules \
  -H "Authorization: Bearer YOUR_TOKEN"

# Lizenz registrieren
curl -X POST http://localhost:8000/api/modules/billing/license \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -d '{"license_key": "YOUR_LICENSE_KEY"}'

# Lizenz widerrufen
curl -X DELETE http://localhost:8000/api/modules/billing/license \
  -H "Authorization: Bearer YOUR_TOKEN"

Via Frontend (Vue.js):

Die Komponente ModuleManagement.vue bietet eine Admin-Oberfläche zur Verwaltung:

// In router.js hinzufügen
{
    path: '/admin/modules',
    name: 'module-management',
    component: () => import('@/views/ModuleManagement.vue'),
    meta: {
        requiresAuth: true,
        requiresRole: 'ROLE_ADMIN'
    }
}

4. Cache leeren und testen

php bin/console cache:clear
php bin/console app:module:list

Modul entwickeln

Schritt 1: Projekt-Struktur erstellen

mkdir mycrm-mymodule
cd mycrm-mymodule
composer init

Schritt 2: composer.json konfigurieren

{
    "name": "mycrm/mymodule",
    "description": "Mein Custom CRM-Modul",
    "type": "symfony-bundle",
    "require": {
        "php": ">=8.3",
        "symfony/framework-bundle": "^7.1"
    },
    "autoload": {
        "psr-4": {
            "MyCRM\\MyModule\\": "src/"
        }
    }
}

Schritt 3: Plugin-Klasse implementieren

<?php
// src/MyModulePlugin.php

namespace MyCRM\MyModule;

use App\Plugin\LicenseValidatorInterface;
use App\Plugin\ModulePluginInterface;

class MyModulePlugin implements ModulePluginInterface
{
    public function __construct(
        private readonly LicenseValidatorInterface $licenseValidator
    ) {}

    public function getIdentifier(): string
    {
        return 'mymodule';
    }

    public function getDisplayName(): string
    {
        return 'Mein Modul';
    }

    public function getVersion(): string
    {
        return '1.0.0';
    }

    public function getDescription(): string
    {
        return 'Beschreibung meines Moduls';
    }

    public function isLicensed(): bool
    {
        $info = $this->getLicenseInfo();
        return $info['valid'];
    }

    public function getLicenseInfo(): array
    {
        return $this->licenseValidator->validate($this->getIdentifier());
    }

    public function boot(): void
    {
        if (!$this->isLicensed()) {
            throw new \RuntimeException('Modul nicht lizenziert');
        }

        // Modul-spezifische Boot-Logik
    }

    public function getPermissionModules(): array
    {
        return ['mymodule'];
    }

    public function canInstall(): array
    {
        return ['success' => true, 'errors' => []];
    }
}

Schritt 4: Bundle-Klasse erstellen

<?php
// src/MyModuleBundle.php

namespace MyCRM\MyModule;

use Symfony\Component\HttpKernel\Bundle\Bundle;

class MyModuleBundle extends Bundle
{
    public function getPath(): string
    {
        return \dirname(__DIR__);
    }
}

Schritt 5: Services konfigurieren

# config/services.yaml
services:
    _defaults:
        autowire: true
        autoconfigure: true

    # Plugin registrieren
    MyCRM\MyModule\MyModulePlugin:
        tags: ['app.module_plugin']

    # Weitere Services...
    MyCRM\MyModule\Controller\:
        resource: '../src/Controller/'
        tags: ['controller.service_arguments']

Schritt 6: Entities mit ModuleAwareInterface

<?php
// src/Entity/MyEntity.php

namespace MyCRM\MyModule\Entity;

use ApiPlatform\Metadata\ApiResource;
use App\Entity\ModuleAwareInterface;
use Doctrine\ORM\Mapping as ORM;

#[ORM\Entity]
#[ApiResource(
    security: "is_granted('VIEW', 'mymodule')"
)]
class MyEntity implements ModuleAwareInterface
{
    #[ORM\Id]
    #[ORM\GeneratedValue]
    #[ORM\Column]
    private ?int $id = null;

    // ... weitere Properties

    public function getModuleName(): string
    {
        return 'mymodule';
    }
}

Schritt 7: Voter für Permissions (optional)

<?php
// src/Security/Voter/MyEntityVoter.php

namespace MyCRM\MyModule\Security\Voter;

use App\Security\Voter\ModuleVoter;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;

class MyEntityVoter extends Voter
{
    public function __construct(
        private readonly ModuleVoter $moduleVoter
    ) {}

    protected function supports(string $attribute, mixed $subject): bool
    {
        return in_array($attribute, ['VIEW', 'EDIT', 'DELETE'])
            && ($subject instanceof MyEntity || $subject === 'mymodule');
    }

    protected function voteOnAttribute(
        string $attribute,
        mixed $subject,
        TokenInterface $token
    ): bool {
        // An ModuleVoter delegieren
        return $this->moduleVoter->vote($token, $subject, [$attribute]) === Voter::ACCESS_GRANTED;
    }
}

Lizenzserver-Integration

Lizenzserver-Endpunkt

Der Lizenzserver muss folgenden Endpunkt bereitstellen:

POST /api/validate
Content-Type: application/json

{
    "license_key": "YOUR-LICENSE-KEY",
    "module": "billing",
    "instance_id": "unique-instance-id",
    "version": "1.0.0"
}

Response (200 OK):
{
    "valid": true,
    "expires_at": "2026-12-31T23:59:59Z",
    "licensed_to": "Firma GmbH",
    "features": ["feature1", "feature2"],
    "message": "Lizenz gültig"
}

Response (400/403):
{
    "valid": false,
    "message": "Lizenz abgelaufen / ungültig / etc."
}

Lizenzschlüssel-Format (JWT)

Empfohlenes Format als JWT mit folgenden Claims:

{
    "sub": "billing",
    "customer_id": "CUST-12345",
    "licensed_to": "Firma GmbH",
    "expires_at": "2026-12-31T23:59:59Z",
    "features": ["invoicing", "recurring_billing"],
    "iat": 1704278400,
    "exp": 1767350399
}

Permission-System Integration

Automatische Modul-Registrierung

Module, die getPermissionModules() implementieren, werden automatisch im Permission-System verfügbar:

public function getPermissionModules(): array
{
    return [
        'billing',      // Hauptmodul
        'invoices',     // Sub-Modul
        'payments',     // Weiteres Sub-Modul
    ];
}

Diese Module können dann in der Role-Verwaltung zugewiesen werden.

Permission-Checks in Modul-Code

// In Controller
$this->denyAccessUnlessGranted('VIEW', 'billing');

// In Voter
$user->hasModulePermission('billing', 'edit');

// In API Platform
#[ApiResource(
    security: "is_granted('VIEW', 'billing')"
)]

Frontend-Integration

Modul-Status im Frontend verfügbar machen

// In Symfony Twig-Template (oder API-Endpunkt)
<script>
window.myCRM = {
    modules: {{ modules_status|json_encode|raw }}
}
</script>

Bedingte Route-Registrierung

// router.js
import { moduleRegistry } from './moduleRegistry'

const routes = [
    // Core-Routes...
]

// Modul-Routes nur hinzufügen, wenn aktiv
if (moduleRegistry.isActive('billing')) {
    routes.push({
        path: '/billing',
        component: () => import('@/views/BillingDashboard.vue')
    })
}

Modul-Komponenten lazy-loaden

// In App.vue oder Layout
import { computed } from 'vue'
import { useModules } from '@/composables/useModules'

const { isModuleActive } = useModules()

const showBillingMenu = computed(() => isModuleActive('billing'))

Testing

Unit-Tests für Plugin

<?php

namespace MyCRM\MyModule\Tests;

use MyCRM\MyModule\MyModulePlugin;
use PHPUnit\Framework\TestCase;

class MyModulePluginTest extends TestCase
{
    public function testGetIdentifier(): void
    {
        $licenseValidator = $this->createMock(LicenseValidatorInterface::class);
        $plugin = new MyModulePlugin($licenseValidator);

        $this->assertSame('mymodule', $plugin->getIdentifier());
    }

    public function testIsLicensedReturnsTrueWithValidLicense(): void
    {
        $licenseValidator = $this->createMock(LicenseValidatorInterface::class);
        $licenseValidator->method('validate')
            ->willReturn(['valid' => true]);

        $plugin = new MyModulePlugin($licenseValidator);

        $this->assertTrue($plugin->isLicensed());
    }
}

Troubleshooting

Modul wird nicht erkannt

  1. Bundle in config/bundles.php registriert?
  2. Composer-Autoloading aktualisiert? (composer dump-autoload)
  3. Cache geleert? (php bin/console cache:clear)
  4. Service-Tag app.module_plugin gesetzt?

Lizenz wird nicht validiert

  1. Environment-Variablen korrekt gesetzt?
  2. Lizenzserver erreichbar? (Netzwerk, Firewall)
  3. Cache-Problem? Cache löschen und neu validieren
  4. Logs prüfen: tail -f var/log/dev.log | grep -i license

Modul bootet nicht

  1. Lizenz gültig? (php bin/console app:module:license mymodule --validate)
  2. Abhängigkeiten erfüllt? (php bin/console app:module:list)
  3. Doctrine-Mappings korrekt? (php bin/console doctrine:schema:validate)
  4. Fehler in boot()-Methode? Logs prüfen

Best Practices

1. Versionierung

  • Semantic Versioning verwenden (Major.Minor.Patch)
  • Breaking Changes nur in Major-Versionen
  • Migrations mit Rollback-Fähigkeit

2. Fehlerbehandlung

  • Keine Exceptions in isLicensed() werfen
  • Detaillierte Fehlermeldungen in getLicenseInfo()
  • Graceful Degradation bei Netzwerkfehlern

3. Performance

  • Lizenz-Status cachen (bereits implementiert)
  • Lazy-Loading für Frontend-Komponenten
  • Doctrine-Queries optimieren

4. Sicherheit

  • Nie Lizenzschlüssel im Frontend exponieren
  • API-Endpunkte mit ROLE_ADMIN schützen
  • Input-Validierung in allen Controllern

5. Dokumentation

  • README.md mit Installation-Guide
  • API-Dokumentation (OpenAPI/Swagger)
  • Changelog pflegen

Beispiel: Komplettes Mini-Modul

Ein vollständiges Mini-Beispiel findet sich in:

  • /docs/example-module/ - Backend-Code
  • /docs/EXAMPLE_MODULE_STRUCTURE.md - Struktur-Übersicht

Support und Weiterentwicklung

Geplante Features

  • Multi-Tenancy-Support (verschiedene Instanzen, eine Lizenz)
  • Offline-Lizenzierung (Air-Gapped Systeme)
  • Automatische Updates über Composer
  • Modul-Marketplace Integration
  • Rollback-Mechanismus bei fehlgeschlagenen Installations

Beitragen

Weitere Module können nach diesem Pattern entwickelt werden. Core-Interfaces sind stabil und abwärtskompatibel.