Coding Standards
This document outlines the coding standards and best practices for contributing to BudgetControl.
General Principles
Code Quality
- Write clean, readable, and maintainable code
- Follow SOLID principles
- Use meaningful variable and function names
- Keep functions and classes small and focused
- Write self-documenting code
Documentation
- Document all public APIs
- Include inline comments for complex logic
- Update documentation when changing functionality
- Write clear commit messages
PHP Standards (Backend)
PSR Standards
We follow PHP-FIG standards:
- PSR-1: Basic Coding Standard
- PSR-2: Coding Style Guide (deprecated, use PSR-12)
- PSR-12: Extended Coding Style Guide
- PSR-4: Autoloader
Code Style
Class Structure
<?php
declare(strict_types=1);
namespace App\Services;
use App\Models\Entry;
use App\Repositories\EntryRepository;
use Illuminate\Support\Collection;
/**
* Service for handling entry operations
*/
final class EntryService
{
public function __construct(
private readonly EntryRepository $entryRepository
) {
}
public function createEntry(array $data): Entry
{
// Implementation
}
}
Method Naming
// Good
public function calculateTotalAmount(): float
public function findByWorkspaceId(string $workspaceId): Collection
public function isValidEntry(Entry $entry): bool
// Bad
public function calc(): float
public function get($id): Collection
public function check($entry): bool
Type Declarations
// Always use type declarations
public function processEntries(array $entries): Collection
public function calculateSum(float $amount, int $quantity): float
public function isActive(): bool
Laravel Specific
Controllers
<?php
namespace App\Http\Controllers;
class EntryController extends Controller
{
public function index(Request $request): JsonResponse
{
$entries = $this->entryService->getPaginated(
$request->get('page', 1),
$request->get('per_page', 30)
);
return response()->json($entries);
}
public function store(StoreEntryRequest $request): JsonResponse
{
$entry = $this->entryService->create($request->validated());
return response()->json($entry, 201);
}
}
Models
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Entry extends Model
{
protected $fillable = [
'amount',
'note',
'date_time',
'account_id',
'category_id',
];
protected $casts = [
'amount' => 'decimal:2',
'date_time' => 'datetime',
'confirmed' => 'boolean',
];
public function account(): BelongsTo
{
return $this->belongsTo(Account::class);
}
}
Validation
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
class StoreEntryRequest extends FormRequest
{
public function rules(): array
{
return [
'amount' => ['required', 'numeric'],
'note' => ['nullable', 'string', 'max:255'],
'account_id' => ['required', 'exists:accounts,id'],
'category_id' => ['required', 'exists:categories,id'],
];
}
}
JavaScript/Vue.js Standards (Frontend)
Vue.js Component Structure
<template>
<div class="entry-form">
<form @submit.prevent="handleSubmit">
<input
v-model="form.amount"
type="number"
placeholder="Amount"
required
>
<button type="submit" :disabled="isSubmitting">
{{ isSubmitting ? 'Saving...' : 'Save' }}
</button>
</form>
</div>
</template>
<script setup lang="ts">
import { ref, reactive } from 'vue'
import type { Entry } from '@/types/entry'
interface EntryForm {
amount: number
note: string
accountId: string
}
// Props
defineProps<{
entry?: Entry
}>()
// Emits
const emit = defineEmits<{
submit: [entry: EntryForm]
}>()
// State
const isSubmitting = ref(false)
const form = reactive<EntryForm>({
amount: 0,
note: '',
accountId: ''
})
// Methods
const handleSubmit = async (): Promise<void> => {
isSubmitting.value = true
try {
emit('submit', { ...form })
} finally {
isSubmitting.value = false
}
}
</script>
<style scoped>
.entry-form {
max-width: 400px;
margin: 0 auto;
}
</style>
TypeScript Standards
// Types
interface Entry {
uuid: string
amount: number
note?: string
dateTime: Date
confirmed: boolean
}
// API Service
class EntryService {
private readonly baseUrl = '/api/entries'
async getEntries(page = 1, perPage = 30): Promise<PaginatedResponse<Entry>> {
const response = await fetch(`${this.baseUrl}?page=${page}&per_page=${perPage}`)
if (!response.ok) {
throw new Error('Failed to fetch entries')
}
return response.json()
}
async createEntry(data: CreateEntryRequest): Promise<Entry> {
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(data),
})
if (!response.ok) {
throw new Error('Failed to create entry')
}
return response.json()
}
}
Naming Conventions
// Variables and functions: camelCase
const userBalance = 100
const calculateTotal = () => {}
// Constants: SCREAMING_SNAKE_CASE
const API_BASE_URL = 'https://api.example.com'
const MAX_ENTRIES_PER_PAGE = 50
// Components: PascalCase
// EntryForm.vue
// UserProfile.vue
// DashboardStats.vue
Database Standards
Migration Structure
<?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('entries', function (Blueprint $table) {
$table->id();
$table->uuid('uuid')->unique();
$table->decimal('amount', 10, 2);
$table->text('note')->nullable();
$table->timestamp('date_time');
$table->boolean('confirmed')->default(true);
$table->foreignId('account_id')->constrained();
$table->foreignId('category_id')->constrained();
$table->foreignId('workspace_id')->constrained();
$table->timestamps();
$table->softDeletes();
$table->index(['workspace_id', 'date_time']);
$table->index(['account_id', 'confirmed']);
});
}
public function down(): void
{
Schema::dropIfExists('entries');
}
};
Model Conventions
- Use singular table names
- Use
snake_casefor column names - Always include
created_at,updated_at - Use
soft deleteswhere appropriate - Add proper indexes for performance
API Standards
RESTful Design
GET /api/entries # List entries
POST /api/entries # Create entry
GET /api/entries/{id} # Get specific entry
PUT /api/entries/{id} # Update entry
DELETE /api/entries/{id} # Delete entry
Response Format
{
"data": {
"uuid": "123e4567-e89b-12d3-a456-426614174000",
"amount": -50.00,
"note": "Coffee shop",
"date_time": "2024-01-15T10:30:00Z"
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"version": "1.0"
}
}
Error Response Format
{
"error": {
"code": "VALIDATION_ERROR",
"message": "The given data was invalid.",
"details": {
"amount": ["The amount field is required."]
}
},
"meta": {
"timestamp": "2024-01-15T10:30:00Z",
"request_id": "abc123"
}
}
Testing Standards
Unit Tests
<?php
namespace Tests\Unit\Services;
use App\Services\EntryService;
use App\Repositories\EntryRepository;
use PHPUnit\Framework\TestCase;
use Mockery;
class EntryServiceTest extends TestCase
{
private EntryService $entryService;
private EntryRepository $entryRepository;
protected function setUp(): void
{
parent::setUp();
$this->entryRepository = Mockery::mock(EntryRepository::class);
$this->entryService = new EntryService($this->entryRepository);
}
public function test_can_create_entry(): void
{
// Arrange
$data = [
'amount' => -50.00,
'note' => 'Test entry',
'account_id' => 1,
];
$this->entryRepository
->shouldReceive('create')
->once()
->with($data)
->andReturn(new Entry($data));
// Act
$result = $this->entryService->createEntry($data);
// Assert
$this->assertInstanceOf(Entry::class, $result);
$this->assertEquals(-50.00, $result->amount);
}
protected function tearDown(): void
{
Mockery::close();
parent::tearDown();
}
}
Integration Tests
<?php
namespace Tests\Feature;
use Tests\TestCase;
use Illuminate\Foundation\Testing\RefreshDatabase;
class EntryControllerTest extends TestCase
{
use RefreshDatabase;
public function test_can_create_entry_via_api(): void
{
// Arrange
$user = User::factory()->create();
$workspace = Workspace::factory()->create(['user_id' => $user->id]);
$account = Account::factory()->create(['workspace_id' => $workspace->id]);
$data = [
'amount' => -50.00,
'note' => 'Test entry',
'account_id' => $account->id,
];
// Act
$response = $this->actingAs($user)
->postJson('/api/entries', $data);
// Assert
$response->assertStatus(201)
->assertJsonStructure([
'data' => [
'uuid',
'amount',
'note',
]
]);
$this->assertDatabaseHas('entries', [
'amount' => -50.00,
'note' => 'Test entry',
]);
}
}
Git Standards
Commit Messages
feat: add entry creation endpoint
fix: resolve balance calculation bug
docs: update API documentation
test: add unit tests for EntryService
refactor: extract common validation logic
style: fix code formatting issues
Branch Naming
feature/add-budget-alerts
bugfix/fix-currency-conversion
hotfix/security-patch
release/v2.1.0
Code Review Guidelines
What to Look For
- Code follows established patterns
- Proper error handling
- Security considerations
- Performance implications
- Test coverage
- Documentation updates
Review Checklist
- Code follows coding standards
- All tests pass
- No security vulnerabilities
- Performance impact considered
- Documentation updated
- Breaking changes noted
Tools and Automation
PHP Code Style
# Install PHP CS Fixer
composer require --dev friendsofphp/php-cs-fixer
# Run code style fixer
vendor/bin/php-cs-fixer fix
ESLint/Prettier (Frontend)
# Install ESLint and Prettier
npm install --save-dev eslint prettier @vue/eslint-config-typescript
# Run linting
npm run lint
npm run lint:fix
Pre-commit Hooks
# Install pre-commit
pip install pre-commit
# Install hooks
pre-commit install
# Configuration in .pre-commit-config.yaml
Following these standards ensures consistent, maintainable, and high-quality code across the BudgetControl project.