Archiv pro rubriku: nette

Fotoalbum v NETTE (flickr style) – jak obejít databázi

Dva body, které mi vadili na starém albu:

  • složité nahrávání fotek (přes již prastarou aplikaci Olympus Camedia Photoalbum)
  • proprietální databáze Olympus Camedia Photoalbum postavená na MS ACCESS 1997, bez rozumné možnosti rozšíření

A jeden bod, který jsem měl jako požadavek na album nové:

  • minimální závislost na databázi

Když jsem psal tyto řádky netušil jsem, že se databáze budu schopen zbavit úplně (alespoň na straně uživatele). Došlo mi to ve chvíli, kdy jsem nahrával první album a následně jsem se musel přepínat do prostředí databáze a tam zadávat údaje (jméno, cesta, parent_id, …). Čím to nahradit?

INI souborem. Výhoda je, že je to i pro laika pochopitelné. Stačí do přidávané složky nahrát soubor album.ini a v něm mít následující obsah:

[album]
nazev="Vildštejn - Mohyly"
dukaz=1753
datum="5. - 6. 10. 2013"
 
[nezobrazovat]
fotka[]=DSC_9317.JPG
fotka[]=DSC_9318.JPG

Toto nepotřebuje delšího komentáře. V oddíle je zvykem mít název tvořen ve tvaru „1753 – Vildštejn – Mohyly 5. – 6. 10. 2013“ a obsah souboru je pochopitelný i pro laika.

Ještě drobné zabezpečení v .htaccess.

RewriteRule (.*)album.ini$ - [F]

Práce se souborem album.ini:
Načítání (zatím i s databází):

	public function getNameFromPath($path) {
		$row = $this->connection->fetchColumn('SELECT `desc` FROM `I_dir` WHERE `webalize-path`=? LIMIT 1', strtr($path, '/', '\\'));
		if ($row['desc'] == '') {
			$ini_array = @parse_ini_file(ALBUM_PATH . $path . 'album.ini');
			if (!isset($ini_array['nazev'])) return 'Album nemá vyplněno název';
			return iconv('WINDOWS-1250', 'UTF-8', "{$ini_array['dukaz']} - {$ini_array['nazev']} {$ini_array['datum']}");
		} else {
			return $row;
		}
	}

Ukládání:
Bude potřeba celou databázi převést na album.ini soubory a dále ve webovém rozhraní opatřit admin módem, aby šlo z rozhraní označit fotky, které nemají jít vidět směrem ven. Na webu jsem nenašel rozumnou komplexní funkci/třídu pro zápis do tohoto formátu, tak jsem si ji napsal sám.

