Batiburrillo del programador
Cosas que me sirven y que sirven
- Creación de diagramas DBML
- Borrar archivos o directorios que comienzan por letra (A–Z/a–z)
- Guía de Semantic Versioning y Git Flow
- Análisis: Principios de Good Code en Laravel
- El vocabulario del canario: testing, infraestructura y por qué se confunden
Creación de diagramas DBML
Introduccción
Me gusta tener un cuadro de lo que tengo en Mysql. A los largo de los años, ha dio cambiando el formato, las utilidades. Lejos queda el insoportable Workbench de mysql.
Ahora puedo usar TablePlus, pero a la postre muchas veces eso se me queda corto para lo que me gusta.
Asi que ahor aprefioer mantener la logica guardada en formato DBML, lo cual me permite una visión más adecuada, y real de lo que hay y lo que no hay en la app, y que uso a menudo con DBDiagram
db2dbml
db2dbml es una puntenteherramienta para generar un fichero DBML contectando a un servidor o motores de basos de datos.
Un ejemplo abajo de como generar en local el diagrama de mi proyecto.
❯ dbdocs db2dbml mysql 'mysql://root:MYPASSWORD@localhost:3306/lowino?socketPath=/tmp/mysql.sock' -o notas/DBML/lowino.dbml
✔ Connecting to database... done.
✔ Generating DBML... done
✔ Wrote to notas/DBML/lowino.dbml
Tras eso tenemos el codigo en el fichero
Table "advert_properties" {
"id" "bigint unsigned" [pk, not null, increment]
"advert_id" binary(26) [not null]
"property_id" "bigint unsigned" [not null]
"value" text
"created_at" timestamp
"updated_at" timestamp
}
Table "adverts" {
"id" binary(26) [unique, not null]
"user_id" binary(26) [not null]
"category_code" "mediumint unsigned" [not null, default: 1, note: 'without classification']
"title" varchar(80) [not null]
"slug" varchar(80) [unique, not null, note: 'Integrate -YYMMDDNN']
"advert" text [not null]
"price" "bigint unsigned"
"currency_id" "tinyint unsigned" [not null, default: 1]
"details" json
"published_at" timestamp
"expire_at" timestamp
"created_at" timestamp
"updated_at" timestamp
"deleted_at" timestamp
Indexes {
published_at [type: btree, name: "adverts_published_at_index"]
}
}
...
Y con ello ya podemos hacwer y deshacer como queramos, para crear nuestro digrama.
Cierto que tambien TablePlus es casi más eficaz en términos visuales, pero el dbml sirve para nuchas más cosas.
Aviso
Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris
Si necesitas soporte profesional puedes contratar con Castris soporte profesional.
Borrar archivos o directorios que comienzan por letra (A–Z/a–z)
Objetivo
Eliminar todos los archivos o carpetas cuyo nombre comience por una letra, sin distinguir mayúsculas/minúsculas.
Ideal para limpiezas de entorno sin afectar archivos ocultos ni aquellos que comiencen por números o símbolos.
🖥️ En macOS (Zsh)
Zsh no usa shopt, pero tiene su propia sintaxis de globbing avanzada.
1. Ver qué se va a borrar (modo seguro)
setopt extended_glob
ls -d (#i)[[:alpha:]]*
- Eliminar los archivos y carpetas
setopt extended_glob
rm -Rf (#i)[[:alpha:]]*
#### 🧠 Explicación técnica
- setopt extended_glob activa los patrones avanzados de Zsh.
- (#i) activa insensibilidad a mayúsculas para todo el patrón.
- [[:alpha:]]* coincide con cualquier nombre que empiece por letra (A–Z o a–z), de forma portable.
🔍 Ojo: patrones como (#i)[a-z]* no siempre funcionan como se espera, porque [a-z] sigue limitado al rango ASCII explícito. Usar [[:alpha:]] es más robusto.
🐧 En Linux (Bash)
Bash usa shopt para habilitar coincidencias insensibles a mayúsculas.
- Ver qué se va a borrar
shopt -s nocaseglob
ls -d [a-zA-Z]*
shopt -u nocaseglob
- Eliminar los archivos y carpetas
shopt -s nocaseglob
rm -Rf [a-zA-Z]*
shopt -u nocaseglob
🧠 Explicación técnica
- nocaseglob hace que Bash trate los patrones como insensibles a mayúsculas.
- [a-zA-Z]* coincide con nombres que comienzan por cualquier letra.
⚠️ Advertencias No borra archivos ocultos (.env, .git, etc.) ni nombres que empiecen con números (2023-img.png).
Usa ls antes de rm -Rf para confirmar qué se eliminará.
Si necesitas excluir ciertos nombres, puedes añadir filtros con grep -v, find, o usar listas de exclusión.
🔄 Alternativas útiles
### Mover en lugar de borrar:
mkdir -p backup_letters
mv [a-zA-Z]* backup_letters/
Ver el tamaño de los elementos que serán eliminados:
du -sh [a-zA-Z]*
Aviso
Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris
Si necesitas soporte profesional puedes contratar con Castris soporte profesional.
Guía de Semantic Versioning y Git Flow
Semantic Versioning (SemVer)
Formato: MAJOR.MINOR.PATCH (ejemplo: 1.2.3)
¿Cuándo incrementar cada número?
MAJOR (1.x.x → 2.x.x)
Cambios que ROMPEN compatibilidad hacia atrás (Breaking Changes)
Ejemplos:
- Cambiar la firma de un método público
- Eliminar un método o clase pública
- Cambiar el comportamiento de un método de forma incompatible
- Renombrar propiedades públicas
// v1.x.x
public function base100Attributes(): array
// v2.0.0 - BREAKING CHANGE
public function base100Attributes(): Collection // Cambió el return type
Impacto: Los usuarios DEBEN revisar su código antes de actualizar.
MINOR (x.1.x → x.2.x)
Nuevas características que SÍ son compatibles hacia atrás
Ejemplos:
- Añadir un nuevo método público
- Añadir un nuevo trait
- Añadir parámetros opcionales a métodos existentes
- Añadir nuevas opciones de configuración
// v1.1.0
class Base100 implements CastsAttributes
{
// Métodos existentes...
// NUEVO método añadido
public function withPrecision(int $decimals): self
{
// ...
}
}
Impacto: Los usuarios pueden actualizar sin cambios en su código.
PATCH (x.x.1 → x.x.2)
Correcciones de bugs que NO cambian funcionalidad
Ejemplos:
- Corregir un bug
- Mejorar rendimiento sin cambiar API
- Actualizar documentación
- Refactoring interno sin cambios en API pública
- Corregir tests
// v1.1.0 - Bug: redondeo incorrecto
return (int) $value * 100; // Error
// v1.1.1 - Patch: corregir redondeo
return (int) round($value * 100); // Correcto
Impacto: Actualización segura, solo mejoras y correcciones.
Estrategia de Branches
Branches Principales
1. main (Branch Principal)
- Propósito: Código estable y listo para producción
- Protección: Protected (no push directo)
- Contiene: Solo código que ha pasado tests y revisión
- Tags: Todas las releases se tagean desde aquí
2. develop (Branch de Desarrollo) - OPCIONAL
- Propósito: Integración de features antes de release
- Uso: Si tienes múltiples features en paralelo
- Para este proyecto: NO necesario (proyecto pequeño)
Branches de Trabajo
Feature Branches: feature/*
Para nuevas características
feature/add-precision-option
feature/base1000-support
feature/custom-rounding
Ejemplo de uso:
# Crear feature branch desde main
git checkout main
git pull origin main
git checkout -b feature/add-precision-option
# Trabajar en la feature...
git add .
git commit -m "feat: add precision option to Base100 cast"
# Cuando esté listo
git push origin feature/add-precision-option
# Crear Pull Request en GitHub
Bugfix Branches: fix/*
Para correcciones de bugs
fix/rounding-precision
fix/null-handling
fix/trait-initialization
Ejemplo de uso:
# Crear bugfix branch
git checkout main
git checkout -b fix/rounding-precision
# Corregir el bug
git add .
git commit -m "fix: correct rounding precision in Base100 cast"
# Push y PR
git push origin fix/rounding-precision
Hotfix Branches: hotfix/*
Para bugs CRÍTICOS en producción
hotfix/security-vulnerability
hotfix/data-corruption
Diferencia con fix/*:
- Hotfix: Bug crítico que necesita release inmediata
- Fix: Bug normal que puede esperar al próximo release
Release Branches: release/* - OPCIONAL
Para preparar una release
release/1.2.0
release/2.0.0
Uso: Solo si necesitas "congelar" features antes de release.
Workflow de Desarrollo
Opción 1: GitHub Flow (RECOMENDADO para este proyecto)
main (protegido)
↑
└── feature/nueva-caracteristica (trabajo aquí)
└── fix/bug-menor (trabajo aquí)
Ventajas:
- Simple y directo
- Ideal para proyectos pequeños-medianos
- Deployments/releases frecuentes
Flujo:
-
Crear branch desde
main -
Desarrollar la feature/fix
-
Abrir Pull Request
-
Code review + Tests automáticos (GitHub Actions)
-
Merge a
main -
Tag y release
Opción 2: Git Flow (Para proyectos grandes)
main (producción)
↑
develop (desarrollo)
↑
├── feature/feature-1
├── feature/feature-2
└── release/1.2.0
Ventajas:
- Mejor para equipos grandes
- Releases planificadas
- Múltiples versiones en paralelo
Desventaja:
- Más complejo
- Overkill para proyectos pequeños
Proceso de Release
Release MINOR (nueva feature - 1.1.0 → 1.2.0)
# 1. Asegurarte que main está actualizado
git checkout main
git pull origin main
# 2. Actualizar CHANGELOG.md
## 1.2.0 - 2025-10-15
### Added
- New `withPrecision()` method for custom decimal precision
- Support for negative values in HasBase100 trait
### Fixed
- Rounding precision issue in edge cases
# 3. Commit el changelog
git add CHANGELOG.md
git commit -m "docs: update changelog for v1.2.0"
# 4. Crear tag
git tag -a v1.2.0 -m "Release v1.2.0 - Add precision support"
# 5. Push tag y código
git push origin main
git push origin v1.2.0
# 6. Crear GitHub Release
gh release create v1.2.0 \
--title "v1.2.0 - Precision Support" \
--notes "See CHANGELOG.md for details"
Release PATCH (bugfix - 1.1.0 → 1.1.1)
# 1. Checkout main
git checkout main
git pull origin main
# 2. Actualizar CHANGELOG.md
## 1.1.1 - 2025-10-12
### Fixed
- Correct rounding precision when handling values > 1000
# 3. Commit y tag
git add CHANGELOG.md
git commit -m "docs: update changelog for v1.1.1"
git tag -a v1.1.1 -m "Release v1.1.1 - Fix rounding precision"
# 4. Push
git push origin main
git push origin v1.1.1
# 5. Release
gh release create v1.1.1 \
--title "v1.1.1 - Bugfix Release" \
--notes "Fix rounding precision issue"
Release MAJOR (breaking changes - 1.x.x → 2.0.0)
# 1. Crear rama para v2
git checkout main
git checkout -b release/2.0.0
# 2. Hacer cambios breaking
# ... código ...
# 3. Actualizar CHANGELOG.md con sección BREAKING CHANGES
Ejemplo de CHANGELOG para v2.0.0:
## 2.0.0 - 2025-11-01
### BREAKING CHANGES
- `base100Attributes()` now returns `Collection` instead of `array`
- Minimum PHP version raised to 8.4
- Removed deprecated `base100()` method
### Migration Guide
**Before (v1.x):**
```php
protected function base100Attributes(): array
{
return ['price', 'cost'];
}
After (v2.0):
protected function base100Attributes(): Collection
{
return collect(['price', 'cost']);
}
Added
- New
Base100Collectionclass - Support for custom transformers
Continuación del proceso:
```bash
# 4. Mergear a main
git checkout main
git merge release/2.0.0
# 5. Tag y release
git tag -a v2.0.0 -m "Release v2.0.0 - Major overhaul"
git push origin main
git push origin v2.0.0
# 6. GitHub Release con ADVERTENCIA
gh release create v2.0.0 \
--title "v2.0.0 - BREAKING CHANGES" \
--notes "See CHANGELOG.md for migration guide"
Ejemplos Prácticos
Ejemplo 1: Añadir nueva feature (MINOR)
Escenario: Quieres añadir soporte para Base1000
# 1. Crear feature branch
git checkout main
git checkout -b feature/base1000-support
# 2. Desarrollar la feature
# - Crear src/Casts/Base1000.php
# - Añadir tests
# - Actualizar README
# 3. Commits durante desarrollo
git add .
git commit -m "feat: add Base1000 cast class"
git add .
git commit -m "test: add Base1000 tests"
git add .
git commit -m "docs: document Base1000 usage"
# 4. Push y crear PR
git push origin feature/base1000-support
gh pr create --title "Add Base1000 support" --body "Adds support for base-1000 conversions"
# 5. Después de aprobación y merge
git checkout main
git pull origin main
# 6. Release como 1.2.0 (MINOR - nueva feature)
git tag -a v1.2.0 -m "Release v1.2.0 - Add Base1000 support"
git push origin v1.2.0
gh release create v1.2.0
Ejemplo 2: Corregir bug (PATCH)
Escenario: Hay un bug en el redondeo
# 1. Crear fix branch
git checkout main
git checkout -b fix/rounding-issue
# 2. Corregir el bug
# - Editar src/Casts/Base100.php
# - Añadir test que reproduce el bug
# - Verificar que el test pasa
# 3. Commit
git add .
git commit -m "fix: correct rounding for values > 10000"
# 4. Push y PR
git push origin fix/rounding-issue
gh pr create --title "Fix rounding issue" --body "Fixes #42"
# 5. Después del merge
git checkout main
git pull origin main
# 6. Release como 1.1.1 (PATCH - bugfix)
git tag -a v1.1.1 -m "Release v1.1.1 - Fix rounding issue"
git push origin v1.1.1
gh release create v1.1.1
Ejemplo 3: Hotfix crítico (PATCH urgente)
Escenario: Descubriste un bug que causa pérdida de datos
# 1. Crear hotfix branch DESDE main
git checkout main
git checkout -b hotfix/data-loss-prevention
# 2. Corregir RÁPIDAMENTE
# - Solo el fix necesario, nada más
# - Test mínimo que demuestre el fix
# 3. Commit
git add .
git commit -m "fix: prevent data loss in null handling (critical)"
# 4. Merge DIRECTO a main (sin PR si es muy urgente)
git checkout main
git merge hotfix/data-loss-prevention
# 5. Release INMEDIATA
git tag -a v1.1.2 -m "Release v1.1.2 - Critical hotfix"
git push origin main
git push origin v1.1.2
gh release create v1.1.2 --title "v1.1.2 - Critical Hotfix"
# 6. Notificar usuarios en GitHub/Packagist
Comandos Git Útiles
Gestión de Branches
# Ver todas las branches
git branch -a
# Eliminar branch local
git branch -d feature/mi-feature
# Eliminar branch remoto
git push origin --delete feature/mi-feature
# Actualizar main desde remoto
git checkout main && git pull origin main
# Crear branch desde un commit específico
git checkout -b fix/bug abc1234
Gestión de Tags
# Listar todos los tags
git tag
# Ver detalles de un tag
git show v1.0.0
# Eliminar tag local
git tag -d v1.0.0
# Eliminar tag remoto
git push origin --delete v1.0.0
# Crear tag desde un commit antiguo
git tag -a v1.0.1 abc1234 -m "Release v1.0.1"
Revertir Cambios
# Revertir un commit (crea nuevo commit)
git revert abc1234
# Revertir último commit (antes de push)
git reset --soft HEAD~1
# Descartar cambios locales
git checkout -- archivo.php
Checklist de Release
Antes de Release
- Todos los tests pasan (
composer test) - PHPStan sin errores (
composer phpstan) - Código formateado (
composer format) - CHANGELOG.md actualizado
- README.md actualizado (si hay cambios en uso)
- Versión en composer.json coincide? (NO - Packagist lo maneja)
- Pull Request revisado y aprobado
Durante Release
- Main actualizado (
git pull origin main) - Tag creado con mensaje descriptivo
- Tag pusheado a GitHub
- GitHub Release creada con notas
- Packagist se actualizó automáticamente (webhook)
Después de Release
- Verificar que aparece en Packagist
- Badges del README actualizados
- Anunciar en redes/comunidad (si es relevante)
- Crear issues/milestones para próxima versión
Recomendaciones Específicas
Estrategia Recomendada
Para proyecto pequeño:
-
Usar GitHub Flow (simple)
-
Main siempre deployable
-
Feature branches para TODO
-
Pull Requests siempre (aunque seas solo tú - para CI)
-
Tags para cada release
Naming Conventions
# Features
feature/add-base1000
feature/custom-precision
feature/collection-support
# Fixes
fix/rounding-precision
fix/null-handling
fix/trait-initialization
# Hotfixes
hotfix/security-vulnerability
hotfix/data-corruption
# Docs
docs/update-readme
docs/add-examples
docs/api-documentation
# Chores
chore/update-dependencies
chore/ci-improvements
Commits Convencionales
feat: add new feature
fix: bug correction
docs: documentation only
style: formatting, no code change
refactor: code restructure
test: add/update tests
chore: maintenance tasks
perf: performance improvements
ci: CI/CD changes
# Ejemplos:
git commit -m "feat: add withPrecision() method"
git commit -m "fix: correct rounding for negative values"
git commit -m "docs: add usage examples to README"
Recursos Adicionales
- Semantic Versioning: https://semver.org/
- Git Flow: https://nvie.com/posts/a-successful-git-branching-model/
- GitHub Flow: https://guides.github.com/introduction/flow/
- Conventional Commits: https://www.conventionalcommits.org/
Preguntas Frecuentes
¿Cuándo hago MAJOR vs MINOR?
MAJOR (2.0.0): Si un usuario actualiza y su código se ROMPE → MAJOR
MINOR (1.1.0): Si un usuario actualiza y todo sigue funcionando → MINOR
¿Debo crear branch para cada pequeño cambio?
SÍ. Siempre trabaja en branches, incluso para cambios pequeños:
- Permite que GitHub Actions verifique antes de merge
- Historial más limpio
- Puedes descartar fácilmente si algo sale mal
¿Cuándo hacer release?
Flexible, pero algunas guías:
- PATCH: Cuando tengas 1+ bugfix importante
- MINOR: Cuando completes 1+ nueva feature
- MAJOR: Cuando hagas breaking changes (con cuidado)
Frecuencia recomendada:
- Patches: Cada 1-2 semanas
- Minor: Cada 1-2 meses
- Major: Solo cuando sea necesario
¿Puedo cambiar un tag después de crearlo?
NO recomendado una vez pusheado. Si lo haces:
- Los usuarios que ya instalaron la versión tendrán problemas
- Packagist se confunde
- Rompe la confianza
Si DEBES hacerlo:
# Eliminar tag
git tag -d v1.0.0
git push origin --delete v1.0.0
# Crear nuevo tag
git tag -a v1.0.0 nuevo_commit -m "..."
git push origin v1.0.0
Última actualización: 2025-10-10
Autor: Abdelkarim Mateos Sanchez
Aviso
Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris
Si necesitas soporte profesional puedes contratar con Castris soporte profesional.
Análisis: Principios de Good Code en Laravel
Contexto
Se plantean seis reglas fundamentales basadas en el Laravel Starter Kit de Nuno Maduro. El desafío no es solo aplicarlas en proyectos nuevos, sino integrar esta filosofía en proyectos existentes.
Cambio de paradigma fundamental
Tesis central: La flexibilidad de Laravel no debe confundirse con libertad para escribir código descuidado. La strictness no es un obstáculo, es un acelerador.
Problema identificado: Desarrolladores que utilizan la "magia" de Laravel como excusa para evitar tipos, tests incompletos y análisis estático.
Análisis de los seis principios
Principio 1: Cobertura de tipos al 100%
Qué implica:
pest --type-coverage --min=100
Análisis crítico:
Este principio fuerza la intencionalidad en cada variable y parámetro. La cobertura de tipos no es documentación, es contrato.
Ventajas:
- Reduce bugs de integración en un 60-70% según estudios empíricos
- El IDE se convierte en aliado activo
- Refactorizaciones seguras sin miedo
Desventajas:
- Requiere tiempo inicial significativo en proyectos legacy
- Puede generar fricción con equipos acostumbrados a PHP dinámico
- Arrays asociativos complejos necesitan DTOs o Value Objects
Implementación gradual en proyectos existentes:
- Activar cobertura de tipos sin bloquear CI inicialmente
- Establecer baseline actual
- Regla: nuevo código debe tener 100% de cobertura
- Refactorizar módulos críticos primero
- Incrementar baseline mensualmente
Relación con SOLID:
Dependency Inversion Principle se beneficia directamente. Interfaces tipadas fuerzan contratos claros.
Principio 2: PHPStan en nivel MAX
Qué implica:
Análisis estático sin concesiones. PHPStan nivel 9 (MAX) detecta:
- Dead code
- Posibles nulls no manejados
- Tipos incompatibles en operaciones
- Propiedades no inicializadas
Análisis crítico:
El nivel MAX expone deuda técnica oculta. No es ego, es disciplina.
Ventajas:
- Detecta bugs antes de testing
- Complementa cobertura de tipos
- Fuerza a pensar en edge cases
Desventajas:
- Output inicial puede ser abrumador en proyectos legacy
- Requiere aprendizaje de sus reglas
- Algunas reglas pueden requerir baseline temporal
Implementación gradual:
# Paso 1: Generar baseline
vendor/bin/phpstan analyse --generate-baseline
# Paso 2: Subir nivel progresivamente
# phpstan.neon
parameters:
level: 5 # Empezar aquí, no en MAX
# Paso 3: Incrementar cada sprint
# Sprint 1: nivel 5
# Sprint 2: nivel 6
# Sprint N: nivel 9 (MAX)
Relación con Testing:
PHPStan reduce la necesidad de ciertos tests unitarios. Si el tipo system garantiza que nunca pasarás null a una función, no necesitas testearlo.
Principio 3: Cobertura de tests al 100%
Qué implica:
pest --parallel --coverage --exactly=100.0
Análisis crítico:
La palabra clave es "exactly". No 99.8%. Exactamente 100%.
Ventajas:
- Elimina código muerto rápidamente
- Tests paralelos aceleran feedback loop
- Confianza absoluta en refactorizaciones
Desventajas:
- 100% coverage no garantiza calidad de tests
- Puede incentivar tests superficiales solo por métricas
- Requiere infraestructura para tests paralelos confiables
La trampa del 100%:
Cobertura de líneas no es cobertura de comportamiento. Un test puede ejecutar línea sin validar su lógica.
Mejor enfoque:
Combinar cobertura de líneas con mutation testing:
# Infection PHP
vendor/bin/infection --min-msi=80
Implementación gradual:
- Identificar módulos críticos de negocio
- Llevar estos a 100% primero
- Expandir a módulos de soporte
- Mantener 100% en código nuevo desde día 1
Relación con SOLID:
Single Responsibility Principle facilita testing. Clases con una responsabilidad son más fáciles de testear al 100%.
Principio 4: Formateo estricto automático
Qué implica:
Laravel Pint + Prettier eliminan decisiones de estilo.
Análisis crítico:
Este es el principio más subestimado. No es cosmético, es cognitivo.
Ventajas:
- Cero tiempo en code reviews discutiendo formato
- Diffs limpios enfocados en lógica
- Onboarding más rápido
Desventajas:
- Puede generar diffs masivos en primera aplicación
- Requiere consenso del equipo
- Algunos desarrolladores lo perciben como pérdida de "estilo personal"
Implementación inmediata:
# Aplicar una sola vez
./vendor/bin/pint
# Pre-commit hook
# .git/hooks/pre-commit
./vendor/bin/pint --dirty
Este principio no tiene desventajas técnicas reales. Solo resistencia humana.
Principio 5: Control del entorno en tests
Qué implica:
- Congelar tiempo
- Fake HTTP calls
- Forzar HTTPS en tests
Análisis crítico:
Tests deben ser deterministas. El mundo exterior es no determinista.
Ventajas en Laravel:
// Determinismo temporal
Carbon::setTestNow('2025-01-15 10:00:00');
// Aislamiento de HTTP
Http::fake([
'api.external.com/*' => Http::response(['data' => 'fake'], 200)
]);
Desventajas:
- Requiere disciplina para identificar qué fakear
- Tests de integración reales siguen siendo necesarios
- Puede ocultar problemas de configuración real
Implementación:
Separar claramente:
- Unit tests: todo fakeado
- Integration tests: servicios reales en ambiente controlado
- E2E tests: flujo completo
Relación con Testing:
Este principio hace posible el 100% de cobertura. Sin él, tests son frágiles y lentos.
Principio 6: CI como senior engineer más estricto
Qué implica:
GitHub Actions ejecutando:
- Pint (formato)
- Rector (refactoring automático)
- PHPStan (análisis estático)
- Pest (tests)
- Lint (sintaxis)
Análisis crítico:
CI no es opcional. Es el guardián de calidad 24/7.
Ventajas:
- Feedback inmediato en PRs
- Imposible mergear código que rompe estándares
- Documentación viva de estándares del equipo
Desventajas:
- Requiere configuración inicial
- Puede ralentizar merges si CI es lento
- Falsos positivos pueden frustrar
Implementación óptima:
# .github/workflows/tests.yml
name: Tests
on: [push, pull_request]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: 8.3
coverage: xdebug
- name: Install Dependencies
run: composer install
- name: Run Pint
run: ./vendor/bin/pint --test
- name: Run PHPStan
run: ./vendor/bin/phpstan analyse
- name: Run Pest
run: ./vendor/bin/pest --parallel --coverage --min=100
Estrategia gradual:
- Empezar solo con tests
- Agregar Pint
- Agregar PHPStan con baseline
- Subir nivel progresivamente
El error fundamental identificado
Cita clave: "Confundir flexibilidad con libertad"
Laravel es flexible en arquitectura, no en disciplina. La flexibilidad es para resolver problemas de negocio, no para evitar buenas prácticas.
Análisis del error
Manifestaciones comunes:
- Arrays asociativos en lugar de DTOs
- Métodos sin type hints "porque total funciona"
- Tests solo de happy path
- "Lo arreglo después" que nunca llega
Costo real:
- Debugging que consume 40-60% del tiempo de desarrollo
- Bugs en producción que dañan reputación
- Refactorizaciones imposibles sin miedo
- Onboarding lento de nuevos desarrolladores
Roadmap de implementación en proyectos existentes
Fase 1: Evaluación y baseline (Sprint 1-2)
Acciones:
- Ejecutar PHPStan nivel 5 con baseline
- Medir cobertura de tipos actual
- Medir cobertura de tests actual
- Aplicar Pint una sola vez
- Documentar estado actual
Entregable: Documento con métricas actuales y brechas.
Fase 2: Quick wins (Sprint 3-4)
Acciones:
- Configurar Pint en pre-commit hook
- Implementar CI básico (tests + Pint)
- Nueva regla: código nuevo debe cumplir 100% tipos y tests
- Identificar 3 módulos críticos para refactorizar
Entregable: CI funcionando, código nuevo bajo estándares.
Fase 3: Refactoring progresivo (Sprint 5-12)
Acciones:
- Refactorizar módulos críticos a 100% tipos y tests
- Subir PHPStan un nivel cada 2 sprints
- Reducir baseline de PHPStan 10% cada sprint
- Code reviews enfocados en principios
Entregable: Módulos críticos bajo estándares, PHPStan nivel 7-8.
Fase 4: Consolidación (Sprint 13+)
Acciones:
- PHPStan nivel MAX sin baseline
- 100% cobertura en todo el proyecto
- Mutation testing implementado
- Documentación de arquitectura
Entregable: Proyecto completamente bajo estándares.
Comparación: Enfoque tradicional vs Good Code
| Aspecto | Tradicional | Good Code |
|---|---|---|
| Types | Opcional | Obligatorio 100% |
| Tests | "Lo importante" | 100% exacto |
| Análisis estático | Si da tiempo | PHPStan MAX |
| Formato | Discutido en CR | Automatizado |
| CI | Tests básicos | Guardián completo |
| Velocidad inicial | Rápida | Moderada |
| Velocidad sostenida | Decreciente | Creciente |
| Confianza deployment | Rezar | Saber |
Relación con SOLID
Single Responsibility Principle
Clases con una responsabilidad son más fáciles de tipar y testear al 100%.
Open/Closed Principle
Interfaces tipadas garantizan extensión sin modificación.
Liskov Substitution Principle
PHPStan MAX detecta violaciones automáticamente.
Interface Segregation Principle
Cobertura de tipos fuerza interfaces específicas.
Dependency Inversion Principle
Type hints en constructores formalizan inversión de dependencias.
Consideraciones finales
Pregunta clave
¿Es esto overkill para proyectos pequeños?
Respuesta: Depende del horizonte temporal. Si el proyecto vivirá más de 6 meses, no es overkill. Es inversión.
Obstáculos reales
- Resistencia del equipo: Cambio cultural requiere tiempo
- Tiempo inicial: Primera implementación consume sprints
- Falsa sensación de lentitud: Strictness se siente lenta hasta que debuggear se vuelve raro
Beneficios medibles
- Reducción de bugs en producción: 60-80%
- Tiempo de onboarding: -50%
- Confianza en refactoring: +200%
- Velocidad de features (después de 3 meses): +30%
Conclusión
La filosofía de Good Code no es una lista de herramientas. Es un cambio de mentalidad: de "funciona ahora" a "funciona siempre".
El starter kit de Nuno Maduro no es el objetivo. Es el ejemplo de que es posible. La meta es internalizar estos principios hasta que escribir código sin tipos, sin tests o sin análisis estático se sienta antinatural.
Tesis final: Strictness no es burocracia. Es la única forma conocida de escalar complejidad sin colapsar en caos.
Recursos
- Laravel Starter Kit de Nuno Maduro
- PHPStan Documentation
- Pest PHP Documentation
- Laravel Pint Documentation
- Principios SOLID aplicados a Laravel - Opinated
- Agradecimientos a Vishal Rajpurohit por su hilo conductor
Aviso
Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris
Si necesitas soporte profesional puedes contratar con Castris soporte profesional.
El vocabulario del canario: testing, infraestructura y por qué se confunden
Introducción
Nota de alcance: este documento no es sobre pájaros. Es sobre un patrón de detección de fallo silencioso que aparece, con matices distintos, en código y en infraestructura — y sobre por qué llamar "canario" a cosas distintas genera confusión operativa real.
1. El origen: por qué la mina importa
Los mineros llevaban un canario porque el gas grisú (metano, monóxido de carbono) es indetectable para el ser humano hasta que ya es tarde. El canario, con metabolismo más sensible, dejaba de cantar — o moría — antes de que la concentración fuera letal para las personas. Su valor no estaba en sobrevivir; estaba en fallar de forma temprana, específica y observable ante una amenaza concreta que de otro modo se acumula sin ruido.
Esa es la propiedad que toda la terminología posterior intenta capturar: detectar la ausencia silenciosa de una protección, no el fallo ruidoso de una función.
Guárdate esa frase. Es el criterio que separa un canario de verdad de algo que solo se le parece.
2. Canario de test (código)
Contrato
Un canario de test no verifica que el sistema funcione. Verifica que una protección específica sigue activa. Estructura:
- Construyes deliberadamente el caso prohibido (usuario sin permiso, input inválido, estado no autorizado).
- Afirmas que un guard concreto lo rechaza.
- La aserción identifica cuál guard actuó, no solo que "algo" rechazó.
Canario ≠ test negativo
Se solapan pero no son lo mismo. La diferencia está en la intención y, por tanto, en qué necesita afirmar el test:
| Test negativo (genérico) | Canario | |
|---|---|---|
| Qué protege | El contrato de comportamiento: "ante input inválido, se rechaza" | La existencia de un mecanismo concreto: "el guard G sigue siendo quien rechaza" |
| Aserción típica | "La petición fue rechazada" | "La petición fue rechazada por G, identificable por una firma propia" |
Qué pasa si G desaparece pero otro mecanismo cubre el mismo caso |
Sigue en verde — y con razón, el contrato de comportamiento se cumple | Debería fallar, porque G ya no existe. Si sigue en verde, el canario está roto |
Todo canario es un test negativo. No todo test negativo es un canario. La confusión de categorías es la fuente de la mayoría de canarios que fallan en su propósito sin que nadie se entere.
3. El fallo silencioso: co-extensividad y tautología
Co-extensivo = dos guards cubren exactamente el mismo conjunto de casos. Todo lo que rechaza A también lo rechaza B.
El problema
Si escribes un canario para vigilar al guard A, pero B es co-extensivo con A en la ruta que pruebas, tu aserción ("la petición fue rechazada") no puede distinguir cuál de los dos actuó. Puedes eliminar A por completo — un refactor, una regresión, un merge descuidado — y el canario sigue en verde porque B sigue atrapando el mismo caso.
El test se ha vuelto tautológico: ya no puede fallar por la causa que dice vigilar. En términos de mutation testing: un test solo vale lo que valen los mutantes que mata. Si "matas" (eliminas) el guard A y el test no muere, el test no estaba probando A — estaba probando la propiedad más débil de "algo en esta ruta rechaza esto".
Ejemplo genérico
Ruta protegida por:
Guard A (autorización de dominio) → lanza ForbiddenException → 403
Guard B (validación de framework, más genérica) → lanza ValidationException → 403 también
Ambos devuelven 403. El canario que solo afirma response.status === 403 es tautológico: prueba la ruta, no prueba A.
Cómo se rompe la tautología: el discriminador
Un canario válido necesita un discriminador observable entre A y B. Puede ser:
- Tipo de excepción concreto (afirmar la clase, no solo el status code).
- Código de estado distinto entre los dos guards.
- Mensaje o payload de error con firma propia.
- Entrada de log/auditoría específica del mecanismo.
Caso real, generalizado: el defecto que sin querer arregló el canario
En un caso concreto, una excepción de un paquete de terceros (AuthorizationException del framework) no renderizaba el 403 canónico, sino un 500 — un defecto real, no un diseño intencional. El guard propio del dominio sí devolvía 403.
Efecto lateral: ese defecto rompió la co-extensividad observable entre ambos guards. El canario pudo afirmar el código de estado exacto y así saber con certeza cuál guard había actuado. El bug ajeno funcionó, sin buscarlo, como discriminador.
Advertencia importante si vas a documentar un caso así: esta validez es frágil, no robusta. Depende de un comportamiento no contractual de una dependencia externa. El día que el paquete corrija su excepción para devolver el 403 correcto, los dos guards vuelven a ser observablemente co-extensivos — y el canario vuelve a ser tautológico en silencio, sin que ningún test rompa para avisarte. La lección correcta no es "qué suerte que el bug nos ayudó", es "hay que sustituir ese discriminador accidental por uno propio antes de que alguien arregle el bug ajeno".
4. Canario en infraestructura: tres animales bajo el mismo nombre
Aquí es donde la terminología SysOp/DevOps se vuelve resbaladiza, porque "canario" se usa para tres cosas distintas y solo una de ellas hereda de verdad la semántica minera.
4.1 Sandbox desechable — esto normalmente NO es un canario minero
Levantas una VM/contenedor efímero, aplicas ahí un cambio de infraestructura (playbook, regla de firewall, imagen, migración) antes de tocar el entorno real, y compruebas que "funciona" o que "no rompe nada evidente".
Es un ensayo con doble de riesgo: una copia sacrificable donde fallar es barato. Tiene valor real, pero su lógica es la de un test negativo genérico como mucho ("¿el cambio rompe algo detectable?"), no la del canario. No vigila la desaparición silenciosa de un mecanismo concreto; solo da una oportunidad de fallar en un sitio que no duele.
Llamarlo "canario" no es grave, pero no aporta el rigor que el nombre promete.
4.2 Canary deployment / canary release — el uso correcto de "deployment canary"
Una instancia real de producción recibe una fracción del tráfico real antes que el resto de la flota. Si sus métricas se degradan, se frena el rollout.
Aquí sí hay vigilancia activa con consecuencia automática, pero el objeto vigilado es la salud general del sistema ante un cambio, no "¿sigue vivo el guard X?". Es un patrón legítimo y bien nombrado — pero resuelve un problema distinto al canario de test.
4.3 El canario minero real en infraestructura
Para que una prueba de infra tenga la misma estructura que el canario de código, necesita:
- Provisionarse deliberadamente para intentar la acción prohibida contra un mecanismo de defensa concreto (AppArmor/SELinux profile, política de firewall, IAM policy, seccomp, permisos POSIX).
- Afirmar que ese mecanismo específico — identificable por su firma propia — es quien la bloquea. No basta con "la operación falló".
Ejemplo: una política de AppArmor impide que un proceso escriba fuera de /var/lib/miapp. El canario correcto no levanta la VM y comprueba que "todo funciona bien" — intenta deliberadamente escribir fuera de esa ruta y afirma que AppArmor, y no otro mecanismo, generó el rechazo, verificando la entrada de auditoría específica de AppArmor (AVC denied en syslog, por ejemplo), no un Permission denied genérico que podría venir de cualquier sitio.
La misma trampa reaparece en infra
Si además del AppArmor profile hay un contenedor con seccomp restrictivo o un usuario sin permisos POSIX suficientes que también bloquearía esa escritura, un canario que solo comprueba "la escritura falló" es tautológico exactamente por la misma razón que en código: no distingues si AppArmor sigue activo o si alguien lo desactivó y el rechazo viene de otra capa. El grisú se acumula sin que el pájaro cante, porque otro mecanismo tapa el hueco por casualidad.
Tabla comparativa
| Sandbox desechable | Canary deployment | Canario minero (infra) | |
|---|---|---|---|
| Qué es | VM/contenedor de usar y tirar | Instancia real con tráfico parcial | Prueba deliberada de la acción prohibida |
| Qué vigila | Nada en particular — solo abarata el fallo | Salud general del sistema | Un guard concreto y su firma de rechazo |
| ¿Hereda la semántica minera? | No | Parcialmente | Sí, si tiene discriminador |
| Riesgo típico | Ninguno grave, solo falsa sensación de rigor | Detecta degradación, no ausencia de guard concreto | Tautológico si hay guards co-extensivos sin discriminador |
5. El test decisivo
Ante cualquier cosa etiquetada "canario" — código o infra — una sola pregunta la diagnostica:
¿Este test falla específicamente porque el guard G desapareció, o fallaría igual con G ausente por cualquier otra razón coincidente?
- Si la respuesta es "falla igual, no lo sé" → es un test negativo o un sandbox con nombre bonito. Útil, pero no un canario.
- Si la respuesta es "falla solo si G desaparece, porque afirmo su firma propia" → es un canario de verdad.
6. Por qué esto pesa más cuando eres SysOp + DevOps a la vez
Cuando una sola persona diseña guards en varias capas — autorización de aplicación, políticas del SO, reglas de red, IAM de infraestructura — la probabilidad de introducir co-extensividad accidental entre capas sube, no baja. Es fácil que un guard de aplicación y una política de sistema operativo terminen cubriendo el mismo caso sin que nadie lo diseñara así a propósito; cada capa se construyó pensando solo en la suya.
La disciplina que compensa esto no es "confiar menos en los guards", es exigir discriminador propio en cada canario que cruce capas: si un canario de infra y uno de aplicación podrían, en teoría, disparar por el mismo caso prohibido, cada uno debe afirmar su firma específica — no un resultado genérico que ambos podrían producir.
7. Resumen operativo
- Test negativo: prueba que un input/estado inválido se rechaza. Contrato de comportamiento.
- Canario: prueba que un guard concreto sigue vivo. Contrato de existencia de mecanismo. Necesita discriminador observable o es tautológico.
- Sandbox desechable: ensayo barato antes de tocar producción. No es un canario, aunque se llame así.
- Canary deployment: fracción de tráfico real como detector de salud general. Legítimo, pero resuelve otro problema.
- Canario minero en infra: intento deliberado de la acción prohibida contra un mecanismo específico, con aserción de su firma propia. El único de los cuatro que hereda sin trampa la metáfora original.
Regla mnemotécnica: el canario no muere de cualquier cosa — muere de una cosa concreta, y si no puedes decir de cuál, no es un canario, es ruido de fondo con buena reputación.
Aviso
Esta documentación y su contenido, no implica que funcione en tu caso o determinados casos. También implica que tienes conocimientos sobre lo que trata, y que en cualquier caso tienes copias de seguridad. El contenido el contenido se entrega, tal y como está, sin que ello implique ningún obligación ni responsabilidad por parte de Castris
Si necesitas soporte profesional puedes contratar con Castris soporte profesional.