přesměrování při špatném přihlašení na presenter

vlkodlak
Člen | 160
+
0
-

Zdravím,

řeším, pro mně takovou zapeklitost a nedaři se, chci vytvořit možnost pro přihlašení uživatele a účet neexistuje, aby byl přesměrován na presenter, kde bude moci provést registraci, jenže se mi nedaří předat chybu / povel do SingPresenteru, který by mohl dal vše přesměrovat nebo rovnou ze SingInFormFactory přenést „děj“ na presenter VytvorUzivatele.

**SingInFormFactory **


<?php

declare(strict_types=1);

namespace App\Forms;

use Nette;
use Nette\Application\UI\Form;
use Nette\Security\User;
use Nette\Application\UI\Control;

final class SignInFormFactory
{

   use Nette\SmartObject;

   /** @var FormFactory */
   private $factory;

   /** @var User */
   private $user;

   /** $var file */
   private $infofile;

   public function __construct(FormFactory $factory, User $user, $infofile = '')
   {
      $this->factory	 = $factory;
      $this->user	 	 = $user;
      $this->infofile	 = $infofile;
   }

   public function create(callable $onSuccess): Form
   {
      $form = $this->factory->create();

      $form->addText('username', 'Username:')
	      ->setHtmlAttribute('class', 'bigNumbers')
	      ->setRequired('Please enter your username.');

      $form->addPassword('password', 'Password:')
	      ->setRequired('Please enter your password.');

      $form->addCheckbox('remember', 'Keep me signed in');

      $form->addSubmit('send', 'Sign in')
	      ->setHtmlAttribute('class', 'button btn btn-outline-primary')
	      ->setHtmlAttribute('style', '    color: #212529;  border-color: #28a745;');

      $form->onSuccess[] = function (Form $form, \stdClass $values) use ($onSuccess): void {
	 try {
	    $this->user->setExpiration($values->remember ? '14 days' : '20 minutes');
	    $this->user->login($values->username, $values->password);
	 } catch (Nette\Security\AuthenticationException $e) {
	    $form->addError($e->getMessage());
	    return;
	 }
	 $onSuccess();
      };
      return $form;
   }

}

zkoušel jsem experimentovat i s onError, ale neuspešně

      $form->onError[] = function() use ($form) {
           $this->redirect('Offlineuser:default');
		};
Marek Bartoš
Nette Blogger | 1167
+
0
-

V onSuccess je pozdě na přidávání errorů do formuláře. K tomu aby došlo na událost onError musí být chyba přidaná skrze validační pravidla nebo při onSubmit.

David Grudl
Nette Core | 8125
+
+1
-

Není pozdě.

vlkodlak
Člen | 160
+
0
-

@MarekBartoš David má pravdu, ten onError opravdu zachytí špatnou událost dřív než dojde k viz. výjimce

catch (Nette\Security\AuthenticationException $e) {
	    $form->addError($e->getMessage());
	    return;

@DavidGrudl pak mám dotaz na člověka nejznalejšího a nejzkušenějšího ;-)

  • Laděnka mi říká, že nezná

Call to undefined method App\Forms\SignInFormFactory::redirect()

a já v klauzuli Use nevidím, co by jí mělo chybět?

use Nette;
use Nette\Application\UI\Form;
use Nette\Security\User;
use Nette\Application\UI\Control;
  • pokud jsem pochopil správně: když událost proběhne přes onError tak bych měl řešit i situaci, kdy uživatel prostě zadá špatně heslo, to co mi teď řeší ta část catch (Nette\Security\AuthenticationException $e)

Mohu pak znovu vyvolat Exeption? ve větvi onError provedu ověření zda podmínka pro přesměrování je splněna, pak přesměrují, pokud ne nechám doběhnout výjimku / znovu ji vyvolám? něčím takovým

...
throw $e;
...
nightfish
Člen | 472
+
+2
-

vlkodlak napsal(a):

  • Laděnka mi říká, že nezná

Call to undefined method App\Forms\SignInFormFactory::redirect()

a já v klauzuli Use nevidím, co by jí mělo chybět?

@vlkodlak Bude to tím, že SignInFormFactory na sobě žádnou metodu redirect() nemá.

Osobně bych to celé řešil úplně jinak – v SignInFormFactory bych nechal jen vytvoření formuláře a navěšení callbacku $onSuccess a veškerý kód, který volá službu User nechal v presenteru:

protected function createComponentSignInForm(): Form {
    return $this->signInFormFactory->create(function(Form $form): void {
        $values = $form->getValues();

        try {
	        $this->getUser()->setExpiration($values->remember ? '14 days' : '20 minutes');
	        $this->getUser()->login($values->username, $values->password);
	    } catch (\Nette\Security\AuthenticationException $e) {
	        // tady se nejak rozhodni, jestli presmerovat, ci nikoliv
            // ja si v Authorizatoru vyhazuju ruzne vyjimky podle nastalych situaci
            // (spatne udaje, zablokovany ucet, nedokoncena aktivace),
            // takze tady bych mel vic vetvi `catch` a bud presmeroval, nebo pridal formularovou chybu
            $canRedirect = ...;

            if ($canRedirect) {
               $this->redirect('Offlineuser:default');
            } else {
               $form->addError($e->getMessage());
            }
	    }
    });
}
vlkodlak
Člen | 160
+
0
-

@nightfish děkují za zajímavý podnět … z tohoto úhlu pohledu jsem ještě nepřemýšlel

dakur
Člen | 493
+
+2
-