Funkce dostane cestu k souboru a pole, které uloží do struktury. Tedy nejen že uloží, ale umí do souboru i data přidávat ne jen přepisovat.

	/**
	 *	přidá do existujícího INI souboru hodnoty ve tvaru $arr['klic']['podklic'] = $hodnota
	 *
	 */	 	 	 	
	public function saveIni($array, $path) {
		$ini_array = parse_ini_file(ALBUM_PATH . $path . 'album.ini', true);
 
		$res = array();
		foreach($ini_array as $key => $val) {
		    if(is_array($val)) {
		        $res[] = "[$key]";
		        foreach($val as $skey => $sval) {
		        	if(is_array($sval)) {
		        		foreach($sval as $skey1 => $sval1) {
							$res[] = "{$skey}[] = ".(is_numeric($sval1) ? $sval1 : '"'.$sval1.'"');
						}
					} else {
						$res[] = "$skey = ".(is_numeric($sval) ? $sval : '"'.$sval.'"');
					}
				}
				if (array_key_exists($key, $array)) {
					foreach($array[$key] as $skey => $sval) {
						if(is_array($sval)) {
			        		foreach($sval as $skey1 => $sval1) {
								$res[] = "{$skey}[] = ".(is_numeric($sval1) ? $sval1 : '"'.$sval1.'"');
							}
						} else {
							$res[] = "$skey = ".(is_numeric($sval) ? $sval : '"'.$sval.'"');
						}
					}
					unset($array[$key]);
				}
				$res[] = '';
		    } else {
				$res[] = "$key = ".(is_numeric($val) ? $val : '"'.$val.'"');
				if (array_key_exists($key, $array)) {
					$res[] = "$array[$key] = ".(is_numeric($array[$key]) ? $array[$key] : '"'.$array[$key].'"');
					unset($array[$key]);
				}
			}
		}
		foreach($array as $key => $val) {
			if(is_array($val)) {
		        $res[] = "[$key]";
		        foreach($val as $skey => $sval) {
		        	if(is_array($sval)) {
		        		foreach($sval as $skey1 => $sval1) {
							$res[] = "{$skey}[] = ".(is_numeric($sval1) ? $sval1 : '"'.$sval1.'"');
						}
					} else {
						$res[] = "$skey = ".(is_numeric($sval) ? $sval : '"'.$sval.'"');
					}
				}
				$res[] = '';
		    } else {
				$res[] = "$key = ".(is_numeric($val) ? $val : '"'.$val.'"');
			}
		}
 
		file_put_contents('safe://' . ALBUM_PATH . $path . 'album.ini', implode("\r\n", $res));
	}

Jediné co mi nyní na ni vadí je standardní funkce parse_ini_file(), která nenačte komentáře a tak o ně tímto přijdeme.

Fotoalbum v NETTE (flickr style) – práce s fotkami

Myšlena práce s fotkami na straně serveru…

Co bylo třeba vyřešit:

  1. zmenšení fotek
  2. záloha velkých originálů
  3. vytvoření náhledů
  4. vytvoření úvodní fotky pro danou složku
  5. (vyplynulo až později) „kešování“ dat, pro dostatečně rychlé procházení fotoalbem

Všechno se děje vlastně sériově. Vždy se ověří, zda danou operaci je nutné dělat a pokud ne, tak se vynechá. Zároveň se dané části kešují, jelikož z praxe vyplynulo, že i když není třeba nic dělat, tak samotné ověřování trvá řády jednotek až desítek sekund. A na to nikdo čekat nechce.

Poznámka: Vím, že kód níže není optimální a zasloužil by jistou refaktorizaci, ale to bych tento článek a především fotoalbum nenapsal ani za rok (Ano píšu ho ve svém volném čase a pro dobro věci. Ne pro peníze.). A taky se omlouvám za ne úplně 100% prezentaci kódu, ale neznám lepší plugin do WordPressu, který by to zvládal dokonale.

# VYTÁHNEMKE SI SLOŽKY A K NIM ÚVODNÍ FOTKU (samozřejmě jen pokud již nejsou v cache)
$value = $cache->load("folders-{$path}");
if ($value === NULL) {
	$i=0;
	foreach (Finder::findDirectories('*')->exclude('[0-9][0-9][0-9]', '_big')->in($path)->orderByName('DESC') as $key => $file) {
		$this->template->dirs[$i]['name'] = $this->I->getNameFromPath(substr($key, strlen(ALBUM_PATH)).'/');
		// vytahneme si small_home
		foreach (Finder::findFiles('_small_home.png')->in($key) as $key_home => $file_home) {
			$this->template->dirs[$i]['small_home'] = ALBUM_DIR . substr($file_home->getPath(), strlen(ALBUM_PATH)) . '/_small_home.png';
		}
		$this->template->dirs[$i++]['pos'] = substr($key, strlen(ALBUM_PATH)+5);
	}
	$cache->save("folders-{$path}", $this->template->dirs);
} else {
	$this->template->dirs = $value;
}
 
