Session trochu jinak než běžně
- daliborcaja
- Člen | 57
Standartně ukládám informace o zboží v košíku, posledních navštívených produktech a jiných podobných věcech do session. Co když ale chci tyto informace (vždy související s konkrétním uživatelem) uchovávat i mezi jednotlivými přihlášeními uživatele, i s jiného pc?
Přes sessions to již v tomto případě řešit nelze, protože i když je nastavím na dostatečnou dobu, aby zustavaly i při zavření prohlížeče, tak stejně na jiném pc samozřejmě nebude uložená cookies se session id.
Zajímal by mě váš názor na to jak to řešit.
- daliborcaja
- Člen | 57
No možná jsem to špatně formuloval. Nezajímalo mě až tak datové úložiště (tím samozřejmě může být databáze, soubor, …). Ale spíše jsem chtěl rozvinout diskuzi o tom jakým způsobem řešit nějakou obecnou třídu pro ukládání těchto uživatelských dat třeba podobně jako je tomu u session do mamespaces.
- Petr Motejlek
- Člen | 293
Pokoušet se rozvinout můžeš ;) Můj názor konkrétně na ukládání košíku a dalších uživatelských věcí je – když se uživatel může přihlásit, ukládat mu je do databáze (může se mi pak kontrolovat integrita, atd.), když se uživatel nepřihlašuje, ukládat mu je do session.
Nikdo snad neočekává, že když se na stránce nikde nepřihlašuje, že na jiném počítači se mu magicky objeví jeho košík ;)
- Filip Procházka
- Moderator | 4668
Entity-Respository-Mapper
a pak je ti jedno jak to mapper
persistuje :)
- daliborcaja
- Člen | 57
HosipLan napsal(a):
Entity-Respository-Mapper
a pak je ti jedno jak to mapper persistuje :)
Mol bys to trochu přiblížit prosím?
- Filip Procházka
- Moderator | 4668
tady máš nějaké to počtení: http://www.phpguru.cz/…rstev-modelu
V zásadě jde o to, že si navrhneš Entitu, což je třída, do které dáš data, když je načteš z databáze. Víceméně je to DibiRow (což je ValueObject), ale má properties nadefinované tak, aby odrážely strukturu z databáze (né nezbytně, protože entita může být složená i z více tabulek v DB).
Potom máš Repository, ten by měl umět pracovat s daným interface mapperu, něco jako
interface IMapper
{
function save($entity); // uložit entitu, sám si rozhodne, jestli aktualizuje, nebo ukládá novou
function find($id); // najít entitu s ID
function findBy(array $values); // předáš tomu pole hodnot, podle kterých má hledat. Vrátí entity co odpovídají
function findOneBy(array $values); // předáš tomu pole hodnot, podle kterých má hledat. Vrátí jednu entitu
function findAll(); // vrátí všechno
// popř. si můžeš napsat další funkce, které budou umět nějaké velice specifické funkce
// ale to spíše až v tom konkrétnějším mapperu
}
Potom takový mapper (třeba ShoppingCartDibiMapper
), který
implementuje tohle interface, předáš tomu repository a to respository už pak
jenom pracuje s tímhle a je mu jedno jestli to ukládá do databáze (dibi
mapper) nebo někam do session (ShoppingCartSessionMapper
).
Takže máš entitu
class ShoppingCartItem // není třeba, aby entita cokoliv rozšiřovala, netřeba vytvářet ActiveRecord
{
private $id; // přidat getter a nastavovat v mapperu reflexí, aby nešlo změnit
public $name; // jméno položky
public $cost; // kolik stojí
// .. další vlastnosti
public function getId()
{
return $this->id;
}
}
Máš repozitář
class ShoppingCartRepository extends Nette\Object
{
private $mapper;
public function __construct(IShoppingCartMapper $mapper) // interface IShoppingCartMapper extends IMapper
{
$this->mapper = $mapper
}
public function save(ShoppingCartItem $item)
{
$this->mapper->save($item);
// pokud ukládám novou, může mapper automaticky nastavovat ID
return $item; // a pak můžu s ID pracovat $item = $repository->save($item); echo $item->getId();
}
public function find($id)
{
return $this->mapper->find($id);
}
// metody jako findByName patří spíše sem
public function findByName($name)
{
return $this->mapper->findBy(array(
'name' => $name
));
}
// žádná metoda zde, by neměla šahat na úložiště, to má dělat mapper
}
jednoduchý mapper by pak mohl vypadat
class ShoppingCartDibiMapper extends Nette\Object implements IShoppingCartMapper
{
private $conn;
public function __construct(DibiConnection $conn)
{
$this->conn = $conn;
}
public function save(ShoppingCartItem $item)
{
if ($item->getId() === NULL) { // insert
$data = $this->itemToData($item); // vytáhne data z entity a vrátí jako pole
$id = $this->conn->insert('shopping_cart', $data)->execute();
$this->setIdentity($item, $id);
} else { // update
$data = $this->itemToData($item); // vytáhne data z entity a vrátí jako pole
// tady se velice hodí logika, která porovná v jakém stavu byla entita při načtení
// a v jakém je teď, aby se nemuselo posílat všechno, ale to jsou hodně pokročílé funkce
// a optimalizace se má dělat až když je potřeba, že :)
$this->conn->update('shopping_cart', $data)
->where('id = %i', $item->getId())->execute();
}
}
public function find($id)
{
$data = $this->conn->select('*')->from('shopping_cart')->where('id = %i', $id)->fetch();
return $this->load($data);
}
public function findAll()
{
$data = $this->conn->select('*')->from('shopping_cart')->where('id = %i', $id)->fetchAll();
$result = array();
foreach ($data as $row) {
$result[$row->id] = $this->load($row);
}
return $result;
}
private function load($data)
{
$item = new ShoppingCartItem;
$this->setIdentity($item, $data->id);
unset($data['id']);
foreach ($data as $prop => $val) {
$item->$prop = $val;
}
return $item;
}
private function setIdentity($item, $id)
{
$ref = Nette\Reflection\ClassReflection($item);
$idProp = $ref->getProperty('id');
$idProp->setAccessible(TRUE);
$idProp->setValue($item, $id);
return $item;
}
}
Tenhle mapper je opravu hodně jednoduchý, neumí řadit, limity, složitější věci, atd. A tohle je právě docela otrava dělat pro každou entitu. Ale je to nejlepší možné řešení. Protože je to pak krásné oddělené a moc hezky se s tím pracuje.
$repository = new ShoppingCartRepository(new ShoppingCartDibiMapper(dibi::getConnection()));
$item = new ShoppingCartItem();
$item->name = "Ponožky";
$item->cost = 20;
$item = $repository->save($item);
echo $item->getId();
$item = $repository->find(1);
echo $item->name;
Pokud chceš pokoročit pak ještě dál, tak si Repository
nadefinuješ jako službu, uděláš si na něj nějakou továrniču a pracuješ
s tím zase o něco hezčeji :)
// $this === $presenter
$repository = $this->getApplication()->getService('JmenoEshopu\ShoppingCart');
Tenhle přístup používá i Doctrine2 mimochodem :)
… sakra jsem se zase rozepsal :D
// přepsáno do kuchařky: https://pla.nette.org/…itory-mapper
Editoval HosipLan (19. 2. 2011 19:04)
- daliborcaja
- Člen | 57
Pěkný „ČLÁNEK“ :)
Přesně něco takového jsem měl na mysli, konkrétně hlavně tu entitu. Nicméně ještě trochu jinak a to tak že by se nejednalo o model pro konkrétní nákupní košík ale prostě o obecný model pro proměnlivá uživatelská data (teď nemám na mysli data jako příslušnost do uživatelské skupiny, nebo fakturační adresa, ani objednávky a faktury, ale data košíku, historie vyhledávání, poslední zobrazené produkty atd. jak už jsem psal výše).
Potom když budeme předpokládat jako storage MySQL databázi ta tabulka by vypadala asi takto:
CREATE TABLE IF NOT EXISTS `uzivatele_variabledata` (
`id` int(11) NOT NULL auto_increment,
`uzivatele_id` int(11) NOT NULL,
`namespace` varchar(100) collate utf8_czech_ci NOT NULL,
`data` text collate utf8_czech_ci NOT NULL,
PRIMARY KEY (`id`),
KEY `uzivatele_variabledata_ibfk_1` (`uzivatele_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_czech_ci AUTO_INCREMENT=1 ;
ALTER TABLE `uzivatele_variabledata`
ADD CONSTRAINT `uzivatele_variabledata_ibfk_1` FOREIGN KEY (`uzivatele_id`) REFERENCES `uzivatele` (`id`) ON DELETE CASCADE ON UPDATE CASCADE;
Jden řádek tabulky (jedna instance entity) by pak představovala např.
celý nákupní košík jednoho uživatele, namespace by určovala o jakou
komponentu jde (např. shoppingCart nebo productHistory). V entitě by se data
dobjektu košíku konvertovala pro uložení pomocí serialize()
a
zpět pomocí unserialize()
.
Dále by se dal mořná přístup k modelu integrovat např. do něčeho takového:
<?php
Environment::getUser()->getIdentity()->getVariabledata("shoppingCart");
?>
Zajímal by mě váš názor.
- Filip Procházka
- Moderator | 4668
Můj názor je takový, že by jsi se měl podívat na dokumentové databáze http://couchdb.apache.org/ a http://www.mongodb.org/ :)
- daliborcaja
- Člen | 57
HosipLan napsal(a):
Můj názor je takový, že by jsi se měl podívat na dokumentové databáze http://couchdb.apache.org/ a http://www.mongodb.org/ :)
Pokud jsem se dobře díval (mrknul jsem na to zatím jem zběžně), tak je třeba instalace na straně serveru. Sice u tohoto konkrétního projektu by to nebyl problém (je to pronajatý virtualserver), ale tuto feature plánuji využít i v jiných projektech kde tato možnost nebude.
Nicméně se na to ještě mrknu až najdu trošku času, může to být zajimavé pro velké projekty.
- daliborcaja
- Člen | 57
HosipLan napsal(a):
private function setIdentity($item, $id) { $ref = Nette\Reflection\ClassReflection($item); $idProp = $ref->getProperty('id'); $idProp->setAccessible(TRUE); $idProp->setValue($item, $id); return $item; }
Narazil jsem ještě při testování tvého příkladu pětivrstvého nodelu
na problém s ClassReflection
a nedaří se mi přes něj
přenést. Aplikace mi v mém případě spadne konkrétně na řádku:
$ref = ClassReflection($entity);
Když zakomentuji volání setIdentity tak vše funguje až na to že v entitě není samozřejmě id.
Dále jsem přišel ještě na to že v interface
IShoppingCartMapper
je třeba mít u parametru metody
save()
definovano ShoppingCartItem
, jinak mi to
nefungovalo.
function save(ShoppingCartItem $entity);
- paranoiq
- Člen | 392
Petr Motejlek napsal(a):
Nikdo snad neočekává, že když se na stránce nikde nepřihlašuje, že na jiném počítači se mu magicky objeví jeho košík ;)
magicky se objevit může, pokud by web při přihlášení ukládal nějakou sledovací cookie. stačilo by tedy přihlásit se z obou prohlížečů jednou a pak už přihlašování neřešit. problém je, jak zajistit smazání takové cookie, když se přihlásíte z cizího počítače. uživatelé, kteří se spoléhají na magii za sebou stopy zametat nebudou
- bojovyletoun
- Člen | 667
new Nette\Reflection\ClassReflection($item);
- Jinak proč to nefungovalo(kvůli chybějícímu určení typu v interface)? jakou to házelo hlášku? Z důvodu, že v interface neni určen typ a v třídě ano?
- daliborcaja
- Člen | 57
bojovyletoun napsal(a):
new Nette\Reflection\ClassReflection($item);
Taka blba chyba, ja sem slepy. Nicméně ještě pořád mi to nefunguje, a
to o řádek níž nezná metodu getProperty
, nevím proč, když
se dívám do nette tak by ji měl dědit z php. Zatim jsem to obešel pomocí
public $id
.
- Jinak proč to nefungovalo(kvůli chybějícímu určení typu v interface)? jakou to házelo hlášku? Z důvodu, že v interface neni určen typ a v třídě ano?
Je mi jasné, jen jsem chtěl HosipLana upozornit že tam má chybu, hlášku to myslím ani nevyhodilo (to se mi stává běžně třeba když nenajde třídu, prostě prohlížeč napíše něco ve smyslu že nemůže stránku zobrazit, možná stará verze nette, nebo chyba v konfiguraci)
- Filip Procházka
- Moderator | 4668
samozřejmě to umře na
Compile Error
Can't inherit abstract function IMapper::save() (previously declared abstract in IShoppingCartMapper)
Ve wiki jsem to opravil, aby nikoho nenapadlo ten obecný dědit. :)