Session trochu jinak než běžně

Upozornění: Tohle vlákno je hodně staré a informace nemusí být platné pro současné Nette.
daliborcaja
Člen | 57
+
0
-

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.

Lopata
Člen | 139
+
0
-

Databáze.

daliborcaja
Člen | 57
+
0
-

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
+
0
-

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
+
0
-

Entity-Respository-Mapper a pak je ti jedno jak to mapper persistuje :)

daliborcaja
Člen | 57
+
0
-

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
+
0
-

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;
	}
}

*reflexe

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-

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
+
0
-
  • 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
+
0
-

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)

studna
Člen | 181
+
0
-

Já to mám takhle (Nette dev):

<?php
$ref = \Nette\Reflection\ClassReflection::from( $entity );
?>

HosipLan tam chybu nemá, nastínil jen obecný interface pro mapper.

Editoval studna (25. 2. 2011 17:16)

Filip Procházka
Moderator | 4668
+
0
-

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. :)