Při pravidelném používání Nette můžete přijít na to, že vám výchozí UserStorage, který uchovává v session informace o uživateli, nemusí stačit nebo nevyhovuje. Typické problémy mohou být, že pokud máte uloženy informace o uživateli jako třeba jméno, příjmení, tak v případě, že uživatel si tyto údaje změní v nastavení, tak logicky nedojde k jejich aktualizaci v UserStorage, pokud tomu explicitně nepomůžete.
V tomto článečku bych proto chtěl napsat o tom, jak mít vlastní UserStorage a co to vlastně vyžaduje. K závěru pak ukázka řešení pro Doctrine.
UserStorage
UserStorage, jak asi většina ví, je perzistentní uložistě pro uživatelská data, které implementuje interface IUserStorage. To definuje několik metod, jako třeba nastavení expirace uložiště nebo nastavení identity uživatele, kterou uložiště drží.
Identita je pak třída reprezentující identitu uživatele a opět implementuje předepsaný interface IIdentity. Pokud chcete do UserStorage uložit jakoukoliv identitu uživatele, pak stačí, aby tato identita respektovala interface IIdentity. UserStorage je pak schopen s takovou identitou pracovat. Může to tak tedy být třída Identity z Nette nebo třeba entita z Doctrine reprezentujícího uživatele, která bude implementovat interface IIdentity.
Taková Doctrine entita může vypadat třeba takto:
<?php
namespace petrjirasek\Entities\User
use DateTime;
use Doctrine\ORM\Mapping as ORM;
use Nette\Security\IIdentity;
/**
* @ORM\Entity
*/
class User implements IIdentity
{
const ROLE_MEMBER = 'member';
const ROLE_ADMIN = 'admin';
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue
*/
protected $id;
/**
* @ORM\Column(type="string", unique=true)
*/
protected $email;
/**
* @ORM\Column(type="string")
*/
protected $role = self::ROLE_MEMBER;
/**
* @ORM\Column(type="string")
*/
protected $password;
/**
* @return int
*/
public function getId()
{
return $this->id;
}
/**
* @return array
*/
public function getRoles()
{
return [$this->role];
}
...
}
V případě volání metody getIdentity() nad uživatelem, třeba v presenteru, pak dostaneme v případě přihlášeného uživatele vždy tu identitu, kterou jsme si zvolili a při autentizaci uživatele nastavili. Opět ukázka:
function authenticate($email, $password)
{
/** @var User $user */
$user = $this->userRepository->findByEmail($email);
if ($user === NULL) {
throw new AuthenticationException("Uživatel neexistuje.");
}
if (!$user->verifyPassword($password, $this->hasher)) {
throw new AuthenticationException("Nesprávné přihlašovací údaje.");
}
return $user;
}
Zde nám return $user;
vrátí naši entitu User
implementující IIdentity.
To by nám mohlo stačit, pokud by nám nevadilo, že v případě, že si přihlášený uživatel změní v nastavení některý atribut (např. budeme ukládat v entitě navíc jméno), se tato změna neprojeví v identitě, pokud bychom neprovedli explicitně její aktualizaci po uložení nastavení.
A to není jediná situace, změnu některého atributu může provést i někdo jiný, třeba uživatelská podpora v adminu a určitě by bylo vhodné, aby se změna v identitě uživatele projevila okamžitě.
Co s tím?
Jelikož je identita často obrazem identity uložené v jiném uložišti, typicky v databázi, bylo by šikovné, kdybychom dostali vždy aktuální identitu reflektující aktuální stav.
Málokomu se totiž chce psát kód podobný tomuto, když by chtěl získat aktuální informace o uživateli:
public function renderDefault()
{
$user = $this->em->getRepository(User:class)->find($this->getUser()->getId());
...
}
A raději by psal pouze:
public function renderDefault()
{
$user = $this->getUser()->getIdentity();
...
}
Nemluvě pak o použití v šablonách, kde je velice pohodlné přes
{$user->getIdentity()}
přistupovat k čerstvé entitě, než
si někde v beforeRender() metodě do šablony globálně vkládat
čerstvou entitu přihlášeného uživatele voláním repozitáře při
každém requestu.
Vlastní UserStorage
K tomu mít vždy aktuální identitu uživatele vám může právě pomoci
vlastní UserStorage. A jelikož nechci psát něco, co už bylo
někdy vymyšleno, proto bych rád ukázal jeden super balíček
majkl578/nette-identity-doctrine
(odkaz na Github),
který je tak jednoduchý a přitom šikovný, že nemá smysl vymýšlet
vlastní kolo.
Balíček do vaší Nette aplikace nainstalujete jednoduše přidáním přes Composer a vložením rozšíření do config.neon:
extensions:
doctrine2identity: Majkl578\NetteAddons\Doctrine2Identity\DI\IdentityExtension
Základní idea je jednoduchá. Doctrine entita reprezentující vašeho uživatele musí implementovat interface IIdentity, což jsme si ukázali už výše. Při autentizaci tedy váš autentifikátor vráti pouze entitu z repozitáře.
V getIdentity() pak vždy dostaneme aktuální entitu díky vlastnímu UserStorage, které je implementováno ve zmíněném balíčku. Navíc zde existuje podpora pro lazy načítání, takže samotná databáze se pro načtení aktuální entity zavolá právě tehdy, až budete z identity něco potřebovat. Ne dříve, tak jako by to bylo v případě naivního přístupu přes beforeRender().
Mrkněme se na samotnou implementaci UserStorage v balíčku, jak vlastně vypadá. Navíc to může být dobrá nápověda pro to vidět, jak si tvořit vlastní UserStorage a to i v podobě balíčků, pokud někdo nepreferuje Doctrine a rád by použil jiný přístup.
V implementaci můžeme vidět, že vlastní UserStorage dědí od výchozího a v konstruktoru navíc přidává závislost pro EntityManager, skrze který pak budeme získávat aktuální entitu. Dále se zde implementují dvě metody a to setIdentity(…), ke kterému dochází typicky při vrácení entity z autentifikátoru a pak samozřejmě getIdentity().
V setIdentity() dochází k šikovnému uložení entity konverzí na FakeIdentity ve výchozím UserStorage. De facto se (typicky při autentizaci) do FakeIdentity poznamená identifikátor naší Doctrine entity a o jakou třídu se jedná.
Při volání getIdentity() se pak z výchozího UserStorage získá FakeIdentity a z ní pomoci informace o identifikátoru a příslušné třídě se vytáhne aktuální entita voláním instance EntityManager.
A to je celá věda. Jednoduché, přímočaré, efektivní. Osobně používám balíček na několika projektech a funguje výborně.
Upozornění
V případě, že chcete změnit systém práce s identitami u již existující aplikace, počítejte s tím, že starší sessions uživatelů mohou při deserializaci vracet také starší výstup. Pokud tedy najednou Identity změníte na entitu User implementující IIdentity, tak v případě, že budete přistupovat k metodám, které nejsou dána rozhraním IIdentity (třeba budete volat nad entitou getFirstName()), může se klidně stát, že u identit serializovaných před aktualizací dostanete něco, co jste nemuseli zrovna očekávat, tj. starou identitu, která není vůbec vaší entitou z Doctrine.
To lze vyřešit tak, že buď budete předpokládat a vyřešíte si, že můžete získávat odlišné instance IIdentity odlišných tříd nebo smažete uložené sessions tak, aby se všichni uživatelé museli přihlásit znovu a tím jste všem od této chvíle při autentizaci ukládali do sessions správnou identitu.