- 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.
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-nameinstalliert 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
- Bundle in
config/bundles.phpregistriert? - Composer-Autoloading aktualisiert? (
composer dump-autoload) - Cache geleert? (
php bin/console cache:clear) - Service-Tag
app.module_plugingesetzt?
Lizenz wird nicht validiert
- Environment-Variablen korrekt gesetzt?
- Lizenzserver erreichbar? (Netzwerk, Firewall)
- Cache-Problem? Cache löschen und neu validieren
- Logs prüfen:
tail -f var/log/dev.log | grep -i license
Modul bootet nicht
- Lizenz gültig? (
php bin/console app:module:license mymodule --validate) - Abhängigkeiten erfüllt? (
php bin/console app:module:list) - Doctrine-Mappings korrekt? (
php bin/console doctrine:schema:validate) - 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_ADMINschü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.