Skip to content

Convenciones

Reglas del equipo. Las viola quien trabaja solo; las sigue quien trabaja en equipo.

Naming

ArtefactoPatrónEjemplo
ModeloSingular PascalCaseUser, CandidateProfile, Vacancy
Tabla DBplural snake_caseusers, candidate_profiles, vacancies
Controller{Resource}ControllerVacancyController, InterviewController
Controller en namespace por rolbajo Api/V1/{Role}/Api\V1\Candidate\ProfileController
Form Request{Action}{Resource}RequestCreateVacancyRequest, UpdateCandidateProfileRequest
API Resource{Resource}ResourceUserResource, VacancyResource
Policy{Model}PolicyVacancyPolicy
Service{Domain}ServiceMembershipService, InterviewService
State Machine{Domain}StateMachine o StageMachineVacancyStateMachine, AssignmentStageMachine
Enum{Domain}State o {Domain}TypeCandidateState, InterviewMode
Migration{timestamp}_{action}_{table}2026_01_15_create_vacancies_table
Test featuretests/Feature/Api/V1/{Module}/{Action}Test.phptests/Feature/Api/V1/Membership/CheckoutTest.php
Factory{Model}FactoryVacancyFactory
Seeder{Purpose}SeederRolesAndPermissionsSeeder, BigFiveQuestionsSeeder
Notification{Event}NotificationMembershipActivatedNotification
Job{Verb}{Object}JobExpireMembershipsJob
Mailable (si aplica){Purpose}Mail— (usamos Notifications, no Mailables directos)

Reglas que NO se negocian

  • Modelos en singular. Vacancy no Vacancies.
  • Nombres en inglés para modelos/clases/métodos, español en mensajes de validación y notificaciones.
  • Sin abreviacionesVacancyAssignment no VacAsg.
  • snake_case para columnas, nunca camelCase en DB.

Contra-convenciones documentadas

  • Vacancy, no Job. Laravel usa jobs como tabla del queue driver. Renombramos para evitar colisión.
  • CandidateProfile, no Candidate. El User es la persona; el CandidateProfile es el expediente.
  • candidate_educations (no candidate_education). Laravel pluraliza incorrectamente "education" como uncountable; forzamos el nombre con protected $table.

Type safety

declare(strict_types=1) en todos los archivos

php
<?php

declare(strict_types=1);

namespace App\Services;
// ...

Pint lo agrega automáticamente si falta.

PHPDoc para generics y arrays

Cuando PHP no puede expresar un tipo (arrays asociativos, generics de Eloquent), usa PHPDoc:

php
/**
 * @return array{url: string, session_id: string, payment_id: int}
 */
public function createCheckoutSession(User $user, MembershipPlan $plan): array;

/**
 * @return Builder<Vacancy>
 */
public function scopeActive(Builder $query): Builder;

/**
 * @return HasMany<VacancyAssignment, $this>
 */
public function assignments(): HasMany
{
    return $this->hasMany(VacancyAssignment::class);
}

Laravel 12 covariance

Para relaciones en modelos Laravel 12, usa $this en vez de self en el segundo type param (HasMany<X, $this>). Laravel 11 usa self. Si copias código de tutoriales viejos, ajusta.

@property docblocks en modelos

PHPStan nivel 8 requiere type hints para métodos encadenados en modelos. Agrega un docblock completo:

php
/**
 * @property int $id
 * @property string $email
 * @property string|null $phone
 * @property string|null $avatar_url
 * @property UserStatus $status
 * @property \Carbon\Carbon|null $email_verified_at
 * @property \Carbon\Carbon $created_at
 * @property \Carbon\Carbon $updated_at
 * @property-read Collection<int, Role> $roles
 * @property-read CandidateProfile|null $candidateProfile
 */
class User extends Authenticatable { /*...*/ }

Commits

Conventional Commits obligatorio.

Formato: <type>(<scope>): <subject>

Types:

  • feat — nueva funcionalidad
  • fix — bug fix
  • refactor — cambio de estructura sin cambiar comportamiento
  • test — agregar/modificar tests
  • docs — documentación
  • chore — deps, config, tooling
  • perf — mejora de performance
  • style — formato (usado raramente; Pint lo hace automático)

Ejemplos:

feat(membership): add Stripe Checkout flow with idempotent webhook
fix(pipeline): prevent skipping stages in AssignmentStageMachine
refactor(services): extract CvGenerationService from controller
test(interviews): cover no_show transition from confirmada
docs(backend): add API rate limiting reference
chore(deps): bump laravel/framework to 12.3

