Jednoduchý web bez databáze aneb Nette den po dni

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

Ahoj všem. Začala jsem s Nette a po třech dnech se mi zvedá kufr, když se podívám na jakýkoliv svůj starší kód v PHP. V následujícím textu zkusím popsat, jak jsem postupovala při tvorbě malého, ale šikovného webu bez použití databáze. Text vznikal během psaní kódu, takže je mírně emotivní, obsahuje tvrzení a jejich popření atakdále. Třeba to někomu pomůže. Třeba mi na to řeknete, co je navrženo blbě a mělo by se dělat líp.

magraata
Člen | 4
+
0
-

nette – day 1

Pročetla jsem tutorial, proletěla diskuzní fóra s předmětem like %začátečník%. Vytvořila si doporučenou adresářovou strukturu, zkusila zkopírovat DefaultPresenter, šablony Default.default.phtml, @layout.phtml. Pak začala honička po diskuzích a příkladech: jak udělat menu? Našla jsem šablonu od DG, opsala ji do @menu.phtml, ale čím ji naplnit?

Položky menu patří do modelu? Určitě jo, protože souvisí s obsahem, nikoli formou. Nakonec jsem se rozhodla nacpat strom menu do config.ini, pobídlo mě hlavně zjištění, že v config.ini můžou být sekce, podsekce a pole. Takhle se dá do configu pohodlně nacpat víceúrovňové menu. Trochu oříšek bylo zjistit, kam patří config.ini: tedy do app/.
část config.ini:

menu.polozka1[]="Položka 1"
menu.polozka2[]="Položka 2"
menu.polozka2.champagne="Champagne"
menu.polozka2.bourgogne="Bourgogne"
menu.polozka3[]="Položka 3"
menu.polozka3.polozka4[]="Položka 4"
menu.polozka3.polozka4.cognac="Cognac"

Další úkol teda byl rozpárat správně položky configu a naplnit jimi šablonu @menu.phtml. Zatím se mi to nelíbí, protože je tam změť ifů, instanceOfů a countů, ale funguje to.
@menu.phtml:

<ul>
    {foreach $menu as $key=>$item}
      {if $item instanceOf Config}
	<li><a href="{plink Default:default 'page'=>$key}" title="{$item[0]}">{$item[0]}</a>{include '@menu.phtml', 'menu' => $item}</li>
      {else}
	{if $key!="0"}
	<li>{if count($item)>1} {include '@menu.phtml', 'menu' => $item}
	    {else}<a href="{plink Default:default 'page'=>$key}" title="{$item}">{$item}</a>{/if}
	</li>
	{/if}
      {/if}
    {/foreach}
</ul>

Následovalo zjištění, že odkaz musí být na default s parametrem, nebo se ušablonuju k smrti (nebo zjistím, že mi stačí jedna šablona, ale musím to někde říct). Proto je tam plink Default:default ‚page‘=>cosi. Rekurze bez potíží. Sláva, aspoň něco. Parametr page je můj oblíbenej, v menu je definovaný klíč, který se náhodou rovná page a hodnota je český název. Hodnota se zároveň použije pro pojmenování položky menu a zároveň pro nadpis druhé úrovně (proměnná heading). Pěkná vychytávka je zjištění téhle hodnoty z parametru page, menu je totiž vnořené, takže rozpárání config.ini na pole polí a dvojice klíč-hodnota je bohužel nechutnost plná ifů, castů a is_arrayů.

I na malém webu je radost se vyznat. Proto mě baví mít tam klikací cestu. Do DefaultPresenteru jsem napsala metodu getPath, která vrací pole obsahující aktuální větev stromu menu. To už šlo dobře, protože hledání hodnoty pro zadaný klíč je vlastně tentýž úkol.
@path.phtml:

<div id='path'>
    {foreach $path as $key=>$item}
    {if $key!=$page}<a href="{plink Default:default 'page'=>$key}" title="{$item}">{$item}</a>{else}{$item}{/if}{if !$iterator->last} &gt;{/if}
    {/foreach}
</div>

Nejtěžší pro dnešek bylo uložení obsahu: nabízí se adresář models, ale do toho se hnedtak nedostanu ze šablony… proto došlo nakonec na totéž, jako u heading a path, na proměnnou v DefaultPresenteru.
část DefaultPresenter.php:

