Laravel 12.51: 3 Funciones Que Cambiaron Mi Forma de Desarrollar
Cuatro segundos. Eso es lo que tardaba en cargar una sola página de administración en un proyecto de un cliente el año pasado.
El culpable no era un índice faltante. No era una consulta N+1. Era una llamada a firstOrCreate — código Laravel completamente normal, sin nada llamativo — donde el array de atributos de creación incluía una llamada API a un servicio de facturación externo y una operación de hash. Ambas se evaluaban en cada petición. Incluso cuando el usuario ya existía. Incluso cuando no se estaba creando nada.
Encontré el bug después de casi una hora de llamadas a dd() y desplazamiento lento por la línea de tiempo del Laravel Debugbar. Cuando finalmente lo entendí, sentí esa mezcla específica de alivio y vergüenza que solo aparece cuando te das cuenta de que el bug era invisible no porque fuera ingenioso, sino porque no entendías cómo funciona realmente la evaluación de argumentos en PHP.
La solución que escribí en ese momento fue un parche: verificar si el registro existía primero, y luego llamar al método apropiado según el resultado. Funcionaba. Era tosco. Y cada vez que me encontraba con ese patrón en otros proyectos, pensaba "tiene que haber una forma más limpia."
Laravel 12.51 incluye esa forma más limpia de manera nativa. Y eso es solo una de las tres novedades de esta versión que me hicieron pausar lo que estaba haciendo y leer el changelog dos veces.
Las otras dos — un método nativo de timeout para consultas y callbacks encadenables en el validador — abordan puntos de dolor que llevo años resolviendo con código personalizado y soluciones improvisadas. Una tiene que ver con el rendimiento que degrada silenciosamente las aplicaciones en producción. La otra con la calidad del código que degrada silenciosamente a los equipos. Ambas importan.
Esto es lo que realmente cambió, cómo usarlo y — porque la mayoría de los changelogs se saltan esta parte — cuándo vale la pena usar cada función y cuándo no.
El Problema Que Se Ha Estado Ocultando en Tus Llamadas a firstOrCreate
Antes de explicar la solución, necesitas entender por qué existe el bug en primer lugar. Porque si eres como la mayoría de desarrolladores Laravel, llevas años escribiendo este código y nunca ha roto nada de forma evidente.
El problema central: PHP evalúa todos los argumentos de una función antes de ejecutarla.
Esa frase suena simple. Las implicaciones no lo son.
Cuando escribes esto:
$user = User::firstOrCreate(
['email' => $email],
[
'name' => $name,
'avatar' => $this->avatarService->generate($email), // API call
'api_key' => $this->keyService->issue($email), // another API call
'hash' => bcrypt(Str::random(64)), // CPU-bound
]
);
PHP construye el segundo array completo — ejecutando generate(), issue() y bcrypt() — antes de que firstOrCreate siquiera comience. Luego firstOrCreate ejecuta su consulta. Si el usuario existe, devuelve ese usuario y descarta completamente el array calculado. Toda la computación que acabas de ejecutar? Desperdiciada.
En desarrollo, esto no duele. Las bases de datos de prueba tienen pocos registros. Las llamadas API llegan a sandboxes que responden en milisegundos. Ejecutas el código, funciona, lo despliegas.
En producción, esto se convierte en un impuesto que pagas en cada petición que toca un registro existente — que, en una aplicación madura, es casi cada petición. Si tu servicio de avatares tarda 300ms y tu servicio de claves tarda 200ms, estás añadiendo 500ms de desperdicio puro a esas peticiones. Para siempre. Hasta que alguien rastree el endpoint lento y descubra por qué.
La solución que introduce Laravel 12.51 es limpia: pasar el segundo argumento como un closure.
$user = User::firstOrCreate(
['email' => $email],
function () use ($name, $email) {
return [
'name' => $name,
'avatar' => $this->avatarService->generate($email),
'api_key' => $this->keyService->issue($email),
'hash' => bcrypt(Str::random(64)),
];
}
);
La implementación de Laravel 12.51 verifica si el segundo argumento es un callable. Si lo es, el closure solo se ejecuta cuando el registro no existe. Los registros existentes se devuelven inmediatamente. El trabajo de creación nunca se ejecuta.
Esto es evaluación perezosa (lazy evaluation) — un patrón que ha estado en la caja de herramientas de PHP durante años a través de closures y generators, pero que no estaba disponible en los métodos de creación de registros del query builder hasta ahora.
El método createOrFirst recibe el mismo tratamiento. Ese método funciona en orden inverso: primero intenta insertar y recurre a un select si hay violación de restricción única. Se aplica la misma lógica de evaluación perezosa. Pasa un closure, y los atributos solo se calculan si realmente se necesita una inserción.
Cuándo esto realmente importa para tu código
Para ser claros: si tus atributos de creación son valores estáticos — cadenas, enteros, flags booleanos — no necesitas el closure. PHP los evalúa en microsegundos. La optimización tiene sentido cuando la computación es costosa. Pregúntate: ¿algo en ese segundo array hace una llamada de red, ejecuta una operación criptográfica, consulta la base de datos o ejecuta código que tarda más de unos pocos milisegundos?
Si la respuesta es sí, envuélvelo. Cada llamada a firstOrCreate y createOrFirst con lógica de creación costosa es candidata.
Encuéntralas en tu proyecto ahora mismo:
grep -rn "firstOrCreate\|createOrFirst" app/ --include="*.php"
Abre cada archivo. Mira el segundo argumento. Si está haciendo trabajo real, estás pagando el impuesto de la evaluación anticipada.
Los números de mis propias pruebas — usando un sleep artificial de dos segundos para simular dos llamadas API — fueron contundentes: la búsqueda de registros existentes bajó de 2,003ms a 8ms. La creación de nuevos registros se mantuvo en 2,003ms porque el trabajo es realmente necesario. Ese es el comportamiento que quieres. Computar solo cuando es imprescindible.
Pero aquí es donde las cosas se ponen interesantes, y donde la mayoría de artículos sobre esta función se detienen antes de la parte importante.
Eliminar Consultas Largas Antes de Que Eliminen Tu Base de Datos
La segunda función es operacionalmente diferente de la evaluación perezosa. La evaluación perezosa consiste en evitar trabajo innecesario. El timeout de consultas consiste en limitar trabajo necesario que se ha salido de control.
Si has trabajado con Laravel en tablas MySQL grandes — hablo de cientos de miles a millones de filas — probablemente hayas visto este patrón: una consulta que funcionaba perfectamente hace seis meses ahora tarda 30, 40, 60 segundos en completarse. La tabla creció. No se añadieron nuevos índices. Un informe analítico que solía ejecutarse en menos de un segundo ahora mantiene una conexión de base de datos abierta lo suficiente como para que múltiples peticiones HTTP se agoten.
MySQL tiene una solución para esto desde hace años: el hint de optimización MAX_EXECUTION_TIME. Puedes incrustarlo directamente en una sentencia SELECT para establecer un techo en milisegundos a la duración de la consulta. Si la consulta excede ese techo, MySQL la termina y devuelve un error en lugar de dejarla ejecutarse indefinidamente.
El problema era que usar esto en Laravel requería salirse del query builder:
// Option 1: Raw SQL. Breaks the fluent interface.
$results = DB::select('SELECT /*+ MAX_EXECUTION_TIME(5000) */ * FROM orders WHERE status = "pending"');
// Option 2: Session-level setting. Affects more than just this query.
DB::statement('SET SESSION MAX_EXECUTION_TIME = 5000');
$orders = Order::where('status', 'pending')->get();
DB::statement('SET SESSION MAX_EXECUTION_TIME = 0'); // must reset or everything is capped
// Option 3: Custom macro. Works, but requires maintenance and project-specific setup.
Builder::macro('maxExecutionTime', function (int $ms) {
return $this->beforeQuery(function ($q) use ($ms) {
$q->addSelectExpression("/*+ MAX_EXECUTION_TIME($ms) */");
});
});
Yo estaba usando la Opción 3 en varios proyectos. Funciona, pero estás cargando con código personalizado que los nuevos miembros del equipo no conocen, que no aparece en el autocompletado del IDE, y que tienes que recordar añadir a cada nuevo proyecto Laravel que inicias.
Laravel 12.51 incluye esto de forma nativa:
$orders = Order::where('status', 'pending')
->timeout(5)
->get();
El método timeout() acepta segundos (no milisegundos — nota la diferencia con el hint raw de MySQL, que usa milisegundos). Internamente, Laravel lo convierte e inyecta el hint de optimización en la consulta. Si se alcanza el límite, obtienes una QueryException con el mensaje de consulta interrumpida de MySQL.
Este es el patrón listo para producción para envolver consultas con timeout:
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\Log;
public function generateSalesReport(array $filters): array
{
try {
$results = Order::query()
->where('created_at', '>=', $filters['start_date'])
->where('created_at', '<=', $filters['end_date'])
->with(['items', 'customer', 'discounts'])
->when($filters['status'] ?? null, fn($q, $s) => $q->where('status', $s))
->timeout(15)
->get();
return $this->formatReport($results);
} catch (QueryException $e) {
if (str_contains($e->getMessage(), 'Query execution was interrupted')) {
Log::warning('Sales report query timed out', [
'filters' => $filters,
'timeout_seconds' => 15,
]);
// Graceful degradation: return cached result or a reduced dataset
return $this->getCachedReport($filters) ?? [];
}
throw $e; // Re-throw unexpected database errors
}
}
Este patrón — timeout más manejo explícito de excepciones — es lo que separa "añadí timeout" de "manejé correctamente el timeout." El timeout solo reemplaza un cuelgue infinito con una excepción. El manejo de la excepción es donde realmente proteges a tus usuarios.
La parte que la mayoría de tutoriales omiten: esto es solo para MySQL
El comportamiento del método timeout() depende del driver. Para MySQL y MariaDB, usa el hint de optimización MAX_EXECUTION_TIME. Para PostgreSQL, la implementación es diferente — PostgreSQL usa timeouts a nivel de sentencia configurados de forma distinta, y el comportamiento entre drivers puede no ser el que esperas.
Si estás construyendo un SaaS multi-tenant donde diferentes clientes podrían estar en diferentes drivers de base de datos, o si desarrollas en SQLite localmente pero despliegas en MySQL en producción (una configuración sorprendentemente común), prueba el comportamiento del timeout en el driver real de producción antes de depender de él.
También vale la pena saber: MAX_EXECUTION_TIME aplica a sentencias SELECT en MySQL. No aplica a operaciones INSERT, UPDATE o DELETE — esas requieren técnicas diferentes para establecer timeouts. El método timeout() en Laravel es para consultas de lectura.
Úsalo en tus endpoints de reportes. Úsalo dentro de queue jobs que ejecutan procesamiento intensivo de base de datos. Úsalo en cualquier lugar donde una consulta pueda legítimamente necesitar ejecutarse durante un rato pero nunca debería ejecutarse indefinidamente.
A estas alturas ya tienes evaluación perezosa y timeouts de consultas aplicados. El tercer cambio en 12.51 es diferente en naturaleza — menos sobre rendimiento bruto, más sobre el tipo de calidad de código que se acumula en todo un equipo con el tiempo.
Callbacks del Validator: Haciendo Que la Validación Manual Vuelva a Sentirse Como Laravel
La validación de peticiones HTTP en Laravel es excelente. Defines una clase Form Request, la inyectas en tu controlador, y el framework se encarga de todo automáticamente. La validación pasa, el controlador se ejecuta. La validación falla, el usuario recibe los errores de vuelta. Limpio, automático, cero código repetitivo.
Pero no toda la validación ocurre en peticiones HTTP.
Los comandos Artisan necesitan validar argumentos. Las clases de servicio necesitan validar entrada de queue jobs. Las clases de dominio necesitan validar datos que vienen de API externas. En todos estos escenarios, estás haciendo validación manual — creando una instancia de Validator a mano y comprobando su resultado tú mismo.
El patrón clásico:
public function handle(): int
{
$validator = Validator::make($this->arguments(), [
'email' => 'required|email|max:255',
'role' => 'required|in:admin,editor,viewer',
]);
if ($validator->fails()) {
$this->error('Validation failed:');
foreach ($validator->errors()->all() as $error) {
$this->line(" — {$error}");
}
return self::FAILURE;
}
$this->info('Creating user...');
$this->userService->create($validator->validated());
return self::SUCCESS;
}
Este código es correcto. Pero léelo de nuevo y observa el ritmo: construyes el validador, luego rompes ese flujo para comprobar fails(), manejar el camino de error, y luego — solo entonces — proceder con el camino exitoso. Dos flujos de control separados para una sola operación de validación.
Laravel 12.51 añade whenFails() y whenPasses() directamente en la instancia de Validator:
public function handle(): int
{
return Validator::make($this->arguments(), [
'email' => 'required|email|max:255',
'role' => 'required|in:admin,editor,viewer',
])
->whenFails(function (Validator $validator) {
$this->error('Validation failed:');
foreach ($validator->errors()->all() as $error) {
$this->line(" — {$error}");
}
return self::FAILURE;
})
->whenPasses(function (Validator $validator) {
$this->info('Creating user...');
$this->userService->create($validator->validated());
return self::SUCCESS;
});
}
Ambos callbacks reciben la instancia del validador como parámetro. El valor de retorno del callback que se ejecute se convierte en el valor de retorno de la cadena. Si la validación falla, se ejecuta whenFails y se devuelve su valor de retorno. Si la validación pasa, se ejecuta whenPasses.
No tienes que usar ambos. En una clase de servicio que debería lanzar una excepción en caso de fallo:
public function processWebhook(array $payload): void
{
Validator::make($payload, [
'event' => 'required|string',
'data' => 'required|array',
'user_id' => 'required|integer|exists:users,id',
])
->whenFails(fn($v) => throw new InvalidPayloadException($v->errors()->toJson()))
->whenPasses(function ($v) {
$this->dispatchWebhookEvent($v->validated());
});
}
O puedes usar solo whenFails para manejar el caso de error y dejar que la ejecución continúe naturalmente en caso de éxito:
Validator::make($data, $rules)
->whenFails(function ($validator) {
Log::error('Data validation failed', ['errors' => $validator->errors()->toArray()]);
return false;
});
// code here runs whether validation passed or failed (if whenFails didn't return/throw)
La API es lo suficientemente flexible para cubrir la mayoría de escenarios del mundo real sin forzarte a un patrón específico.
Aplicando Las Tres Funciones: Un Ejemplo Realista
Déjame mostrarte cómo lucen estas tres funciones juntas en un fragmento real de código — un comando que importa usuarios desde un archivo CSV.
Antes de 12.51:
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Services\BillingService;
use App\Services\AvatarService;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Validator;
class ImportUsers extends Command
{
protected $signature = 'users:import {file}';
public function handle(BillingService $billing, AvatarService $avatars): int
{
$validator = Validator::make(['file' => $this->argument('file')], [
'file' => 'required|string',
]);
if ($validator->fails()) {
$this->error($validator->errors()->first());
return self::FAILURE;
}
$rows = array_map('str_getcsv', file($this->argument('file')));
foreach ($rows as $row) {
[$email, $name, $plan] = $row;
// Eager evaluation: avatar and plan are computed even for existing users
$user = User::firstOrCreate(
['email' => $email],
[
'name' => $name,
'avatar' => $avatars->generate($email), // API call, always runs
'plan_id' => $billing->getDefaultPlan()->id, // DB query, always runs
]
);
// Long-running query with no timeout
$exists = DB::table('user_activities')
->where('user_id', $user->id)
->where('created_at', '>=', now()->subYear())
->count();
$this->line("Processed: {$email} (activities: {$exists})");
}
return self::SUCCESS;
}
}
Después de 12.51:
<?php
namespace App\Console\Commands;
use App\Models\User;
use App\Services\BillingService;
use App\Services\AvatarService;
use Illuminate\Console\Command;
use Illuminate\Database\QueryException;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
class ImportUsers extends Command
{
protected $signature = 'users:import {file}';
public function handle(BillingService $billing, AvatarService $avatars): int
{
return Validator::make(['file' => $this->argument('file')], [
'file' => 'required|string',
])
->whenFails(function ($v) {
$this->error($v->errors()->first());
return self::FAILURE;
})
->whenPasses(function () use ($billing, $avatars) {
$rows = array_map('str_getcsv', file($this->argument('file')));
foreach ($rows as $row) {
[$email, $name] = $row;
// Lazy evaluation: closure only runs if user doesn't exist
$user = User::firstOrCreate(
['email' => $email],
function () use ($email, $name, $billing, $avatars) {
return [
'name' => $name,
'avatar' => $avatars->generate($email),
'plan_id' => $billing->getDefaultPlan()->id,
];
}
);
// Timeout: protect against long-running activity queries
try {
$activityCount = DB::table('user_activities')
->where('user_id', $user->id)
->where('created_at', '>=', now()->subYear())
->timeout(3)
->count();
$this->line("Processed: {$email} (activities: {$activityCount})");
} catch (QueryException $e) {
if (str_contains($e->getMessage(), 'Query execution was interrupted')) {
Log::warning("Activity query timed out for user {$email}");
$this->line("Processed: {$email} (activity count unavailable)");
continue;
}
throw $e;
}
}
return self::SUCCESS;
});
}
}
La versión posterior es ligeramente más larga porque maneja los casos de fallo explícitamente — pero la intención de cada sección es más clara. La lógica de validación vive en un bloque encadenado. La lógica de creación está separada de la lógica de búsqueda. Las consultas largas tienen límites.
Así es como debería verse realmente la actualización a 12.51 en la práctica: mejoras específicas, no reescrituras.
Lo Que Realmente Pienso Sobre Estos Cambios
Bien — hablemos con franqueza, porque la mayoría de artículos no dicen esto.
La función de evaluación perezosa aborda una decisión de diseño que creo fue incorrecta desde el principio. "Pasar atributos de creación como un array" es la API intuitiva, y es lo que cada tutorial y ejemplo de documentación muestra. Pero silenciosamente prepara a los desarrolladores para una trampa de rendimiento. La suposición que hace la mayoría es que PHP es inteligente y no hace trabajo a menos que sea necesario. Esa suposición es incorrecta para los argumentos de funciones.
Me alegra que esté corregido. Pero también creo que vale la pena ser honesto y reconocer que esta es una clase de bug que Laravel podría haber hecho imposible hace años documentando el comportamiento de evaluación anticipada de forma más prominente en la documentación de firstOrCreate. He visto este mismo bug en tres bases de código de clientes diferentes. Probablemente lo tengo en algunos de mis propios proyectos que no han crecido lo suficiente como para sentir el dolor todavía.
Revisa tu código.
La función de timeout para consultas es excelente y la usaré inmediatamente — pero con una salvedad que señalaría: el nombre del método timeout() no da ninguna indicación de que es específico de MySQL. Si trabajas en un equipo donde no todos conocen los internos, alguien añadirá timeout() a una consulta PostgreSQL esperando el comportamiento de MySQL y se confundirá cuando las cosas no funcionen como esperaba. Mejor documentación y posiblemente una advertencia específica del driver en la excepción serían bienvenidas.
Los métodos whenFails y whenPasses son agradables. Me gustan. Pero ya he visto gente en línea usarlos en contextos donde hacen el código menos claro, no más — particularmente cuando la lógica del callback es lo suficientemente compleja como para que un if/else simple hubiera sido más legible. Estos métodos brillan para manejo de validación corto y enfocado: lanzar una excepción en caso de fallo, despachar un job en caso de éxito. Si tus callbacks tienen 15 líneas cada uno, pregúntate si un bloque if tradicional no sería realmente más limpio.
El modelo de lanzamientos incrementales de Laravel significa que mejoras de "versión menor" como estas tienden a pasar desapercibidas. Eso es un error. Solo la corrección de evaluación perezosa en firstOrCreate podría mejorar los tiempos de respuesta en segundos para aplicaciones que han vivido con este problema sin saberlo. Eso no es menor en la práctica — es una mejora significativa disfrazada de entrada aburrida en el changelog.
Las Matemáticas del Rendimiento: Qué Puedes Esperar Realmente
Permíteme ser específico sobre lo que estos cambios harán y no harán por las métricas de tu aplicación.
Lazy firstOrCreate — los números:
La mejora depende completamente de lo que hay en tu closure de creación. Aquí tienes un marco aproximado:
| Costo del Atributo de Creación | Peticiones que Tocan Registros Existentes | Ahorro Esperado por Petición |
|---|---|---|
| Solo valores estáticos | Cualquiera | ~0ms (no te molestes con el closure) |
| Un solo bcrypt / hash | 80%+ | 50–200ms |
| Una llamada API externa | 80%+ | 200–1,500ms |
| Dos llamadas API + consulta DB | 80%+ | 500–3,000ms |
Si el 80% de tus peticiones tocan registros existentes (típico para sesiones de usuarios autenticados), y tus atributos de creación incluyen dos llamadas API que promedian 500ms cada una, estás viendo una reducción de 1,000ms por petición en el percentil 80. Para endpoints de alto tráfico, eso es transformador.
Para el proyecto del cliente que mencioné al inicio de este artículo — el de las páginas de administración de cuatro segundos — la mejora medida después de cambiar a atributos de creación basados en closure fue de 3.8 segundos por petición en el nivel p95. Eso no es teórico. Es una aplicación que pasó de lenta y frustrante a ágil en un solo commit.
Query timeout — el panorama operacional:
El timeout no hace las consultas más rápidas. Hace que el modo de fallo sea controlado en lugar de ilimitado. El valor está en tu presupuesto de errores y la fiabilidad del sistema:
- Antes del timeout: consulta de larga duración mantiene conexiones de base de datos → el pool de conexiones se agota → otras consultas se encolan → fallo en cascada
- Después del timeout: consulta de larga duración alcanza el techo → QueryException → manejas con gracia → otras consultas proceden normalmente
Para una aplicación en producción manejando tráfico real, esa diferencia es la frontera entre una página lenta y una caída completa.
Callbacks encadenables del Validator — el ángulo de experiencia del desarrollador:
Sin impacto en rendimiento en tiempo de ejecución. El retorno de inversión aquí se mide en tiempo de revisión de código, velocidad de onboarding y costo de mantenimiento a lo largo de meses y años. Los equipos que escriben código más limpio cometen menos errores. Menos errores significan menos incidentes. El efecto acumulativo es real aunque no sea medible con un cronómetro.
Actualiza y Usa Estas Funciones de Verdad
Aquí tienes el camino práctico.
Primero, actualiza. Si estás en Laravel 12.x, es una actualización de Composer:
composer require laravel/framework:^12.51
Ejecuta tu suite de tests. Estos son cambios retrocompatibles — si tus tests pasan, todo está bien.
Después, audita tus llamadas a firstOrCreate y createOrFirst. Ejecuta el grep que mencioné antes:
grep -rn "firstOrCreate\|createOrFirst" app/ --include="*.php"
Para cada resultado, revisa el segundo argumento. Si hace trabajo real, conviértelo a un closure y mide el antes/después en tu entorno de staging.
Luego encuentra tus consultas más pesadas — endpoints analíticos, rutas de reportes, queue jobs que hacen agregaciones complejas. Añade ->timeout() con un techo razonable. Envuélvelos en try/catch con degradación elegante. Despliega a staging, prueba la ruta del timeout explícitamente (puedes reducir temporalmente el timeout para activarlo), y confirma que tu manejo de errores funciona como se espera.
Finalmente, toma un comando Artisan o clase de servicio que use validación manual y refactorízalo para usar whenFails/whenPasses. Observa cómo se lee. Si es más limpio, aplica el patrón en otros lugares. Si tu equipo prefiere la estructura tradicional de if/else, eso también está bien — usa la herramienta que haga tu código más claro.
Mantengo una lista de funciones que he querido tener nativamente en Laravel durante un tiempo. Lazy firstOrCreate estaba en esa lista. Query timeout estaba en esa lista. Y cada vez que una versión entrega algo de esa lista — sin romper nada más — es un buen recordatorio de que el framework está siendo construido por personas que realmente lo usan en producción.
¿Aquella carga de página de cuatro segundos en el panel de administración del cliente? Ahora son 90ms. La misma base de datos. La misma infraestructura. Un solo cambio en cómo se pasan los atributos de creación a una única llamada de método.
Ve a revisar tu código.
🤝 Trabajemos Juntos
¿Buscas construir sistemas de IA, automatizar flujos de trabajo o escalar tu infraestructura tecnológica? Me encantaría ayudar.
- 🔗 Fiverr (desarrollos e integraciones a medida): fiverr.com/s/EgxYmWD
- 🌐 Portfolio: mejba.me
- 🏢 Ramlit Limited (soluciones empresariales): ramlit.com
- 🎨 ColorPark (diseño y branding): colorpark.io
- 🛡 xCyberSecurity (servicios de seguridad): xcybersecurity.io