Coding Standards

v1.0 Last updated: December 2025

Overview

This document defines the coding standards and best practices for the QR Igniter project. Adherence to these standards ensures code consistency, maintainability, and quality across all components.

Quality Gates

All code must pass automated linting and testing before merging. The GitLab CI pipeline enforces these standards automatically: Laravel Pint (--test), PHPStan/Larastan level 5, Pest 4 (--coverage --min=90), composer audit, and gitleaks — all blocking jobs on merge requests and pushes to main/develop.

PHP / Laravel Standards

PSR-12 Extended Coding Style

All PHP code follows PSR-12 with additional Laravel-specific conventions.

Naming Conventions

Element Convention Example
Classes PascalCase QrCodeService
Methods camelCase generateQrCode()
Properties camelCase $qrCodeConfig
Constants SCREAMING_SNAKE_CASE MAX_QR_SIZE
Variables camelCase $brandLogo
Database Tables snake_case (plural) qr_codes
Database Columns snake_case created_at

File Organization

// File header (optional, but consistent if used)
<?php

declare(strict_types=1);

namespace App\Services\QrCode;

use App\Models\QrCode;
use App\Services\Gs1\Gs1Service;
use Illuminate\Support\Facades\Storage;

/**
 * Service for generating QR codes with GS1 Digital Link compliance.
 */
class QrCodeService
{
    /**
     * Create a new service instance.
     */
    public function __construct(
        private readonly Gs1Service $gs1Service,
    ) {}

    /**
     * Generate a QR code image.
     *
     * @param QrCode $qrCode The QR code model
     * @return string The path to the generated image
     */
    public function generate(QrCode $qrCode): string
    {
        // Implementation
    }
}

Service Classes

  • Use constructor property promotion (PHP 8.x)
  • Declare strict types in all files
  • Use readonly properties where applicable
  • Return type declarations are mandatory
  • Parameter type declarations are mandatory

Eloquent Models

<?php

declare(strict_types=1);

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;

class QrCode extends Model
{
    use HasFactory;

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'campaign_id',
        'gtin',
        'batch_number',
        'serial_number',
        'destination_url',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'metadata' => 'array',
        'is_active' => 'boolean',
        'expires_at' => 'datetime',
    ];

    /**
     * Get the campaign that owns the QR code.
     */
    public function campaign(): BelongsTo
    {
        return $this->belongsTo(Campaign::class);
    }

    /**
     * Get the scans for the QR code.
     */
    public function scans(): HasMany
    {
        return $this->hasMany(Scan::class);
    }
}

Controllers

  • Use single-action controllers where appropriate
  • Use Form Requests for validation
  • Use API Resources for response transformation
  • Keep controllers thin - delegate to services
<?php

declare(strict_types=1);

namespace App\Http\Controllers\Api\V1;

use App\Http\Controllers\Controller;
use App\Http\Requests\Api\StoreQrCodeRequest;
use App\Http\Resources\QrCodeResource;
use App\Services\QrCode\QrCodeService;
use Illuminate\Http\JsonResponse;

class QrCodeController extends Controller
{
    public function __construct(
        private readonly QrCodeService $qrCodeService,
    ) {}

    /**
     * Store a newly created QR code.
     */
    public function store(StoreQrCodeRequest $request): JsonResponse
    {
        $qrCode = $this->qrCodeService->create($request->validated());

        return response()->json([
            'data' => new QrCodeResource($qrCode),
        ], 201);
    }
}

Dart / Flutter Standards

Effective Dart Guidelines

All Dart code follows Effective Dart guidelines.

Naming Conventions

Element Convention Example
Classes PascalCase QrScannerScreen
Extensions PascalCase StringExtension
Functions/Methods camelCase parseGs1Data()
Variables camelCase scanResult
Constants camelCase defaultTimeout
Files snake_case qr_scanner_screen.dart

Widget Structure

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../providers/scanner_provider.dart';

/// A screen for scanning QR codes.
class QrScannerScreen extends ConsumerStatefulWidget {
  const QrScannerScreen({super.key});

  @override
  ConsumerState<QrScannerScreen> createState() => _QrScannerScreenState();
}

class _QrScannerScreenState extends ConsumerState<QrScannerScreen> {
  @override
  Widget build(BuildContext context) {
    final scannerState = ref.watch(scannerProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('Scan QR Code'),
      ),
      body: scannerState.when(
        data: (result) => _buildScanner(),
        loading: () => const Center(child: CircularProgressIndicator()),
        error: (error, stack) => _buildError(error),
      ),
    );
  }

  Widget _buildScanner() {
    // Implementation
  }

  Widget _buildError(Object error) {
    // Implementation
  }
}

Riverpod Providers

import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../data/api/api_client.dart';
import '../data/models/scan_result.dart';

/// Provider for the API client.
final apiClientProvider = Provider<ApiClient>((ref) {
  return ApiClient();
});

/// Provider for scan results.
final scanResultProvider = FutureProvider.family<ScanResult, String>(
  (ref, qrCode) async {
    final client = ref.watch(apiClientProvider);
    return client.resolveScan(qrCode);
  },
);