# VYTÁHNEMKE SI FOTKY (samozřejmě jen pokud již nejsou v cache)
$value = $cache->load("files-f-{$path}");
if ($value === NULL) {
	# POKUD EXISTUJÍ TAKOVÉ, KTERÉ MAJÍ VĚTŠÍ STRANU VĚTŠÍ NEŽ 1200PX, TAK JE:
	#	- ZMENŠÍME
	# 	- VELKÉ ORIGINÁLY ZAZÁLOHUJEME
	# 	- PŘENESEME EXIF INFORMACE			
	foreach (Finder::findFiles('*.jpg', '*.jpeg')->exclude('*_small*')->dimensions('>1200', '>1200')->in($path) as $key => $file) {
		$image = @Image::fromFile($key); // ini_set(‘gd.jpeg_ignore_warning’, 1);
 
		$image->resize(1200, 1200, Image::SHRINK_ONLY | Image::FIT);
 
		$ex = exif_read_data($key, 'EXIF');
		if(!empty($ex['Orientation'])) {
			switch($ex['Orientation']) {
				case 8:
					$image->rotate(90, 0);
					break;
				case 3:
					$image->rotate(180, 0);
					break;
				case 6:
					$image->rotate(-90, 0);
					break;
			}
		}
 
		$image->sharpen();
 
		$s = $key;
		$d = $file->getPath() . '/_big/' . $file->getFilename();
		$fd = new SplFileInfo($file->getPath() . '/_big/');
		if (!$fd->isDir()) mkdir($file->getPath() . '/_big/');
		rename($s, $d);
		$image->save($key);
 
		// přeuložíme EXIF informace - použita externí knihovna PelJpeg
		$input_jpeg = new PelJpeg($d);
		$exif = $input_jpeg->getExif();
		if ($exif != null) {
			$output_jpeg = new PelJpeg($key);
			$output_jpeg->setExif($exif);
			$output_jpeg->saveFile($key);
		}
	}
 
	# DÁLE VYTVOŘÍME MALÉ NÁHLEDY
	foreach (Finder::findFiles('*.jpg', '*.jpeg')->exclude('*_small*')->exclude($denyFiles)
		->filter(function($file) {
			$file_extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
			$info = new SplFileInfo($file->getPath() . '/' . $file->getBasename('.' . $file_extension) . '_small_500' . '.jpg');
			return !$info->isFile();
		})->in($path) as $key => $file) {
 
		// vytvoření náhledu
		$image = @Image::fromFile($key);
		$image->resize(500, 500, Image::SHRINK_ONLY | Image::FIT);
 
		$image->sharpen();
		$file_extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
		$image->save($file->getPath() . '/' . $file->getBasename('.' . $file_extension) . '_small_500' . '.jpg');
	}
 
	# DÁLE VYTVOŘÍME ÚVODNÍ NÁHLED, KTERÝ SE BUDE ZOBRAZOVAT MÍSTO OBRÁZKU SLOŽKY
	$small_home = Image::fromBlank(360, 360, Image::rgb(255, 255, 255, 127));
	$i=0;
	foreach (Finder::findFiles('*.jpg', '*.jpeg')->exclude('*_small*')->exclude($denyFiles)
		->filter(function($file) {
			$info = new SplFileInfo($file->getPath() . '/_small_home.png');
			return !$info->isFile();
		})->in($path) as $key => $file) {
		$image = @Image::fromFile($key);
		$image->saveAlpha(true);
		$image->resize(250, 250, Image::SHRINK_ONLY | Image::FIT);
		$blank = Image::fromBlank($image->getWidth()+20, $image->getHeight()+20, Image::rgb(255, 255, 255));
		$blank->alphaBlending(true);
		$blank->saveAlpha(true);
 
		$blank->place($image, 10, 10);
 
		$blank->rotate(mt_rand(-60, 60), Image::rgb(255, 255, 255, 127));
		$blank->sharpen();
 
		$small_home->place($blank, 20+mt_rand(-15, 15), 20+mt_rand(-15, 15));
 
		if ($i++ == 5) {
			$small_home->resize(200, 200, Image::SHRINK_ONLY | Image::FIT);
			$small_home->save($file->getPath() . '/_small_home.png', NULL, Image::PNG);
 
			$upPath = substr($file->getPath(), 0, strrpos($file->getPath(), '/')+1);
			$cache->remove("folders-{$upPath}"); // pokud tvoříme náhled, tak smažeme cache předka složky, aby se mi při dalším načtení tato cache obnovila
			break;
		}
	}
 
	# A KDYŽ MÁME VŠECHNO PŘIPRAVENO, TAK SI JEŠTĚ JEDNOU NAČTU KOMPLETNÍ INFO, KTERÉ SI PAK ULOŽÍM DO CACHE
	$i=0;
	foreach (Finder::findFiles('*.jpg', '*.jpeg')->exclude('*_small*')->exclude($denyFiles)->in($path)->orderByEXIFTime() as $key => $file) {
		$this->template->files[$i]['key'] = ALBUM_DIR . substr($key, strlen(ALBUM_PATH));
		$exif_ifd0 = read_exif_data($file, 'IFD0', 0);
		$this->template->files[$i]['file'] = date("d.m.y H:i", strtotime($exif_ifd0['DateTime']));
 
		$file_extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
		$tf = @Image::fromFile($file->getPath() . '/' . $file->getBasename('.' . $file_extension) . '_small_500' . '.jpg');
		$this->template->files[$i]['w'] = $tf->getWidth(); 
		$this->template->files[$i]['h'] = $tf->getHeight(); 
		$this->template->files[$i++]['pos'] = ALBUM_DIR . substr($file->getPath(), strlen(ALBUM_PATH)) . '/' . $file->getBasename('.' . $file_extension) . '_small_500' . '.jpg';
	}
	$cache->save("files-f-{$path}", $this->template->files);
} else {
	$this->template->files = $value;
}

