This guide helps you migrate existing BDM modules to use the Universal API framework. The migration process maintains backward compatibility while adding new features.
Small Module (1-3 models): 2-4 hours
Medium Module (4-8 models): 1-2 days
Large Module (9+ models): 3-5 days
# Backup database mysqldump -u username -p database_name > backup_$(date +%Y%m%d).sql # Backup code git checkout -b migration-universal-api git commit -am "Pre-migration checkpoint" # Tag current state git tag pre-universal-api-migration
Identify:
# Create test database CREATE DATABASE bdm_test; # Copy .env to .env.testing cp .env .env.testing # Update test database credentials DB_DATABASE=bdm_test
Create configuration files
config/modules/bdm_[module]_permissions.php
config/modules/bdm_[module]_api.php
Update models
Add HasModulePermissions trait
Add user_id if missing
Create API controllers
Extend BaseApiController
Implement validation rules
Update routes
Create app/BDM[Module]/Routes/api.php
Register in routes/api.php
Create guest access handlers
For resources that need external access
Write tests
Feature tests for all endpoints
Permission tests
Update frontend (if needed)
Change API endpoints
Update response handling
Documentation
Update API documentation
Document breaking changes
Deploy
Run migrations (if any new tables)
Clear cache
Test in production
Create config/modules/bdm_accounting_permissions.php
<?php
return [
'roles' => [
'owner' => [
'label' => 'Owner',
'permissions' => ['*'],
],
'admin' => [
'label' => 'Administrator',
'permissions' => [
'invoices.view',
'invoices.create',
'invoices.edit',
'invoices.delete',
// ... add all permissions
],
],
// ... other roles
],
'guest_access' => [
'invoice' => [
'default_permissions' => ['view', 'download', 'pay'],
'default_expiry_days' => 30,
],
],
];
Create config/modules/bdm_accounting_api.php
<?php
return [
'searchable_fields' => [
'invoices' => ['number', 'customer_name', 'customer_email'],
],
'sortable_fields' => [
'invoices' => ['number', 'total', 'due_date', 'created_at'],
],
'filterable_fields' => [
'invoices' => ['status', 'customer_id'],
],
'webhook_events' => [
'invoice.created' => 'Invoice Created',
'invoice.paid' => 'Invoice Paid',
],
];
Add trait to existing model:
<?php namespace App\BDMAccounting\Models; use Illuminate\Database\Eloquent\Model; use App\BDMCore\API\Traits\HasModulePermissions; class Invoice extends Model { use HasModulePermissions; protected $table = 'bdm_accounting_invoices'; protected $fillable = [ 'number', 'customer_name', 'total', 'user_id', // Add if missing ]; public function getModuleSlug(): string { return 'accounting'; } }
⚠️ Migration Note: If your table doesn't have user_id, create a migration to add it.
Create app/BDMAccounting/API/Controllers/InvoiceApiController.php
<?php
namespace App\BDMAccounting\API\Controllers;
use App\BDMCore\API\Controllers\BaseApiController;
use App\BDMAccounting\Models\Invoice;
use Illuminate\Http\Request;
class InvoiceApiController extends BaseApiController
{
protected $model = Invoice::class;
protected $moduleSlug = 'accounting';
protected function createRules(): array
{
return [
'customer_name' => 'required|string|max:255',
'customer_email' => 'required|email',
'total' => 'required|numeric|min:0',
];
}
protected function updateRules(): array
{
return [
'customer_name' => 'sometimes|string|max:255',
'customer_email' => 'sometimes|email',
'total' => 'sometimes|numeric|min:0',
];
}
// Add custom endpoints if needed
public function sendInvoice(Request $request, $id)
{
if (!$this->canPerformAction('invoices.send')) {
return $this->errorResponse('Unauthorized', 403);
}
$invoice = $this->getModelQuery()->findOrFail($id);
// Your send logic here
return $this->successResponse($invoice, 'Invoice sent');
}
}
Create app/BDMAccounting/Routes/api.php
<?php
use Illuminate\Support\Facades\Route;
use App\BDMAccounting\API\Controllers\InvoiceApiController;
Route::prefix('v1/accounting')
->middleware(['auth:sanctum', 'log.api'])
->group(function () {
// Standard REST endpoints
Route::apiResource('invoices', InvoiceApiController::class);
// Custom actions
Route::post('invoices/{id}/send', [InvoiceApiController::class, 'sendInvoice']);
Route::post('invoices/{id}/guest-access', [InvoiceApiController::class, 'createGuestAccess']);
});
Then register in routes/api.php:
// Load module API routes
require app_path('BDMAccounting/Routes/api.php');
# Get auth token
POST /api/login
{
"email": "user@example.com",
"password": "password"
}
# Test new endpoint
GET /api/v1/accounting/invoices
Authorization: Bearer YOUR_TOKEN
# Test create
POST /api/v1/accounting/invoices
Authorization: Bearer YOUR_TOKEN
{
"customer_name": "Test Customer",
"customer_email": "test@example.com",
"total": 100.00
}
// Old custom controller
class InvoiceController extends Controller
{
public function index(Request $request)
{
$invoices = Invoice::where('user_id', auth()->id())
->paginate(15);
return response()->json([
'data' => $invoices
]);
}
public function store(Request $request)
{
$validated = $request->validate([
'customer_name' => 'required',
'total' => 'required|numeric',
]);
$invoice = Invoice::create([
'user_id' => auth()->id(),
...$validated
]);
return response()->json([
'data' => $invoice
], 201);
}
}
// New API controller
class InvoiceApiController extends BaseApiController
{
protected $model = Invoice::class;
protected $moduleSlug = 'accounting';
protected function createRules(): array
{
return [
'customer_name' => 'required|string|max:255',
'total' => 'required|numeric|min:0',
];
}
// index() and store() inherited from BaseApiController
// Handles: pagination, filtering, permissions, logging automatically
}
public function update(Request $request, $id)
{
$invoice = Invoice::findOrFail($id);
// Manual permission check
if ($invoice->user_id !== auth()->id()) {
return response()->json(['error' => 'Unauthorized'], 403);
}
$invoice->update($request->all());
return response()->json(['data' => $invoice]);
}
// update() method inherited from BaseApiController
// Automatically handles:
// - Permission checking (via HasModulePermissions trait)
// - Shared access (user can update if shared with them)
// - Validation
// - Standard response format
// You only need to define validation rules:
protected function updateRules(): array
{
return [
'customer_name' => 'sometimes|string|max:255',
'total' => 'sometimes|numeric|min:0',
];
}
public function index(Request $request)
{
$query = Invoice::where('user_id', auth()->id());
if ($request->status) {
$query->where('status', $request->status);
}
if ($request->search) {
$query->where('customer_name', 'like', "%{$request->search}%");
}
if ($request->sort_by) {
$query->orderBy($request->sort_by, $request->sort_order ?? 'asc');
}
return response()->json(['data' => $query->paginate(15)]);
}
// In config/modules/bdm_accounting_api.php
'searchable_fields' => [
'invoices' => ['customer_name', 'number'],
],
'sortable_fields' => [
'invoices' => ['created_at', 'total', 'status'],
],
'filterable_fields' => [
'invoices' => ['status'],
],
// BaseApiController handles:
// GET /api/v1/accounting/invoices?search=acme&filter[status]=paid&sort_by=total
// All filtering, sorting, searching automatic!
<?php
namespace Tests\Feature;
use Tests\TestCase;
use App\Models\User;
use App\BDMAccounting\Models\Invoice;
use Illuminate\Foundation\Testing\RefreshDatabase;
class InvoiceApiTest extends TestCase
{
use RefreshDatabase;
public function test_can_list_invoices()
{
$user = User::factory()->create();
Invoice::factory()->count(5)->create(['user_id' => $user->id]);
$response = $this->actingAs($user, 'sanctum')
->getJson('/api/v1/accounting/invoices');
$response->assertOk()
->assertJsonStructure([
'success',
'data' => [
'*' => ['id', 'customer_name', 'total']
],
'meta'
]);
}
public function test_cannot_access_other_users_invoices()
{
$user1 = User::factory()->create();
$user2 = User::factory()->create();
$invoice = Invoice::factory()->create(['user_id' => $user1->id]);
$response = $this->actingAs($user2, 'sanctum')
->getJson("/api/v1/accounting/invoices/{$invoice->id}");
$response->assertNotFound();
}
public function test_shared_user_can_access()
{
$owner = User::factory()->create();
$sharedUser = User::factory()->create();
// Share module with user
app(PermissionService::class)->shareModule(
$owner,
$sharedUser->email,
'accounting',
PermissionRole::VIEWER
);
$invoice = Invoice::factory()->create(['user_id' => $owner->id]);
$response = $this->actingAs($sharedUser, 'sanctum')
->getJson("/api/v1/accounting/invoices/{$invoice->id}");
$response->assertOk();
}
}
# Run all tests php artisan test # Run specific test file php artisan test tests/Feature/InvoiceApiTest.php # Run with coverage php artisan test --coverage # Stop on first failure php artisan test --stop-on-failure
Error: Column 'user_id' not found
Solution: Create migration to add column:
php artisan make:migration add_user_id_to_invoices_table
// In migration
public function up()
{
Schema::table('bdm_accounting_invoices', function (Blueprint $table) {
$table->foreignId('user_id')->after('id')->constrained();
$table->index('user_id');
});
// If you have existing data, assign to a default user
DB::table('bdm_accounting_invoices')
->whereNull('user_id')
->update(['user_id' => 1]); // Admin user
}
Error: Routes accessible without authentication
Solution: Check middleware in routes:
// Ensure these middleware are applied
Route::middleware(['auth:sanctum', 'log.api'])
->group(function () {
// Your routes
});
Error: Access-Control-Allow-Origin header missing
Solution: Configure CORS in config/cors.php:
'paths' => ['api/*'], 'allowed_origins' => ['http://localhost:3000'], // Your frontend URL 'supports_credentials' => true,
Error: 429 Too Many Requests
Solution: Increase rate limits in .env:
BDM_API_RATE_LIMIT_PER_MINUTE=500 BDM_API_RATE_LIMIT_PER_HOUR=20000
Error: 404 on new endpoints
Solution: Clear route cache:
php artisan route:clear php artisan route:cache php artisan config:clear
Always have a rollback plan. Here's how to safely revert changes:
# Return to pre-migration state git checkout pre-universal-api-migration # Or reset to specific commit git reset --hard COMMIT_HASH # Force push if already deployed (careful!) git push origin main --force
# Restore from backup mysql -u username -p database_name < backup_YYYYMMDD.sql # Or rollback specific migration php artisan migrate:rollback --step=1
php artisan cache:clear php artisan config:clear php artisan route:clear php artisan view:clear
🎉 Congratulations! Your module is now using the BDM Universal API Framework