Reglas:

  • Un commit = una sub-tarea. No mezcles "fix bug en auth" con "agregar endpoint de reports" en un solo commit.
  • Subject en imperativo — "add" no "added", "fix" no "fixed".
  • Subject ≤ 72 chars.
  • Body opcional con el "por qué" si no es obvio.

Linting — Laravel Pint

Config en pint.json. Preset: Laravel con extras.

bash
# Check (no modifica)
composer lint:check

# Fix (modifica)
composer lint

Pint corre automáticamente en pre-commit hook (si lo configuras con husky o lefthook) y en CI.

Reglas clave

  • Arrays cortos ([] no array()).
  • declare(strict_types=1); obligatorio.
  • Imports ordenados alfabéticamente.
  • Trailing commas en arrays multilínea.
  • PSR-12 base.

Si Pint formatea algo que no te gusta

No desactives la regla en un solo archivo. Discútelo con el equipo y cambia pint.json si hay consenso.

Análisis estático — PHPStan (Larastan)

Config en phpstan.neon. Nivel 8.

bash
composer analyse

Level 8 = máximo strictness. Detecta:

  • Propiedades no declaradas
  • Type mismatches
  • Null safety violations
  • Unused variables
  • Dead code

Scope

phpstan.neon aísla:

yaml
paths:
  - app
  - routes
  - database/factories
  - database/seeders

Tests están excluidos porque los helpers de Pest requieren plugin específico. Si usas pestphp/pest-plugin-type-coverage, podemos activarlo.

Ignorar un error (último recurso)

php
/** @phpstan-ignore-next-line */
$result = $this->service->doSomething();

Úsalo raramente. Si lo agregas, incluye un comentario del motivo.

Baseline

Si herramientas externas introducen errores que no puedes arreglar hoy, genera baseline:

bash
./vendor/bin/phpstan analyse --generate-baseline

El archivo phpstan-baseline.neon lista errores pre-existentes. No abusar — aíslalos y arréglalos con el tiempo.

Testing — Pest

bash
# Toda la suite
composer test

# Con cobertura (requiere Xdebug o pcov)
composer test:coverage

# Un archivo específico
./vendor/bin/pest tests/Feature/Api/V1/Membership/CheckoutTest.php

# Con filter
./vendor/bin/pest --filter "idempotent"

# En watch mode
./vendor/bin/pest --watch

Detalle: Testing.

Regla: features > units

Prefiere tests feature (contra endpoint real) que unit tests mockeados. El costo es casi el mismo en Pest (vía RefreshDatabase + SQLite in-memory) y el valor es mucho mayor.

Excepción: state machines + helpers puros (sin DB) → unit tests rápidos.

Pre-commit check

Antes de cada commit, corre:

bash
composer check

Que ejecuta:

  1. composer lint:check (Pint)
  2. composer analyse (PHPStan 8)
  3. composer test (Pest)

Si algo falla, no comitees hasta arreglar.

Scripts de composer.json

json
"scripts": {
    "dev": "php -r \"...\" artisan serve + queue + logs",
    "test": "pest",
    "test:coverage": "pest --coverage --min=70",
    "lint": "pint",
    "lint:check": "pint --test",
    "analyse": "phpstan analyse",
    "check": [
        "@lint:check",
        "@analyse",
        "@test"
    ],
    "docs": "php artisan scribe:generate"
}

API documentation — Scribe

bash
composer docs
# → public/docs/index.html

Scribe lee controllers + FormRequests y genera OpenAPI + HTML browseable.

Recomendado regenerar antes de cada deploy que cambie endpoints.

IDE Helper

bash
php artisan ide-helper:generate     # _ide_helper.php
php artisan ide-helper:models -W    # _ide_helper_models.php
php artisan ide-helper:meta         # .phpstorm.meta.php

Ayuda a PHPStorm / VS Code con autocomplete de Laravel. Los archivos generados están en .gitignore — cada dev los regenera localmente.

Docblocks en métodos públicos

Para métodos complejos, documenta el por qué no el qué:

php
// ❌ obvio — no aporta
/**
 * Activates a membership from a Checkout session.
 */
public function activateFromCheckoutSession(CheckoutSession $session): Payment;

// ✅ aporta contexto
/**
 * Marca el Payment como succeeded y crea la Membership asociada.
 * Es idempotente: si el Payment ya está en succeeded, devuelve sin crear.
 * Se invoca desde el webhook handler, que puede dispararse múltiples veces.
 */
public function activateFromCheckoutSession(CheckoutSession $session): Payment;

Siguiente

Modelo de datos (ERD + migraciones + enums + factories): Modelo de datos →

Manual de usuario HUMAE · Uso interno