Una sola aplicación sirve tu propio sitio y el sitio de cada equipo. Esta guía cubre cómo se distingue una petición de otra, qué responde team() en cada caso, qué rutas tiene el dominio de un equipo y cómo probar código que corre ahí. Léela antes de escribir cualquier cosa que corra en el dominio de un equipo.
Dos tipos de dominio
El host de APP_URL es el dominio propio de la aplicación. Cualquier otro host se lee como la dirección del sitio de un equipo.
| Dominio | Qué responde |
|---|---|
El host de APP_URL |
Tu página de inicio, las páginas de funciones, el centro de ayuda, /admin, /app y /team, desde routes/web.php, routes/admin.php, routes/app.php y routes/team.php. |
| Cualquier otro host | El sitio de ese equipo, desde routes/site.php: sus páginas, sus formularios, sus archivos y su espacio /team. |
| Ambos | El inicio de sesión, el registro y la recuperación de contraseña, desde routes/auth.php, cada ruta que registra un paquete, y las rutas de routes/site.php de abajo, que no están ligadas a ningún dominio. |
En .env.testing, APP_URL=http://localhost, así que una petición de prueba sin host propio es una petición al dominio de la aplicación.
Cómo un host se convierte en un equipo
En el dominio de un equipo cada petición pasa por dos middlewares, websiteExists y teamConfig:
- Un host que empieza con
www.se redirige, con una redirección temporal, a la misma dirección sin elwww.inicial. Se conservan el esquema, la ruta y la consulta:https://www.example.com/blog?page=2va ahttps://example.com/blog?page=2. UnGETo unHEADrecibe un 302. Cualquier otro método recibe un 307, que conserva el método y el cuerpo, así que un formulario enviado a una dirección conwww.sigue llegando comoPOSTy se guarda. - El host se compara con la
urlde cada equipo. El equipo cuyaurles el host es el equipo del sitio. - Un host que no coincide con ningún equipo se redirige a
APP_URLcon?error=site_not_configured&url={host}. teamConfigcambia las claves de reCAPTCHA de la aplicación por las del equipo durante el resto de la petición.
App\Classes\DetectWebsite hace la comparación, y Team::current() le pregunta. Responde false en el dominio propio de la aplicación, y el equipo, o null, en cualquier otro.
Qué devuelve team()
team() está definido en app/Helpers/builder.php, que se carga antes que app/Helpers/teams.php, así que reemplaza el helper de Teams. Responde, en orden:
- El equipo cuyo sitio se está visitando, cuando la petición está en el dominio de un equipo.
- El equipo en el que la persona con sesión está trabajando —el de la cookie
team_id, o su equipo actual— en cualquier otro caso, exactamente como en Teams.
Así el mismo código lee el equipo correcto en los dos lugares: una página que se muestra en mystore.example.com obtiene el equipo de esa tienda, incluso para un visitante que pertenece a otros equipos, y una pantalla bajo /team en tu propio dominio obtiene el equipo que la persona eligió.
Hay dos salidas para código sin un host real. En la consola, un equipo puesto en la sesión bajo current_team se usa tal cual. En cualquier lugar, el id de un equipo puesto en la sesión bajo force_team fija ese equipo en cada petición:
session(['force_team' => $team->id]);
Las rutas del dominio de un equipo
routes/site.php, en el orden en que se comparan:
| Ruta | Qué hace |
|---|---|
GET /media/{path} |
Sirve un archivo de la biblioteca del equipo, cuando MULTIMEDIA_ENABLE_MASK está encendido. |
POST /form/{slug} |
Recibe un formulario del sitio. Consulta Formularios y registros. |
GET /app, GET /admin |
Redirigen a /team. |
/team/... |
El espacio de trabajo del equipo, el mismo routes/team.php que en tu dominio, con nombres team-admin.* en lugar de team.*. |
GET /api/dns-validation |
Responde {"status":"ok"}, hasta 60 peticiones por minuto. El verificador de dominio lo llama para saber que el DNS llega a ti. |
Cualquier otro GET |
El resolvedor de páginas, registrado como ruta de respaldo. |
Estas rutas se registran sin dominio, así que las que tu propio dominio no responde ya también responden en el host de APP_URL: /media/..., /form/{slug}, /api/dns-validation y el resolvedor de páginas. Ahí team() es el equipo actual de la persona con sesión iniciada, así que a un miembro con sesión que abre una ruta desconocida en tu dominio se le sirve la página de ese equipo en esa ruta, si la tiene, y un visitante sin sesión recibe un 404, o un error en /media/....
Por qué el resolvedor de páginas nunca oculta otra ruta
El resolvedor de páginas se registra con Route::fallback(). Laravel prueba una ruta de respaldo solo después de que ninguna otra ruta coincidió, sin importar el orden en que se registraron. Una ruta de paquete como /account/... del paquete de facturación, o una ruta que agregues en cualquier lugar, siempre se compara antes que el resolvedor, en cualquier dominio. Una ruta que agregues para el sitio de un equipo va en routes/site.php.
Cómo una ruta se convierte en una página
App\Livewire\TeamWeb\Resolver atiende la ruta de respaldo:
| Ruta | Página que se sirve |
|---|---|
/ |
La página activa con el tipo de plantilla Home. Si no hay ninguna, un 404. |
| Una ruta igual al slug de una página | Esa página. Los layouts quedan fuera, así que el slug de un layout responde 404. |
Una ruta que encaja en el patrón del slug de una página, como products/{slug} o categories/products/{?slug} |
Esa página, con los valores capturados entregados a su tipo de plantilla. Solo se prueban las páginas con tipo de plantilla. Consulta Tipos de plantilla y categorías. |
| Cualquier otra | Un 404. |
Solo se consideran las páginas activas, y nunca un layout: un layout no es la página de inicio ni se prueba contra un patrón de slug. La página se muestra dentro de su layout mediante resources/views/components/project/layouts/team-web.blade.php, que agrega el favicon del sitio, su idioma y el script de Tailwind para el navegador. Lo que recibe una página está en Páginas y layouts.
Mientras se muestra una página
El resolvedor de páginas corre detrás del middleware debug, App\Http\Middleware\EnableDebugMode. En el entorno local no cambia nada. En cualquier otro entorno, para la petición que muestra una página, manda el registro al canal null, así que los errores de un sitio nunca se registran, y marca la petición como de solo lectura. Nunca enciende app.debug: en el dominio de un equipo la depuración está apagada para todos, incluido un usuario sudo con sesión iniciada, y en tu propio dominio se queda como diga APP_DEBUG.
En una petición de solo lectura, cada modelo que extiende App\Models\BaseModel usa la conexión mysql_readonly, App\Database\ReadOnlyMySqlConnection. Lee como siempre y rechaza cada inserción, actualización y borrado, y cualquier sentencia que cree, altere, elimine o vacíe una tabla. El código de una página puede leer los registros del equipo; una escritura desde él falla.
Cuando el código de una página falla
Fuera del entorno local, lo que muestra una página que falla depende del Modo debug del equipo, un interruptor en Configuración › Información general, encendido hasta que el equipo lo apaga. Se lee como $team->debug_mode.
Con él encendido, cuando el código Blade de una página lanza un error, el sitio responde con la pantalla de error de la página en lugar de la página, con estado 500. BuilderServiceProvider::boot() la dibuja desde resources/views/components/errors.blade.php. Muestra:
- el mensaje del error;
- la línea que falló, contada en el código compilado de la página;
- el título de la página;
- el código de la página tal como Blade lo compiló a PHP.
Eso es lo que ve el dueño del equipo cuando su página se rompe, y lo que ve también un visitante del sitio. Un error que no vino del código compilado de una página no se dibuja en esta pantalla.
Con él apagado, y para cualquier error fuera del código de una página, un visitante o un miembro del equipo recibe la pantalla genérica resources/views/errors/500.blade.php: «Algo salió mal de nuestro lado», sin mensaje, línea ni código, y un botón de vuelta a la página de inicio. Una página que no existe responde siempre con la pantalla 404, diga lo que diga el interruptor.
Un usuario sudo recibe las mismas pantallas que cualquier otro: fuera del entorno local, la página de error del propio framework nunca se muestra en el dominio de un equipo. El interruptor solo elige la pantalla: el código de la página sigue corriendo en solo lectura y sus errores siguen sin registrarse.
En el entorno local nada de esto aplica: el error se maneja como cualquier otro, según el APP_DEBUG de tu .env.
Pruébalo
Envía la petición con el host del equipo:
$team->update(['url' => 'mystore.test']);
$this->withServerVariables(['HTTP_HOST' => 'mystore.test'])
->get('/about')
->assertOk();
Cuando el dominio no es lo que la prueba verifica, fija el equipo con la sesión:
$this->withSession(['force_team' => $team->id])->get('/about')->assertOk();