File Organization

lib/
├── app/
│   ├── app.dart
│   ├── routes.dart
│   └── theme.dart
├── core/
│   └── constants/
│       ├── colors.dart
│       └── strings.dart
├── data/
│   ├── api/
│   │   └── api_client.dart
│   └── models/
│       └── scan_result.dart
├── presentation/
│   ├── screens/
│   │   └── scanner/
│   │       └── qr_scanner_screen.dart
│   └── widgets/
│       └── common/
│           └── loading_indicator.dart
├── providers/
│   └── scanner_provider.dart
└── services/
    └── gs1_parser_service.dart

Frontend Standards

HTML

  • Use semantic HTML5 elements
  • Include proper ARIA attributes for accessibility
  • Use lowercase for element names and attributes
  • Always include alt text for images
  • Use proper heading hierarchy (h1-h6)

CSS

  • Use CSS custom properties (variables) for theming
  • Follow BEM naming convention for classes
  • Mobile-first responsive design
  • Avoid !important except for utility classes
/* CSS Custom Properties */
:root {
    --primary: #F05A28;
    --primary-dark: #D8481A;
    --text: #1f2937;
    --bg: #ffffff;
}

/* BEM Naming */
.card { }
.card__header { }
.card__title { }
.card__body { }
.card--featured { }

/* Responsive Design */
.container {
    padding: 1rem;
}

@media (min-width: 768px) {
    .container {
        padding: 2rem;
    }
}

JavaScript

  • Use ES6+ features
  • Use const/let, never var
  • Use arrow functions for callbacks
  • Use template literals for string interpolation
  • Document functions with JSDoc
/**
 * Initialize search functionality.
 * @param {string} inputSelector - The search input selector
 * @param {string} resultsSelector - The results container selector
 */
const initSearch = (inputSelector, resultsSelector) => {
    const input = document.querySelector(inputSelector);
    const results = document.querySelector(resultsSelector);

    if (!input || !results) return;

    input.addEventListener('input', (event) => {
        const query = event.target.value.trim();
        if (query.length < 2) {
            results.classList.remove('active');
            return;
        }
        performSearch(query, results);
    });
};

Testing Standards

Test Coverage Requirements

Component Minimum Coverage Target Coverage
Backend Services 90% (CI enforced) 97.2% (current)
API Controllers 90% (CI enforced) 97.2% (current)
Flutter Services 80% 90%
Flutter Widgets 60% 80%

Test Naming

// PHP/Laravel - Pest syntax
it('generates a valid GS1 Digital Link URL', function () {
    // Arrange
    $qrCode = QrCode::factory()->create([
        'gtin' => '09506000134352',
    ]);

    // Act
    $url = $this->service->generateDigitalLink($qrCode);

    // Assert
    expect($url)->toContain('/01/09506000134352');
});

test('validation fails for invalid GTIN', function () {
    // ...
});
// Dart/Flutter
void main() {
  group('Gs1ParserService', () {
    test('should parse GTIN from Digital Link URL', () {
      // Arrange
      const url = 'https://example.com/01/09506000134352';

      // Act
      final result = Gs1ParserService.parse(url);

      // Assert
      expect(result.gtin, equals('09506000134352'));
    });

    test('should throw when URL is invalid', () {
      expect(
        () => Gs1ParserService.parse('invalid'),
        throwsA(isA<FormatException>()),
      );
    });
  });
}

Code Quality Tools

PHP Tools

Tool Purpose Command
Laravel Pint (enforced) Code formatting (PSR-12) ./vendor/bin/pint --test
PHPStan / Larastan (level 5) Static analysis with baseline ./vendor/bin/phpstan analyse --level=5
Pest 4 Testing with coverage floor (≥90%) php artisan test --coverage --min=90
Composer Audit Dependency vulnerability scanning composer audit
Gitleaks Secret / credential scanning gitleaks detect --source .

Dart/Flutter Tools

Tool Purpose Command
dart format Code formatting dart format .
dart analyze Static analysis dart analyze
flutter test Testing flutter test
flutter test --coverage Coverage report flutter test --coverage

Pre-commit Hooks

The project uses Git hooks to enforce standards before commit:

# .git/hooks/pre-commit
#!/bin/bash

# Run PHP formatting
./vendor/bin/pint --test

# Run PHP static analysis
./vendor/bin/phpstan analyse --memory-limit=256M

# Run Dart formatting check
cd flutter_app && dart format --set-exit-if-changed .

# Run tests
php artisan test --stop-on-failure

CI/CD Quality Gates

The GitLab CI pipeline enforces the following checks:

  • Code formatting — Laravel Pint (--test, enforced) and dart format
  • Static analysis — PHPStan/Larastan level 5 with baseline, dart analyze
  • All tests must pass — Pest 4 (php artisan test --coverage --min=90)
  • Minimum backend coverage 97.2% (≥90% enforced in CI)
  • No vulnerable dependencies — composer audit (blocking)
  • No leaked secrets — gitleaks detect (blocking)