Jednotlivé části dle mě nemá smysl detailněji popisovat. Je třeba se tím prokousat a pochopit. Pokud by byly dotazy nechť poslouží komentáře.

Jedna konstrukce by šla napsat jednodušeji, jen DEBIAN v současnosti neobsahuje správnou verzi PHP a v té co tam je obsahuje známou chybu…

// Takže místo...
$tf = @Image::fromFile($file->getPath() . '/' . $file->getBasename('.' . $file->getExtension()) . '_small_500' . '.jpg');
// musím používat toto...
$file_extension = pathinfo($file->getFilename(), PATHINFO_EXTENSION);
$tf = @Image::fromFile($file->getPath() . '/' . $file->getBasename('.' . $file_extension) . '_small_500' . '.jpg');

Fotoalbum v NETTE (flickr style) – příprava složek

V této kapitole prezentuji, jak jsem musel připravit stromovou strukturu současného fotoalba.
webalizace – odstranění diakritiky a nahrazení speciálních znaků (např. mezer) pomlčkou

Výchozí stav:
Struktura složek ve formátu /ALBUM/001/001/038/019/. Tuto strukturu jsem měl uloženou v databázi ve formátu (id, parent_id, path, description). Tuto strukturu jsem dále rozšířil o atributy level (hloubka zanoření ve stromu), webalize-desc )(webalizovaný název složky), webalize-path (webalizovaná cesta).

1. krok – vytvoření webalizovaného popisku v databázi

	public function createCoolURL() {
		$this->connection->exec('begin');
		foreach ($this->connection->table('I_dir') as $row) {
			$this->connection->exec('UPDATE `I_dir` SET `webalize-desc`=? WHERE `id`=? LIMIT 1', Strings::webalize($row->desc, '._', FALSE), $row->id);
		}
		$this->connection->exec('commit');
	}

