Puis-je faire une validation asynchrone du formulaire dans le cadre de jeu 2.x (Scala)?
je fais un réel effort pour comprendre les pouvoirs asynchrones du jeu mais je trouve beaucoup de conflits en ce qui concerne les endroits où l'invocation asynchrone s'inscrit et les endroits où le cadre semble conspirer contre son utilisation.
l'exemple que j'ai se rapporte à la validation du formulaire. Le jeu permet de définir des contraintes ad hoc - voir ceci dans les docs:
val loginForm = Form(
tuple(
"email" -> email,
"password" -> text
) verifying("Invalid user name or password", fields => fields match {
case (e, p) => User.authenticate(e,p).isDefined
})
)
Belle et propre. Cependant, si j'utilise une couche d'accès aux données entièrement asynchrone (par exemple Réactivemongo), un tel appel à User.authenticate(...)
retour Future
et je ne sais pas comment je peux utiliser la puissance à la fois des caractéristiques de reliure de forme intégrée et des outils asynchrones.
c'est très bien de faire connaître l'approche asynchrone, mais je suis frustré que certaines parties du cadre ne fonctionnent pas aussi bien. Si la validation doit être effectuée de façon synchrone, elle semble aller à l'encontre de l'approche asynchrone. J'ai rencontré un problème similaire en utilisant Action
composition-p.ex. a liés à la sécurité Action
cela ferait un appel à Réactivemongo.
est-ce que quelqu'un peut m'éclairer sur ce que je ne comprends pas?
5 réponses
Oui, la validation en jeu est conçue de façon synchrone. Je pense que c'est parce que j'ai supposé que la plupart du temps il n'y a pas d'E/S dans la validation du formulaire: les valeurs des champs sont juste vérifiées pour la taille, la longueur, la correspondance avec regexp, etc.
Validation est construit sur play.api.data.validation.Constraint
cette fonction de stockage de la valeur validée à ValidationResult
(Valid
ou Invalid
, il n'y a pas lieu de mettre Future
ici).
/**
* A form constraint.
*
* @tparam T type of values handled by this constraint
* @param name the constraint name, to be displayed to final user
* @param args the message arguments, to format the constraint name
* @param f the validation function
*/
case class Constraint[-T](name: Option[String], args: Seq[Any])(f: (T => ValidationResult)) {
/**
* Run the constraint validation.
*
* @param t the value to validate
* @return the validation result
*/
def apply(t: T): ValidationResult = f(t)
}
verifying
ajoute juste une autre contrainte avec fonction définie par l'utilisateur.
donc je pense que la liaison de données dans le jeu n'est tout simplement pas conçu pour faire I/O pendant la validation. Le rendre asynchrone le rendrait plus complexe et plus difficile à utiliser, donc il est resté simple. Faire de chaque morceau de code dans le cadre d'un travail sur les données enveloppé dans Future
s est exagéré.
si vous devez utiliser la validation avec ReactiveMongo, vous pouvez utiliser Await.result
. Réactivemongo retourne des contrats à terme partout, et vous pouvez bloquer jusqu'à l'achèvement de ces contrats à terme pour obtenir résultat à l'intérieur de verifying
fonction. Oui, il va gaspiller un fil tandis que MongoDB requête exécute.
object Application extends Controller {
def checkUser(e:String, p:String):Boolean = {
// ... construct cursor, etc
val result = cursor.toList().map( _.length != 0)
Await.result(result, 5 seconds)
}
val loginForm = Form(
tuple(
"email" -> email,
"password" -> text
) verifying("Invalid user name or password", fields => fields match {
case (e, p) => checkUser(e, p)
})
)
def index = Action { implicit request =>
if (loginForm.bindFromRequest.hasErrors)
Ok("Invalid user name")
else
Ok("Login ok")
}
}
peut-être qu'il y a moyen de ne pas gaspiller le fil en utilisant continuations, pas essayé.
je pense qu'il est bon de discuter de cela en Play liste de diffusion, peut-être que beaucoup de gens veulent faire des e/S asynchrones dans le Jeu de la liaison de données (par exemple, pour vérifier les valeurs par rapport à la base de données), si quelqu'un peut le mettre en œuvre pour les futures versions du Jeu.
j'ai eu du mal avec cela, aussi. Les applications réalistes vont généralement avoir une sorte de comptes d'utilisateur et d'authentification. Au lieu de bloquer le thread, une alternative serait de sortir les paramètres de la forme et de gérer l'appel d'authentification dans la méthode du controller elle-même, quelque chose comme ceci:
def authenticate = Action { implicit request =>
Async {
val (username, password) = loginForm.bindFromRequest.get
User.authenticate(username, password).map { user =>
user match {
case Some(u: User) => Redirect(routes.Application.index).withSession("username" -> username)
case None => Redirect(routes.Application.login).withNewSession.flashing("Login Failed" -> "Invalid username or password.")
}
}
}
}
validation du formulaire signifie validation syntaxique des Champs, un par un. Si une demande ne réussit pas la validation, elle peut être marquée (p. ex. barre rouge avec message).
Authentification doit être placé dans le corps de l'action, qui peut être dans un bloc Async.
Il devrait être après le bindFromRequest
appel, donc il y a moi, après la validation, donc après chaque champ n'est pas vide, etc.
basé sur le résultat des appels asynchrones (ex. ReactiveMongo appels) le résultat de l'action peut être soit BadRequest, soit Ok.
les deux avec BadRequest et Ok peuvent rejouer le formulaire avec un message d'erreur si l'authentification échoue. Ces helpers ne spécifient que le code de statut HTTP de la réponse, indépendamment du corps de la réponse.
ce serait une solution élégante de faire L'authentification avec play.api.mvc.Security.Authenticated
(ou écrivez un compositor d'action similaire et personnalisé), et utilisez des messages Flash scoped. Ainsi, l'utilisateur sera redirigé vers la page de connexion, si elle n'est pas authentifié, mais si elle soumet le formulaire de connexion avec de mauvaises références, le message d'erreur sera affiché en plus de la redirection.
veuillez jeter un oeil à L'exemple de ZenTasks de votre installation de jeu.
la même question était demande dans la Play liste de diffusion avec Johan Andrén la réponse:
Je déplacerais l'authentification actuelle hors de la validation du formulaire et le ferais dans votre action à la place et utiliserais la validation seulement pour la validation des champs requis, etc. Quelque chose comme ceci:
val loginForm = Form(
tuple(
"email" -> email,
"password" -> text
)
)
def authenticate = Action { implicit request =>
loginForm.bindFromRequest.fold(
formWithErrors => BadRequest(html.login(formWithErrors)),
auth => Async {
User.authenticate(auth._1, auth._2).map { maybeUser =>
maybeUser.map(user => gotoLoginSucceeded(user.get.id))
.getOrElse(... failed login page ...)
}
}
)
}
j'ai vu sur le guardian's GH repo comment ils gèrent ce scénario de manière asynchrone tout en ayant toujours le support des helpers d'erreur de forme du jeu. À partir d'un rapide coup d'œil, semble qu'ils sont stocker les erreurs de forme dans un cookie crypté de manière à afficher ces erreurs à l'utilisateur la prochaine fois que l'utilisateur accède à la page de connexion.
Extrait à partir de: https://github.com/guardian/facia-tool/blob/9ec455804edbd104861117d477de9a0565776767/identity/app/controllers/ReauthenticationController.scala
def processForm = authenticatedActions.authActionWithUser.async { implicit request =>
val idRequest = idRequestParser(request)
val boundForm = formWithConstraints.bindFromRequest
val verifiedReturnUrlAsOpt = returnUrlVerifier.getVerifiedReturnUrl(request)
def onError(formWithErrors: Form[String]): Future[Result] = {
logger.info("Invalid reauthentication form submission")
Future.successful {
redirectToSigninPage(formWithErrors, verifiedReturnUrlAsOpt)
}
}
def onSuccess(password: String): Future[Result] = {
logger.trace("reauthenticating with ID API")
val persistent = request.user.auth match {
case ScGuU(_, v) => v.isPersistent
case _ => false
}
val auth = EmailPassword(request.user.primaryEmailAddress, password, idRequest.clientIp)
val authResponse = api.authBrowser(auth, idRequest.trackingData, Some(persistent))
signInService.getCookies(authResponse, persistent) map {
case Left(errors) =>
logger.error(errors.toString())
logger.info(s"Reauthentication failed for user, ${errors.toString()}")
val formWithErrors = errors.foldLeft(boundForm) { (formFold, error) =>
val errorMessage =
if ("Invalid email or password" == error.message) Messages("error.login")
else error.description
formFold.withError(error.context.getOrElse(""), errorMessage)
}
redirectToSigninPage(formWithErrors, verifiedReturnUrlAsOpt)
case Right(responseCookies) =>
logger.trace("Logging user in")
SeeOther(verifiedReturnUrlAsOpt.getOrElse(returnUrlVerifier.defaultReturnUrl))
.withCookies(responseCookies:_*)
}
}
boundForm.fold[Future[Result]](onError, onSuccess)
}
def redirectToSigninPage(formWithErrors: Form[String], returnUrl: Option[String]): Result = {
NoCache(SeeOther(routes.ReauthenticationController.renderForm(returnUrl).url).flashing(clearPassword(formWithErrors).toFlash))
}