public function renderDefault()
  {
    $config=Environment::loadConfig();
    //Debug::dump((array) $config["menu"]);
    $page=$this->param["page"];
    $this->template->page=$page;
    $this->template->menu = $config["menu"];
    $this->template->heading = $this->getHeading($page,(array)$config["menu"]);
    $this->template->path = $this->getPath($page,(array)$config["menu"]);
    $this->template->contenttext=file_get_contents(APP_DIR."/models/".$page.".html");
  }

Tohle řešení se mi moc nelíbí – nacpat celej soubor (byť vlastně dost krátkej) to proměnné, to zavání PHP… :-(

Jediné, co mi zbývalo, bylo vložit v šabloně obsah tak, jak je a ne s htmlSpecialChars (odpověď je ve vykřičníku):
část @layout.phtml:

<body>
    <h1>Jméno webu</h1>
{include '@path.phtml', 'path' => $path}
{include '@menu.phtml', 'menu' => $menu}
<h2>{$heading}</h2>
{!$contenttext}
</body>

Ještě mi vrtá hlavou, zda je v pořádku file_get_contents a nepoužít raději Nette\IO\SafeStream. Vylepšit DefaultPresenter, aby si dokázal poradit s neexistující stránkou (nejspíš skočit na default, 404 nemám ráda).

V tomto stavu je Default.default.phtml prázdnej, @layout.phtml, @menu.phtml a @path.phtml mají pár řádků. V adresáři models jsou soubory pojmenované stejně jako položky menu (tedy klíče, ne hodnoty) s příponou html. Alelujá, můžu si vesele stylovat. A když budu estét, zkusím napravit to párání config.ini. Pak se uvidí, jak to půjde dál, chci totiž na web přidat obrázky. Tři nejlepší dojmy po prvním dni? Debug, debug a debug…

magraata
Člen | 4
+
0
-

nette – day 2

Proti své vůli jsem hned začala pátrat dál, jak udělat krásný nový malý web s Nette. Při googlení zjišťuju, že ač je název Nette (čistá) vtipnej, hledá se blbě. Udělala jsem jeden značný objev: pokud nenajdu odpověď na svou otázku na prvních dvou stranách, co mi Google vrátí, pokouším se zřejmě o nějakou kravinu.

Prvně jsem zkusila přizpůsobit presenter a udělat šablonu pro 404: sice nemám ráda 404, ale chci to vyzkoušet. Takže, DefaultPresenter se změnil takto:

public function actionDefault(){
  $config=Environment::loadConfig();
  $this->menu=(array)$config["menu"];
  if (key_exists("page",$this->param))
    $this->page=$this->param["page"];
  else
    $this->page=$config->page->default;
  if ($this->page!=""&&!file_exists(APP_DIR."/models/".$this->page.".html")){
    $this->view = 'error';
  }
}

V config.ini jsem nastavila výchozí stránku prezentace. To se mi líbí. Vše podstatné je v akci a ne ve vykreslení (jako čechohnidopicha mě těší tohle pojmenování místo renderování;-) Vykreslování jsou zatím dvě: normální a chybové:

public function renderDefault()
{
  $this->template->page=$this->page;
  $this->template->menu = $this->menu;
  $this->template->contenttext=file_get_contents(APP_DIR."/models/".$this->page.".html");
  $this->template->heading = $this->getHeading($this->page,$this->menu);
  $this->template->path = $this->getPath($this->page,$this->menu);
}
public function renderError(){
  $this->template->menu = $this->menu;
  $this->template->path = array();
  $this->template->heading="Stránka neexistuje";
  $this->template->contenttext="";
  $this->setView("error");
}

Nejprve jsem zkusila setLayout(), ale rozhodně mi stačí setView() a ušetřím jednu šablonu @error.phtml. Je jen potřeba doplnit obsah do Default.error.phtml.

Další mé velké přání je udělat jednoduchou galerii:
Prvně jsem narazila na potíž, že zatímco file_exists() vrací TRUE, $image = Image::fromFile() ztroskotalo… Odpověď je „práva“! Další pešek je velikost paměti PHP, což ale důvěrně znám z minulých let. Opět se mi zazdálo, jako kdybych psala něco v PHP a mráz mi přeběhl po zádech.

Chci, aby se vzaly obrázky z adresáře, který se jmenuje stejně jako stránka (parametr page). Aby se z nich udělaly náhledy (pokud neexistujou) a aby se ze souboru info.txt v tomtéž adresáři vzaly pořadí a jméno obrázku. Tak do toho:
Soubor s popiskama je otázka file a explode, obrázky zpracovává DefaultPresenter takto:

private function getImages($description){
  $images=array();
  foreach($description as $desc){
	if (!file_exists(WWW_DIR."/pics/".$this->page.'/thumb-'.$desc[0])){
	  $image = Image::fromFile(WWW_DIR."/pics/".$this->page.'/'.$desc[0]);
	  if ($image->getWidth()>$image->getHeight())
	    $thumb = $image->resize(NULL,$this->config->thumbsize);
	  else
	    $thumb = $image->resize($this->config->thumbsize,NULL);
	  $thumb=$thumb->crop(0,0,$this->config->thumbsize,$this->config->thumbsize);
	  $thumb->save(WWW_DIR."/pics/".$this->page.'/thumb-'.$desc[0]);
	}
	$images[]=array("pics/".$this->page.'/thumb-'.$desc[0], "pics/".$this->page.'/'.$desc[0], $desc[1]);
  }
  return $images;
}

Přičemž přibyl renderGallery, který kromě renderDefault obsahuje novou proměnnou a změnu pohledu:

$this->template->gallery=$this->getImages($this->getDescription());
$this->setView("gallery");

V šabloně Default.gallery.phtml se zobrazí cosi takového:

<h3>galerie</h3>
<table class="gallery">
  <tr>
  {foreach $gallery as $image}
    <td><a class="thickbox" rel="gallery1" href="{$image[1]}"><img src="{$image[0]}" alt="{$image[2]}"></a>
	<div class="description">{$image[2]}</div>
    </td>
    {if $iterator->getCounter()/3==round($iterator->getCounter()/3)}
    </tr><tr>
    {/if}
  {/foreach}
  </tr>
</table>

Když tak přemýšlím nad tím, jak upravovat obsah, přesunu stránky HTML z app/models do document_root. Nejhorší, co se může stát, je, že uživatel zadá adresu přímo té stránky. To by ale nemuselo vadit. Anebo naopak všechno do models? V noci se mi to rozleželo. Jo a to včerejší getPath a getHeading nefunguje. Zítra předělám.

Tak, teď se fakt pustím do toho stylování, ať si odpočinu. Na druhou stranu se bojím, že mě to bude nudit. Do šablony @menu.phtml dám místo <li> <li id=„menu-{$key}“>, seznam obalím do divu. Přidám javascript s dropdownem, jquery, thickbox a další ňamky. To už se ale Nette Nettýká.

magraata
Člen | 4
+
0
-

nette – day 3

Na dnešek jsem se vyloženě těšila. Menu jsem dala do objektu Menu, coby seznam objektů MenuItem, každej MenuItem má klíč, jméno a rodiče. Mezi metody Menu patří getMenuItem($key), getTree($root), která vrací menu jako strom klíčů, a toArray(), protože v šablonách bych musela mít iterátor přes položky menu, což by bylo obskurní v rekurziním volání a vlastně mi stačí jen pole. Nadpis je úplně srandovní, protože je to dnes:

private function getHeading(){
    return $this->menu->getMenuItem($this->page)->getLabel();
}

Menu je taky mnohem jednodušší, i šablona pro něj. Předávám jí sice dva parametry, jeden s tím stromem, druhej s poletem klíčů a jmen. Šlo by to asi ještě zkrášlit, ale to si nechám na jindy. Šablona @menu.phtml:

<ul>
    {foreach $tree as $key=>$sub}
    <li id="menu-{$key}">
	<a href="{plink Default:default 'page'=>$key}" title="{$menu[$key]}">
	   {$menu[$key]}
	</a>
	{if !empty($sub)} {include '@menu.phtml', 'menu'=>$menu, 'tree' => $sub} {/if}
    </li>
    {/foreach}
</ul>

Strom menu se udělá přímo v objektu Menu a i když se to Nette nettýká, vypadá takhle:

function getTree($root=NULL){
	$tree=array();
	foreach($this->menuitems as $menu){
	  if ($menu->getParent()==$root){
	    $tree[$menu->getKey()]=$this->getTree($menu->getKey());
	  }
	}
	return $tree;
}

Podobně se zjednodušilo a zároveň zfunkčnilo cestování:

function getPath($key,$oldpath=array()){
	$path=$oldpath;
	foreach ($this->menuitems as $menu){
	  if ($menu->getKey()==$key){
	    $path[]=$menu;
	    if ($menu->getParent()!=NULL)
		$path=$this->getPath($menu->getParent(),$path);
	  }
	}
	return $path;
}

Já vím, oldpath je blbý název proměnné.

Další věc se mi tím vyřesila sama, a to výpis potomků položky menu. Tím už nikoho nechci unavovat. Mezitím si styluju a plním obsah. Co mi vrtá hlavou je to umístění. A co mi vrtá ještě víc: když budu v HTML chtít odkaz nebo obrázek, co s tím? Odpověď zní: integruj Texy. Tak jo: stáhnu, do libs/ nakopíruju texy/ a do DefaultPresenteru přidám:

$this->texy=new Texy();
$this->texy->imageModule->root = "pics/";
$this->texy->headingModule->top=3; // h1 a h2 se generujou v šabloně

Obsah proměnné contenttext získám metodou getContents()

private function getContents(){
    $contents=file_get_contents(WWW_DIR."/data/".$this->page.".html");
    return $this->texy->process($contents);
}

Voil`a. 7 řádků a jsem bez potíží. Později se ukáže, jak to zaonačit s odkazama. Takže web je _pohodlně_ naplněn (texty v texy, obrázky jen nakopíruju do složek, semtam pozměním strukturu v config.ini). A zítra se podívám na stránkování a případně hezká URL.

Ještě dodatek ke 3. dni. Zápis menu se v config.ini malinko změnil, kdyby to někdo zkoušel, jistě by na to přišel, ale nechci nikoho trápit:

menu.polozka1[]="Položka 1"
menu.polozka2[]="Položka 2"
menu.polozka2.champagne[]="Champagne"
menu.polozka2.bourgogne[]="Bourgogne"
menu.polozka3[]="Položka 3"
menu.polozka3.polozka4[]="Položka 4"
menu.polozka3.polozka4.cognac[]="Cognac"

Rozdíl je v tom, že všechny položky končí [].

Editoval magraata (27. 1. 2010 21:19)

Ondřej Mirtes
Člen | 1536
+
0
-

Mno, díky za popis. Nebudu louskat každej řádek, ale napíšu pár bodů.

  • Začít se učit Nette pokusem o napsání univerzální komponenty pro menu není ideální. O tom frameworku nevíš prakticky nic a snažíš se psát něco, po čem chceš, aby tě doprovázelo při vývoji i dalších projektů. Vsadím se, že až získáš nějaké zkušenosti (a přečteš si víc z dokumentace), uděláš s touhle třídenní prací shift+del :)
  • Nevyužíváš vůbec více presenterů – napsala sis nějaké includování souborů vlastně pro celý web. Tohle bych samozřejmě kladl za vinu nekompletní dokumentaci, než tobě. Tvůj přístup zavání víc čistým PHP než možnostmi, které Nette nabízí.
  • Zobrazení 404ky se v Nette řeší velice elegantně – throw new BadRequestException ;) Na lokálu se ti zobrazí Laděnka, na produkčním serveru se spustí ErrorPresenter a vykreslí se jeho šablona – kterou si samozřejmě nastavíš a upravíš.
  • Environment::loadConfig() patří do bootstrapu (při každém zavolání se znova načítá config.ini). Definované položky v něm pak získáváš pomocí Environment::getConfig('menu').
  • Texy se řeší pomocí Template helperu. Registruješ si ho jako helper v šabloně a pak můžeš volat {!$textClanku |texy}.
  • Jestli je vhodné mít strukturu menu v configu, to je diskutabilní. Mně ten zápis přijde zmatený. Daleko pohodlnější je IMHO udělat si ho jako pole (klidně vnořované) v BasePresenteru.

Osobně ti ke studiu doporučuju dostupná videa o Nette a seriál o Nette na Zdrojáku

Každopádně hodně štěstí a neboj se tu ptát :)