2. krok – vypočtení atributu level

	public function computeLevel() {
		$this->connection->exec('begin');
		foreach ($this->connection->table('I_dir') as $row) {
			$this->connection->exec('UPDATE `I_dir` SET `level`=? WHERE `id`=? LIMIT 1', substr_count ($row->path, '\\')-1, $row->id);
		}
		$this->connection->exec('commit');
	}

3. krok – vytvoření webalizovaného překladu cesty
Tady je třeba se zamyslet co vlastně děláme a proč to níže může vypadat relativně složitě. Výchozí stav je takový, že známe cestu k cílové složce (/001/001/038/019/) a její název. Ale abychom byli schopni univerzálně přejmenovat celou cestu, tak musíme začít od kořene. Tj. první přejmenovat složky level=1 (/001/). Následně level=2 (/001/001/) a to pomocí parent_id, kde již víme, že se ta složka nějak jmenuje a jen připojit webalizovaný název aktuální úrovně.

	public function createWebalizePath() {
		$this->connection->exec('begin');
		foreach ($this->connection->fetchAll('SELECT DISTINCT `level` FROM `I_dir` WHERE `level`>0 ORDER BY `level` ASC') as $l) {
			foreach ($this->connection->fetchAll('SELECT `a`.`id`, `a`.`webalize-desc` AS `wd`, `b`.`webalize-path` AS `path` FROM `I_dir` `a` JOIN `I_dir` `b` ON `a`.`parent_id`=`b`.`id` WHERE `a`.`level`=?', $l->level) as $row) {
				$this->connection->exec('UPDATE `I_dir` SET `webalize-path`=? WHERE `id`=? LIMIT 1', ($row->path.$row->wd.'\\'), $row->id);
			}
		}
		$this->connection->exec('commit');
	}

4. krok – přejmenování samotných složek
Záhadná formule REVERSE(SUBSTRING(REVERSE(`path`), 5))

  • REVERSE(`path`) z „\001\002\“ do „\200\100\“
  • SUBSTRING(…, 5) z „\200\100\“ do „100\“
  • REVERSE(…) z „100\“ do „\001“

Zbytek je snad jasný…

	public function renameFolders() {
		foreach ($this->connection->fetchAll('SELECT `path`, REVERSE(SUBSTRING(REVERSE(`path`), 5)) AS `newPath`, `webalize-desc` AS `wd` FROM `I_dir` WHERE `level`>0 ORDER BY `level` DESC') as $row) {
			@rename(ALBUM_PATH . strtr($row->path, '\\', '/'), ALBUM_PATH . strtr($row->newPath, '\\', '/') . $row->wd);
		}
	}

Zatím tu není řešeno přidání nové složky, ale to bude principiálně stejné. Jen budu muset vyřešit jak dám programu vědět, která složka a kde přibyla.
Nápady – databáze (chci se ji maximálně vyhnout a velká náchylnost na chybu), Nějaký FLAG soubor – zatím se mi jeví jako dobré řešení. Musel bych jen udělat „robota“, který by hledal soubor jistých parametrů a v případě nálezu by danou složku zpracoval a soubor třeba smazal. Ale to předbíhám…

Fotoalbum v NETTE (flickr style)

Již delší dobu plánuji, že přepíšu dávno nevyhovující fotoalbum oddílu BVÚ.

Nevýhody současného alba:
Viditelné:

  • malé náhledy (generuje je aplikace)
  • v případě aktualizace fotek, není dostupné
  • závislost na datumu souboru (pokud se nepovedlo nahrání, bylo tam nesmyslné datum)

Neviditelné:

  • složité nahrávání fotek (přes již prastarou aplikaci Olympus Camedia Photoalbum)
  • proprietální databáze Olympus Camedia Photoalbum postavená na MS ACCESS 1997, bez rozumné možnosti rozšíření
  • nesnadná správa stránek v prostředí MS – ASP (VB SCRIPT)
  • závislost na datumu souboru – musel se dodržovat speciální postup jinak se o datum přišlo, případně se musel synchronizovat s EXIF informacemi