Podle mě by v tom callbacku měl být jen redirect a nic jiného, logiku přihlašování bych čekal v komponentě SignIn. Nicméně je to spíš drobnost.

Editoval dakur (19. 12. 2022 9:56)

Bulldog
Člen | 110
+
+1
-

Souhlasím s @dakur
My to tedy děláme ještě tak, že komponenta je jen ‚vykreslitelem‘ formuláře, takže samotná FormFactory jako závislost bere něco, co umožňuje uživatele přihlásit (typicky Nette\Security\User) a rovnou na formulář navěsí onSuccess metodu, která se o přihlášení pokusí, takže ohledně fungování formuláře se už komponenta ani Presenter nestarají.

Komponenta se nám stará o jeho vykreslení/překreslení a Presenter navěšuje pouze, jak píšeš, přesměrování po úspěšném vykonání formuláře.

Tedy náš formulář je komplet přenositelný a nezávisí na to, jak jej zpracuje Presenter, nebo komponenta.
Tzn. pokud ho hodíme do projektu, kde nemáme Presentery, bude pořád fungovat. Pokud jej hodíme do projektu, kde nemáme komponenty, pořád můžeme překopírovat factory, která bude pracovat pořád stejně a nic dalšího se nemusí přidávat.

A pokud jej hodíme do projektu, kde není Nette\Security, tak jen změníme, co se posílá továrně jako závislost přes DI.

Už jsem ale viděl i způsoby od větších firem, kdy to bylo dost podobné, ale FormFactory jen stavěla strukturu formuláře, a nenastavovala mu callbacky. Tato FormFactory se injectovala přímo do Presenteru a ten už nastavoval v určitých callbacích potřebné věci.
Pak měli jednu helper třídu pro každý formulář, která v sobě definovala všechny callbacky a v Presenteru pak dělali to, že navěsili při vykreslení dané callbacky na formulář.

Takže to vypadalo takto: (Pseudokód, ale opravdu reálný. Nic není vymyšleno. Napsáno podle předlohy.)
FormFactory:

class UserFormFactory
{
	public function create(): Nette\Application\UI\Form
	{
		$form = new Nette\Application\UI\Form();

		$form->addText('name', 'Jmeno');
		$form->addText('surname', 'Prijmeni');
		$form->addEmail('email', 'Email');
		$form->addPassword('pass', 'Heslo');

		$form->addSubmit('save', 'Uložit');

		return $form;
	}

	public function setDefaultsByEntity(Nette\Application\UI\Form $form, ?UserEntity $user): void
	{
		if ($user === null) {
			return;
		}

		$data = $user->toArray();
		$form->setDefaults($data);
	}
}

Helper s metodami na process formuláře:

class UserService
{
	public function __construct(
		private App\Model\Orm $orm,
		private Nette\Security\Passwords $passwords,
	) { }

	public function processForm(Form $form, ArrayHash $values, ?UserEntity $user): UserEntity
	{
		if ($user === null) {
			$user = new UserEntity();
			$user->createdAt = new DateTime();
		}

		$user->name = $values->name;
		$user->surname = $values->surname;
		$user->email = $values->email;
		$user->pass = $this->passwords->hash($values->pass);
		$user->updatedAt = new DateTime();

		$this->orm->persistAndFlush($user);

		return $user;
	}
}

Presenter:

class UserPresenter
{
	#[Inject] public App\Model\Orm $orm;
    #[Inject] public UserService $userService;
	#[Inject] public UserFormFactory $userFormFactory;
	private UserEntity $userEntity;

	public function actionEdit(int $id): void
	{
		$this->userEntity = $this->getUserEntity($id);
	}

	protected function createComponentUserForm(): Nette\Application\UI\Form
	{
		$form = $this->userFormFactory->create();

		$form->onAnchor[] = function (Nette\Application\UI\Form $form): void {
			$this->userFormFactory->setDefaultsByEntity($form, $this->userEntity);
		};
		$form->onSuccess[] = function (Nette\Application\UI\Form $form, ArrayHash $values): void {
			$this->userEntity = $this->userService->processForm($form, $values, $this->userEntity);
		};
		$form->onSuccess[] = function (Nette\Application\UI\Form $form, ArrayHash $values): void {
			$this->flashMessage('Uživatel uložen.', 'success');
			$this->redirect('default');
		};

		return $form;
	}

	private function getUserEntity(int $id): UserEntity
	{
		// Chyba. Jsme závislí na ORM. Lepší je injectovat rovnou repozitáře, které by neměly být nazvané tak, aby se nepoznalo, že to jsou repozitáře
		// Díky tomu můžeme měnit ORM podle potřeby, nebo ho vyměnit v určitých místech za jiný způsob získávání dat.
		$user = $this->orm->user->getById($id);
		if ($user === null) {
			// Další chyba.. Nenastavuje se 404 stránka, ale jen se přesměrovává, což může vést ke zmatení vyhledávačů například a změlnění PageRank
			$this->flashMessage('Uživatel nenalezen.', 'danger');
			$this->redirect('default');
		}

		return $user;
	}

}

Což mě osobně přijde přehnané a taky jsi závislý na tom, aby ten, kdo si překopíruje formulář se musel najednou starat o překreslování, vykreslování, zpracování, ověření chyb vůči databázi atp., takže nemáš jeden soubor, co zkopíruješ, ale musíš kopírovat 2 a v presenteru je mezi sebou propojovat, což je více práce, ale hej. Když se jim takto vyvíjí v pohodě, tak proč ne že.