Coding Standards
On This Page
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
alttext 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)