Apariencia
Convenciones
Reglas del equipo. Las viola quien trabaja solo; las sigue quien trabaja en equipo.
Naming
| Artefacto | Patrón | Ejemplo |
|---|---|---|
| Modelo | Singular PascalCase | User, CandidateProfile, Vacancy |
| Tabla DB | plural snake_case | users, candidate_profiles, vacancies |
| Controller | {Resource}Controller | VacancyController, InterviewController |
| Controller en namespace por rol | bajo Api/V1/{Role}/ | Api\V1\Candidate\ProfileController |
| Form Request | {Action}{Resource}Request | CreateVacancyRequest, UpdateCandidateProfileRequest |
| API Resource | {Resource}Resource | UserResource, VacancyResource |
| Policy | {Model}Policy | VacancyPolicy |
| Service | {Domain}Service | MembershipService, InterviewService |
| State Machine | {Domain}StateMachine o StageMachine | VacancyStateMachine, AssignmentStageMachine |
| Enum | {Domain}State o {Domain}Type | CandidateState, InterviewMode |
| Migration | {timestamp}_{action}_{table} | 2026_01_15_create_vacancies_table |
| Test feature | tests/Feature/Api/V1/{Module}/{Action}Test.php | tests/Feature/Api/V1/Membership/CheckoutTest.php |
| Factory | {Model}Factory | VacancyFactory |
| Seeder | {Purpose}Seeder | RolesAndPermissionsSeeder, BigFiveQuestionsSeeder |
| Notification | {Event}Notification | MembershipActivatedNotification |
| Job | {Verb}{Object}Job | ExpireMembershipsJob |
| Mailable (si aplica) | {Purpose}Mail | — (usamos Notifications, no Mailables directos) |
Reglas que NO se negocian
- Modelos en singular.
VacancynoVacancies. - Nombres en inglés para modelos/clases/métodos, español en mensajes de validación y notificaciones.
- Sin abreviaciones —
VacancyAssignmentnoVacAsg. - snake_case para columnas, nunca
camelCaseen DB.
Contra-convenciones documentadas
Vacancy, noJob. Laravel usajobscomo tabla del queue driver. Renombramos para evitar colisión.CandidateProfile, noCandidate. El User es la persona; el CandidateProfile es el expediente.candidate_educations(nocandidate_education). Laravel pluraliza incorrectamente "education" como uncountable; forzamos el nombre conprotected $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 funcionalidadfix— bug fixrefactor— cambio de estructura sin cambiar comportamientotest— agregar/modificar testsdocs— documentaciónchore— deps, config, toolingperf— mejora de performancestyle— 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.3Reglas:
- 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 lintPint corre automáticamente en pre-commit hook (si lo configuras con husky o lefthook) y en CI.
Reglas clave
- Arrays cortos (
[]noarray()). 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 analyseLevel 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/seedersTests 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-baselineEl 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 --watchDetalle: 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 checkQue ejecuta:
composer lint:check(Pint)composer analyse(PHPStan 8)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.htmlScribe 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.phpAyuda 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 →