Co čekám od nového alba:
Vyditelné:

  • rozumné rozhraní odpovídající době
  • možnost větší interakce se stránkami – TOPování fotek, komentáře, TAGy, …
  • větší propojení stránek oddílu (www, stream, fotoalbum)

Neviditelné:

  • sjednocení platforem webů
  • moderní programátorské prostředí (NETTE, jQuery)
  • lepší správa, menší závislost na proprietálním prostředí

Požadavky na nové fotoalbum:
Vyditelné:

  • rychlost
  • přístupnost
  • maximální orientace na prohlížení fotek – je to fotoalbum že!

Neviditelné:

  • minimální závislost na databázi
  • maximální jednoduchost nahrávání fotek

Pro představu uvedu postupy nahrávání fotek:
Postaru:

  • stáhnout fotky v něčem co nemění datumy souborů
  • protřídit
  • zmenšit v něčem co nemění datumy souborů
  • zabalit do ZIP archívu – aby při nahrávání na FTP se nezměnily datumy souborů
  • nahrát archiv na FTP
  • přes vzdálenou plochu na serveru v aplikaci Olympus Camedia Photoalbum vytvořit složku – od otevření aplikace je fotoalbum na webu nedostupné, jelikož si aplikace drží výhradní přístup
  • najít na disku vytvořenou složku (stromová struktura má formát: /ALBUM/001/001/038/019/)
  • nakopírovat fotky z archivu
  • otevřít vytvořené album ve Olympus Camedia Photoalbum – dojde k pomalému vytvoření náhledu a indexaci fotek
  • dříve se ještě v rámci speciální aplikace řešily publikované fotky, to již před časem odpadlo…
  • velké originály popsat a zazálohovat
  • Nově:

    • stáhnout fotky
    • protřídit
    • nahrát na FTP do pojmenované složky (tady ještě uvažuji o použití SAMBA protokolu a ukládání fotek na Windows share, který je pro běžné uživatele dostupnější)

    Úspora kroků je zřejmá!

    Na co se můžete těšit v dalších kapitolách?

    • zpracování fotek – od nahrání až po prezentaci
    • prezentace fotek na celou obrazovku prohlížeče – kdo zná flickr ví o čem píšu

    Migrace aplikace z NETTE 0.9x na NETTE 2.0x

    Potřeboval jsem v jedné starší aplikaci použít dynamické snippety. Po tom co jsem delší dobu hledal jak toto vyřešit ve starém NETTE 0.96, tak jsem na tuto myšlenku nakonec rezignoval a rozhodl jsem se přejít na aktuální verzi NETTE 2.0.6. I když ze začátku jsem se toho bál, tak nakonec to nebylo tak hrozné…

    Co jsem tedy měnil…

    bootstrap.php – ten doznal asi největších změn, ale jedná se spíše jen o přepis než o změnu logiky
    OLD:

    < ?php
    require LIBS_DIR . '/Nette/loader.php';
    require LIBS_DIR . '/dibi/dibi.php';
    require LIBS_DIR . '/currency.php';
    require APP_DIR  . '/models/MyAuthenticator.php';
    require APP_DIR  . '/models/FrontAuthenticator.php';
     
    date_default_timezone_set('Europe/Prague');
     
    //$mode = Debug::DEVELOPMENT;
    $mode = Debug::PRODUCTION;
    Debug::enable($mode, NULL, 'mail@na-me.cz');
    Debug::$strictMode = TRUE;
     
    $loader = new RobotLoader();
    $loader->addDirectory(APP_DIR);
    $loader->register();
     
    // nastavime dibi
    dibi::connect(array(
    	'driver'   => 'mysql',
    	'host'     => 'localhost',
    	'username' => 'user',
    	'password' => 'pass',
    	'database' => 'db',
    	'charset'  => 'utf8',
     ));
     
    $application = Environment::getApplication();
    $application->errorPresenter = 'Error';
    $application->catchExceptions = TRUE;
     
    $router = $application->getRouter();
     
    $router[] = new Route('index.php', array(
    	'presenter' => 'Default',
    	'action' => 'default',
    ), Route::ONE_WAY);
     
    $router[] = new Route('/', array(
        'presenter' => 'Default',
        'action' => 'default',
    ));
     
    $router[] = new Route('admin/<action>/', array(
        'presenter' => 'Admin',
        'action' => 'objednavky',
    ));
     
    // další routy
     
    FormContainer::extensionMethod('FormContainer::addCheckboxList', array('CheckboxList', 'addCheckboxList'));
     
    $application->run();

    NEW:

    < ?php
    require LIBS_DIR . '/Nette/loader.php';
    require LIBS_DIR . '/dibi/dibi.php';
    require LIBS_DIR . '/currency.php';
    require APP_DIR  . '/models/MyAuthenticator.php';
    require APP_DIR  . '/models/FrontAuthenticator.php';
     
    date_default_timezone_set('Europe/Prague');
     
    $configurator = new Configurator;
     
    $configurator->setDebugMode(TRUE);
    $configurator->enableDebugger(dirname(__FILE__) . '/log');
     
    // Enable RobotLoader - this will load all classes automatically
    $configurator->setTempDirectory(dirname(__FILE__) . '/temp');
    $configurator->createRobotLoader()
    	->addDirectory(APP_DIR)
    	->addDirectory(LIBS_DIR)
    	->register();
     
    // Create Dependency Injection container from config.neon file
    //$configurator->addConfig(dirname(__FILE__) . '/config/config.neon');
     
    $container = $configurator->createContainer();
     
    // Setup router
    $container->router[] = new Route('index.php', array(
    	'presenter' => 'Default',
    	'action' => 'default',
    ), Route::ONE_WAY);
     
    $container->router[] = new Route('/', array(
        'presenter' => 'Default',
        'action' => 'default',
    ));
     
    $container->router[] = new Route('admin/<action>/', array(
        'presenter' => 'Admin',
        'action' => 'objednavky',
    ));
     
    // další routy
     
    // nastavime dibi
    dibi::connect(array(
    	'driver'   => 'mysql',
    	'host'     => 'localhost',
    	'username' => 'user',
    	'password' => 'pass',
    	'database' => 'db',
    	'charset'  => 'utf8',
    	'profiler' => TRUE
     ));
     
    FormContainer::extensionMethod('FormContainer::addCheckboxList', array('CheckboxList', 'addCheckboxList'));
     
    $container->application->run();

    Největším oříškem pro mě bylo rozchodit LATTE šablony. Ne že by to bylo v novém NETTE složité ale …
    BasePresenter.php
    OLD

    	// v 0.96 si musíme použití šablony zajistit registrací tohoto filtru
    	public function templatePrepareFilters($template) {
    		$template->registerFilter(new CurlyBracketsFilter);
    	}

    NEW

    	// v novém NETTE jsem myslel, že bude stačit
    	// zakomentovat jeden řádek ($template->register...),
    	// ale to nefungovalo a několik minut jsem nemohl přijít na to proč.
    	// Nakonec pomohlo zakomentovat celou tuto funkci.
    	/*public function templatePrepareFilters($template) {
    		$template->registerFilter(new CurlyBracketsFilter);
    	}*/

    Další změny jsou spíše drobnosti a řešil jsem je jak jsem na ně přicházel:
    $form->addFile() -> $form->addUpload
    User::INACTIVITY -> IUserStorage::INACTIVITY
    $user->setAuthenticationHandler(new MyAuthenticator); -> $user->setAuthenticator(new MyAuthenticator);
    $form->onSubmit[] -> $form->onSuccess[]
    skipFirst() -> setPrompt()
    třída String -> Strings (například Strings::webalize())

    Takže nakonec žádné terno… :)