libra/docs/architecture/coding-standards.md

5.6 KiB

Libra - Coding Standards

Extracted from docs/architecture.md Section 20

Critical Rules

Rule Description
Volt Pattern Use class-based Volt components (per CLAUDE.md)
Flux UI Use Flux components when available
Form Validation Always use Form Request or Livewire form objects
Eloquent Prefer Model::query() over DB:: facade
Actions Use single-purpose Action classes for business logic
Testing Every feature must have corresponding Pest tests
Pint Run vendor/bin/pint --dirty before committing

Naming Conventions

Element Convention Example
Models Singular PascalCase Consultation
Tables Plural snake_case consultations
Columns snake_case booking_date
Controllers PascalCase + Controller ConsultationController
Actions Verb + Noun + Action CreateConsultationAction
Jobs Verb + Noun SendBookingNotification
Events Past tense ConsultationApproved
Listeners Verb phrase SendApprovalNotification
Volt Components kebab-case path admin/consultations/index
Enums PascalCase ConsultationStatus
Enum Cases PascalCase NoShow
Traits Adjective or -able HasConsultations, Bookable
Interfaces Adjective or -able Exportable

Volt Component Template

<?php
// resources/views/livewire/admin/consultations/index.blade.php

use App\Models\Consultation;
use App\Enums\ConsultationStatus;
use Livewire\Volt\Component;
use Livewire\WithPagination;

new class extends Component {
    use WithPagination;

    public string $search = '';
    public string $status = '';

    public function updatedSearch(): void
    {
        $this->resetPage();
    }

    public function updatedStatus(): void
    {
        $this->resetPage();
    }

    public function with(): array
    {
        return [
            'consultations' => Consultation::query()
                ->with('user:id,full_name,email')
                ->when($this->search, fn ($q) => $q->whereHas('user', fn ($q) =>
                    $q->where('full_name', 'like', "%{$this->search}%")
                ))
                ->when($this->status, fn ($q) => $q->where('status', $this->status))
                ->orderBy('booking_date', 'desc')
                ->paginate(15),
            'statuses' => ConsultationStatus::cases(),
        ];
    }
}; ?>

<div>
    <x-slot name="header">
        <flux:heading size="xl">{{ __('models.consultations') }}</flux:heading>
    </x-slot>

    <div class="flex gap-4 mb-6">
        <flux:input
            wire:model.live.debounce.300ms="search"
            placeholder="{{ __('messages.search') }}"
            icon="magnifying-glass"
        />

        <flux:select wire:model.live="status">
            <option value="">{{ __('messages.all_statuses') }}</option>
            @foreach($statuses as $s)
                <option value="{{ $s->value }}">{{ $s->label() }}</option>
            @endforeach
        </flux:select>
    </div>

    <div class="space-y-4">
        @forelse($consultations as $consultation)
            <x-ui.card wire:key="consultation-{{ $consultation->id }}">
                {{-- Card content --}}
            </x-ui.card>
        @empty
            <x-ui.empty-state :message="__('messages.no_consultations')" />
        @endforelse
    </div>

    <div class="mt-6">
        {{ $consultations->links() }}
    </div>
</div>

Action Class Template

<?php
// app/Actions/Consultation/CreateConsultationAction.php

namespace App\Actions\Consultation;

use App\Models\Consultation;
use App\Models\User;
use App\Enums\ConsultationStatus;
use App\Enums\PaymentStatus;
use App\Jobs\SendBookingNotification;
use App\Exceptions\BookingLimitExceededException;
use Illuminate\Support\Facades\DB;

class CreateConsultationAction
{
    public function execute(User $user, array $data): Consultation
    {
        // Validate booking limit
        if ($user->hasBookingOnDate($data['booking_date'])) {
            throw new BookingLimitExceededException(
                __('messages.booking_limit_exceeded')
            );
        }

        return DB::transaction(function () use ($user, $data) {
            $consultation = Consultation::create([
                'user_id' => $user->id,
                'booking_date' => $data['booking_date'],
                'booking_time' => $data['booking_time'],
                'problem_summary' => $data['problem_summary'],
                'status' => ConsultationStatus::Pending,
                'payment_status' => PaymentStatus::NotApplicable,
            ]);

            SendBookingNotification::dispatch($consultation);

            return $consultation;
        });
    }
}

Testing Standards

  • Use Pest 4 with Volt::test() for Livewire components
  • Every feature must have corresponding tests
  • Use factories with custom states when creating test models
  • Follow existing test structure in tests/Feature/ and tests/Unit/

Test Example

use Livewire\Volt\Volt;

test('consultation list can be filtered by status', function () {
    $user = User::factory()->admin()->create();

    Volt::test('admin.consultations.index')
        ->actingAs($user)
        ->set('status', ConsultationStatus::Pending->value)
        ->assertHasNoErrors();
});

Bilingual Support

  • All user-facing strings must use Laravel's __() helper
  • Translations stored in resources/lang/{ar,en}/
  • Use JSON translations for simple strings, PHP arrays for structured data
  • RTL layout handled via Tailwind CSS classes