Intenta responder esto en una revisión de cumplimiento: ¿por qué exactamente tu aplicación dejó que la cuenta 4127 aprobara aquella factura en marzo? No si lo hizo, eso lo zanja el log de acceso. Por qué. Qué regla saltó, qué rol inclinó la balanza, qué vio realmente el voter en ese momento. Para la mayoría de las apps Symfony la respuesta honesta ha sido un booleano y un encogimiento de hombros, porque un voter devuelve true o false y se lleva su razonamiento a la tumba. Esa era está terminando, y creo que deberíamos enterrar los apaños con ella.
security:
access_decision_manager:
strategy_service: App\Security\WeightedComplianceStrategySabes a qué apaño me refiero, porque probablemente lo escribiste tú. Una llamada a $logger->info() dentro de cada voter, cada una formateada de forma ligeramente distinta, ninguna vinculada de manera fiable a la decisión que el access manager acabó tomando. Formateo de cadenas de auditoría en mitad de la lógica de autorización, que es más o menos el último sitio donde debería estar. Symfony 7.3, publicado en mayo de 2025 con el PR #59771 de Nicolas Grekas, dejó obsoleta esa muleta: VoterInterface::vote() y Voter::voteOnAttribute() ganaron un argumento Vote anulable, y un simple $vote?->addReason('por qué dije que no') pone la explicación justo donde ocurre la decisión. Esas razones aparecen en el profiler, en la salida de logs y en las páginas de excepción, y la clase base Voter escribe por sí sola en el Vote el resultado de granted, denied o abstain. Tu subclase solo tiene que explicarse.
Las razones son prosa para humanos. La parte que cambia cómo diseño la autorización llegó en noviembre de 2025 con 7.4 y 8.0: extraData, contribuido por Roman Joly, Eltharin en GitHub, en el PR #60085. Un voto ahora puede llevar un array, o un objeto entero si te empeñas, de carga útil legible por máquina. Una puntuación, una referencia a una política, una bandera de riesgo. Combina eso con AccessDecisionStrategyInterface y la configuración strategy_service, que existían desde mucho antes de 7.4, y una estrategia personalizada de repente tiene algo que merece la pena leer en decide(). La votación en Symfony siempre ha sido estrictamente igualitaria, cada voter cuenta lo mismo, lo cual encaja fatal en cuanto la aprobación de un director financiero debería pesar más que la de un becario en una factura grande. Ahora un voter puede adjuntar su peso, y una sola estrategia puede escribir una entrada de auditoría estructurada por petición que cubra todos los votos emitidos. Añade un décimo voter en el próximo sprint y el formato de auditoría no se mueve.
La capa de plantillas también se puso al día. El PR #61379 de Florent Destremau añade access_decision() y access_decision_for_user() junto a las funciones is_granted(), que solo devuelven booleanos. Ambas retornan un objeto AccessDecision que expone el nombre de la estrategia, isGranted, el array de votos y un método getMessage(), y la variante for_user comprueba una cuenta concreta en lugar del token actual, que es exactamente lo que necesita una pantalla de administración que previsualiza los permisos de otra persona. Una trampa merece subrayado en rojo: getMessage() solo ensambla las razones de los votos que coinciden con el resultado final. Un voter que disintió o se abstuvo desaparece de ese mensaje por muy cuidadosamente que se explicara. Para el cuadro completo sigues abriendo el profiler.
Ahora la concesión, porque es real. A la mayoría de las aplicaciones les sirve perfectamente la estrategia affirmative y un sí o no a secas, y ninguna de las estrategias integradas, affirmative, consensus, unanimous o priority, lee extraData en absoluto. Escribe puntuaciones en tus votos mientras usas affirmative y habrás producido datos muertos bellamente estructurados que no cambian nada. Existe un riesgo genuino de construir un pequeño motor de reglas que nadie pidió, con pesos que nadie puede justificar en una revisión de código. Y una cadena de razón escrita para una pestaña interna del profiler puede nombrar una política de negocio que no quieres bajo ningún concepto renderizada en una página de cara al cliente. Volcar decision.message directamente en Twig sin pensar en la segunda audiencia es una clase de bug nueva, recién estrenada.
Aun así me posiciono con firmeza a favor de adoptar el objeto Vote, y el argumento no es el cumplimiento normativo, es la depuración de un martes por la tarde. Siete voters registrados, una petición devuelve 403, y antes de 7.3 te tocaba bisecarlos con dumps o breakpoints mientras un colega esperaba. addReason() cuesta una línea por rama y convierte esa hora en un vistazo al profiler. Mis colegas que escriben Go montan este tipo de traza de decisión a mano con context values y middleware, y lo consideran normal. Nosotros lo recibimos como primitiva del framework con un argumento anulable. Acepto ese trato siempre.
Cuentas de versiones, en breve. extraData y las dos funciones de Twig requieren 7.4 u 8.0. Symfony 7.4 es la LTS, corre sobre PHP 8.2 en adelante, y recibe correcciones de bugs hasta noviembre de 2028 con parches de seguridad hasta noviembre de 2029; 8.0 pide PHP 8.4 y va en un ciclo más corto, con soporte hasta julio de 2026. Si estás aparcado en 7.3 ya tienes addReason() y deberías estar usándolo hoy. Mi regla de adopción es simple: razones en todas partes, de inmediato, porque no cuestan nada. extraData solo el día en que una estrategia personalizada lo lea de verdad.
Lo cual me deja con la pregunta a la que sigo dando vueltas: ¿dónde trazas tú la línea de exposición? ¿Es decision.message estrictamente un artefacto de back office para administradores y auditores, o mostrarías alguna vez una versión filtrada al usuario final que acaba de ser bloqueado, para que sepa que fue el umbral de la factura y no un bug? Cuéntame dónde aterriza tu equipo, y si alguien por ahí ha necesitado de verdad votos ponderados en producción en lugar de unanimous más una razón bien escrita.




Comentarios
Aún no hay comentarios — escribe el primero.
Inicia la conversación
Sin cuenta ni contraseña — introduce tu correo y te enviamos un enlace de acceso de un solo uso. ¿Primera vez? Todo se configura automáticamente.
Tu valoración se aplicará automáticamente al iniciar sesión.
Revisa tu bandeja de entrada
Hemos enviado un enlace de acceso a …. Ábrelo en este dispositivo — esta pestaña te conectará automáticamente.
¿No llega nada? Mira en la carpeta de spam — y marca el correo como «No es spam» para que la próxima vez llegue directo.