This document is intended for developers who want to contribute to OpenMES, build modules, or understand the internal architecture.
- Tech Stack
- Repository Structure
- Local Development Setup
- Architecture Overview
- Module System
- Hook System
- Frontend (Blade + Alpine.js + Tailwind)
- API Development
- Testing
- Code Style
| Layer | Technology |
|---|---|
| Backend | Laravel 12, PHP 8.3 |
| Frontend | Blade templates, Alpine.js, Tailwind CSS 4 |
| Reactive components | Livewire 4 |
| Database | PostgreSQL 14+ |
| Auth | Laravel Sanctum (session + token) |
| Roles | Spatie Laravel Permission |
| Asset pipeline | Vite |
| File imports | PhpSpreadsheet (via maatwebsite/excel) |
| Deployment | Docker Compose |
OpenMes/
├── backend/ # Laravel application
│ ├── app/
│ │ ├── Console/Commands/ # Artisan commands
│ │ ├── Events/ # Laravel events
│ │ ├── Http/
│ │ │ ├── Controllers/
│ │ │ │ ├── Api/V1/ # REST API controllers
│ │ │ │ └── Web/ # Web (Blade) controllers
│ │ │ └── Requests/ # Form Request validation
│ │ ├── Models/ # Eloquent models
│ │ ├── Services/ # Business logic services
│ │ └── Livewire/ # Livewire components
│ ├── database/
│ │ ├── migrations/ # Core schema migrations
│ │ └── factories/ # Test data factories
│ ├── resources/
│ │ └── views/ # Blade templates
│ │ ├── admin/ # Admin panel views
│ │ ├── supervisor/ # Supervisor views
│ │ ├── operator/ # Operator views
│ │ └── layouts/ # Shared layouts & components
│ ├── routes/
│ │ ├── web.php # Web routes
│ │ └── api.php # API routes
│ └── tests/ # Feature & unit tests
├── modules/ # Optional modules directory
│ └── Packaging/ # Example: Packaging module
├── docs/ # Documentation (this directory)
├── docker-compose.yml
└── README.md
- Docker & Docker Compose
- PHP 8.3 (for running artisan commands locally without Docker)
- Composer
- Node.js 20+ and npm
git clone https://github.com/Mes-Open/OpenMes.git
cd OpenMes
docker-compose up -dNavigate to http://localhost and complete the web installer.
cd backend
composer install
npm install
npm run dev # starts Vite dev server with HMR# Inside Docker
docker compose exec backend php artisan <command>
# Or if PHP is installed locally
cd backend
php artisan <command># Run tests
php artisan test
# Run tests with coverage
php artisan test --coverage
# Reset and re-seed the database
php artisan migrate:fresh --seed
# Load sample data
php artisan db:seed --class=SampleDataSeeder
# Clear all caches
php artisan optimize:clear
# Run code formatter
./vendor/bin/pintOpenMES follows standard Laravel conventions with a few additions:
Web Controllers (app/Http/Controllers/Web/) are split by role:
Web/Admin/— admin panel controllersWeb/Supervisor/— supervisor controllersWeb/Operator/— operator controllers
API Controllers (app/Http/Controllers/Api/V1/) handle the REST API.
Services (app/Services/) contain reusable business logic:
ModuleManager— discovers, enables, disables, installs, and uninstalls modules- Other services for complex operations (import processing, batch creation, etc.)
Livewire Components (app/Livewire/) are reactive components used for real-time UI parts (e.g., dashboard metrics, live search).
Core entities and their relationships:
Factory → Division → Line → Workstation
↓
WorkOrder → ProcessTemplate → TemplateStep
↓
Batch → BatchStep (one per TemplateStep)
↓
Issue → IssueType
Additional tables:
users— authentication, Spatie rolesworkers— shop floor workers (separate from user accounts)crews,skills,wage_groups— HRshifts— working hours definitionaudit_logs— immutable change historyevent_logs— system eventscsv_imports,csv_import_mappings— bulk import history and profilessystem_settings— key/value configuration storeline_statuses— configurable line status codes
Roles are managed with Spatie Laravel Permission:
// In controllers
$this->middleware('role:Admin');
$this->middleware('role:Supervisor|Admin');
// In Blade templates
@hasrole('Admin')
<admin-only-content/>
@endhasrole
@hasanyrole('Supervisor|Admin')
<supervisor-and-admin-content/>
@endhasanyroleThe three main roles:
Admin— full system accessSupervisor— production management, no system configurationOperator— own line only, no management views
Modules are self-contained Laravel packages located in the modules/ directory at the root of the project. They are auto-discovered and loaded by the core application.
modules/
└── MyModule/
├── module.json # Required: module metadata
├── Providers/
│ └── MyModuleServiceProvider.php # Required: registers everything
├── Controllers/
│ └── MyModuleController.php
├── Models/
│ └── MyEntity.php
├── views/
│ └── index.blade.php
├── migrations/
│ └── 2025_01_01_000001_create_my_table.php
└── Console/
└── MyCommand.php
{
"name": "MyModule",
"display_name": "My Module",
"description": "A short description of what this module does.",
"version": "1.0.0",
"author": "Your Name"
}The ModuleManager discovers modules by scanning modules/*/Providers/*ServiceProvider.php. For a module to be loaded, its service provider must be registered.
Create modules/MyModule/Providers/MyModuleServiceProvider.php:
<?php
namespace Modules\MyModule\Providers;
use Illuminate\Support\ServiceProvider;
class MyModuleServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Register routes
$this->loadRoutesFrom(__DIR__ . '/../routes/web.php');
// Register Blade views (access via my-module::view-name)
$this->loadViewsFrom(__DIR__ . '/../views', 'my-module');
// Register migrations
$this->loadMigrationsFrom(__DIR__ . '/../migrations');
// Register Artisan commands
if ($this->app->runningInConsole()) {
$this->commands([
\Modules\MyModule\Console\MyCommand::class,
]);
}
}
}Modules can add items to the sidebar by hooking into MenuRegistry:
use App\Services\MenuRegistry;
// Inside boot() method:
MenuRegistry::add('my-module', [
'label' => 'My Module',
'icon' => '<svg .../>',
'route' => 'my-module.index',
'role' => 'Admin', // or 'Supervisor|Admin', 'Operator|Supervisor|Admin'
]);Place migration files in modules/MyModule/migrations/. They follow the same format as Laravel migrations:
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void
{
Schema::create('my_module_entities', function (Blueprint $table) {
$table->id();
$table->foreignId('work_order_id')->constrained()->cascadeOnDelete();
$table->string('name');
$table->timestamps();
});
}
public function down(): void
{
Schema::dropIfExists('my_module_entities');
}
};Run migrations: php artisan migrate
To add a relationship to a core model (e.g. WorkOrder) from a module, use the service provider's boot() method:
use App\Models\WorkOrder;
use Modules\MyModule\Models\MyEntity;
WorkOrder::resolveRelationUsing('myEntities', function (WorkOrder $model) {
return $model->hasMany(MyEntity::class);
});Now $workOrder->myEntities is available throughout the application.
OpenMES fires Laravel Events throughout the production lifecycle. Modules listen to these events to react to changes without modifying core code.
See HOOKS.md for the full list of available events and examples.
Listen to work order completion in your module:
// In MyModuleServiceProvider::boot()
use App\Events\WorkOrder\WorkOrderCompleted;
Event::listen(WorkOrderCompleted::class, function ($event) {
$workOrder = $event->workOrder;
// Send ERP notification, update inventory, etc.
});OpenMES uses server-rendered Blade templates with:
- Tailwind CSS 4 — utility-first CSS (compiled via Vite)
- Alpine.js — lightweight reactivity for interactive components
- Livewire 4 — full-stack reactive components (forms, live tables)
- Chart.js — dashboard charts
The app supports dark mode via the dark class on <html>:
<!-- Dark mode styles -->
<div class="bg-white dark:bg-gray-900 text-gray-900 dark:text-white">Dark mode preference is stored in localStorage under the key theme.
Sidebar state (collapsed, group open/close states) is managed in the root x-data on the layout. The persistent sidebar collapse state is stored in localStorage under the key sb.
Blade views in modules use the namespace registered in the service provider:
// In ServiceProvider
$this->loadViewsFrom(__DIR__ . '/../views', 'my-module');// In controller
return view('my-module::index', compact('data'));Extend the main layout:
@extends('layouts.app')
@section('title', 'My Module Page')
@section('content')
<div class="max-w-4xl mx-auto">
{{-- your content --}}
</div>
@endsection- Create a Form Request in
app/Http/Requests/ - Create a controller in
app/Http/Controllers/Api/V1/ - Add the route in
routes/api.phpinside thev1group - Use
auth:sanctummiddleware (already applied to the group) - Write a Feature test
Always use Form Requests, never validate inline in controllers:
class StoreWorkOrderRequest extends FormRequest
{
public function authorize(): bool
{
return $this->user()->hasRole('Admin');
}
public function rules(): array
{
return [
'order_no' => ['required', 'string', 'unique:work_orders,order_no'],
'quantity' => ['required', 'integer', 'min:1'],
'due_date' => ['nullable', 'date', 'after:today'],
];
}
}OpenMES uses Laravel's built-in testing framework. Tests live in backend/tests/.
# Run all tests
php artisan test
# Run a specific test file
php artisan test tests/Feature/WorkOrderApiTest.php
# Run with coverage report
php artisan test --coverageEvery feature must have tests covering:
- Happy path
- Validation errors (missing/invalid fields)
- Authorization (unauthenticated + wrong role)
- Edge cases
Example test:
class WorkOrderApiTest extends TestCase
{
use RefreshDatabase;
public function test_admin_can_create_work_order(): void
{
$admin = User::factory()->create()->assignRole('Admin');
$response = $this->actingAs($admin)->postJson('/api/v1/work-orders', [
'order_no' => 'WO-TEST-001',
'quantity' => 100,
]);
$response->assertCreated()
->assertJsonPath('data.order_no', 'WO-TEST-001');
$this->assertDatabaseHas('work_orders', ['order_no' => 'WO-TEST-001']);
}
public function test_operator_cannot_create_work_order(): void
{
$operator = User::factory()->create()->assignRole('Operator');
$this->actingAs($operator)
->postJson('/api/v1/work-orders', ['order_no' => 'WO-X', 'quantity' => 1])
->assertForbidden();
}
}Run before committing:
./vendor/bin/pintPint uses the Laravel preset (PSR-12 based). CI enforces formatting — PRs will fail if pint --test reports changes.
- No raw SQL with user input — always use Eloquent or Query Builder
- No inline validation in controllers — always use Form Requests
- No business logic in controllers — move to Services or model methods
app/Services/for reusable logic- Use transactions for multi-step DB operations
- Log security events (auth failures, permission denials)