Archiv rubriky: php

Jak například využít API COINBASE PRO

Co je to vlastně to API?
Jednoduše řečeno jedná se o způsob jakým vykonávat (v případě Coinbase) operace nad portfoliem vzdáleně z prostředí své aplikace, případně svého scriptu.

Co konkrétně můžu dělat?
Vše co se dá dělat je zdokumentováno, ale abych vás čtenáře takto neodbyl, tak příkladem jsou metody (funkce) pro získání přehledů produktů, za kolik se aktuálně obchodují, data k vykreslení candle grafů, správa objednávek a mnoho dalších.

Co konkrétně používám?
Na API jsem se napojil, abych si v budoucnu mohl vytvořit (naprogramovat) svoji investiční strategii a tu automatizovat. Protože, jak jsem dříve psal, je třeba sledovat výkyvy ceny, ty analyzovat a na základě toho nakupovat a prodávat. A když to nějak svedu já, tak v případě dostatku dat a možnosti algoritmizace, může počítač vyhodnocovat situaci například co pět sekund včetně noci a ne jako já jen několikrát denně. Ale aktuálně je to velké sousto, protože jsem se nad tím zamyslel a uvědomil si s čím vším podvědomě pracuji a jak na to nahlížím. A to co se dá nazvat intuicí by bylo potřeba nahradit velkým množstvím matematiky, kterou bych si musel osvěžit, případně nastudovat (o tom snad jindy :-))…
Takže jsem si aktuálně zvolil menší konkrétní cíl, který mi ale usnadní usnadnit jeden konkrétní případ užití (anglicky „Use case“, takže budu používat jen zkratku UC)…

Můj případ užití
Pokud vidíte v grafu několik opakujících se vlnek, které mají maximum někde kolem 1,3 EUR (pro kryptoměnu Ox (ZRX)) a minimum klesá někde k hodnotě 1,2 EUR, tak je fajn si nastavit nákup v režimu LIMIT a nemuset to stále sledovat. Můj návyk je, že si určuji, jaký chci mít náklad na jeden obchod, abych diverzifikoval riziko a následně si určím jaký chci mít výnos při prodeji. Takže například chci nakoupit přesně za 500 EUR a následně nastavit prodej na 510 EUR, zde již po odečtení FEE, takže čistý výnos 10 EUR. O tom jsem více psal v dříve publikovaném článku. Problém je ten, že v režimu LIMIT se takto naplánovat transakce nedá. Jako vstupní pole jsou „jednotková cena za kryptoměnu“ a „počet jednotek, které chci koupit“. No a nyní se dostávám k té pointě, že si tento jednoduchý formulář mohu přetvořit k obrazu svému a na pozadí si dopočítat počet kupovaných jednotek tak, abych se dostal na těch kýžených 500 EUR.

Objednávkový formulkář v režimu LIMIT
Objednávkový formulář v režimu LIMIT – zadána cena 1,2 EUR/ks a počet 400 ks, ale cena je 481,68 EUR. A takto bych musel iterovat nebo na kalkulačce počítat tak dlouho, dokud bych se nedostal na 500 EUR.

Můj formulář je ale přesně takový, jaký vyhovuje mému UC.
Vlastní formulář
Vlastní formulář, kde zadám přesně co chci a na pozadí se mi provede výpočet a založí daná objednávka.

Co je na pozadí?
Základem je jednoduchá třída pro komunikaci s Coinbase API:

<?php
 
class CoinbaseExchange {
	protected $key;
	protected $secret;
	protected $passphrase;
 
	protected $endpoint = 'https://api.pro.coinbase.com';
	protected $timestamp;
 
	public function __construct($key, $secret, $passphrase) {
		$this->key = $key;
		$this->secret = $secret;
		$this->passphrase = $passphrase;
		$this->timestamp = time();
	}
 
	private function signature($request='', $body='', $method='GET') {
		$body = is_array($body) ? json_encode($body) : $body;
		$what = $this->timestamp.$method.$request.$body;
 
		return base64_encode(hash_hmac("sha256", $what, base64_decode($this->secret), true));
	}
 
	public function sendRequest($request, $body='', $method='GET', $auth=false) {
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $this->endpoint.$request);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
		if (strcasecmp($method, 'POST') === 0) {
			curl_setopt($ch, CURLOPT_POST, 1);
			curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
		}
		curl_setopt($ch, CURLOPT_USERAGENT, $_SERVER['HTTP_USER_AGENT']);
 
		if ($auth) {
			$headers = array();
			$headers[] = "Content-Type: application/json";
			$headers[] = "CB-ACCESS-KEY: " . $this->key;
			$headers[] = "CB-ACCESS-SIGN: " . $this->signature($request, $body, $method);
			$headers[] = "CB-ACCESS-TIMESTAMP: " . $this->timestamp;
			$headers[] = "CB-ACCESS-PASSPHRASE: " . $this->passphrase;
			curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
		}
 
		$output = curl_exec($ch);
 
		if(curl_getinfo($ch, CURLINFO_HTTP_CODE) !== 200) {
			return $output;
		}
 
		curl_close($ch);
		return $output;
	}
}
?>

A pak implementace volání konkrétního requestu, dle dokumentace:

$cb = new CoinbaseExchange(KEY, SECRET, PASSPHRASE); // získáte při registraci
 
$totalPrice = (int)$_POST['totalPrice'];
$fee = 0.0035;
$price = (double)$_POST['price'];
$size = round(($totalPrice - ($totalPrice * $fee)) / $price, 2);
 
$request = '/orders';
$body = array(
    "size" => "$size",
    "price" => "$price",
    "side" => "buy",
    "product_id" => "ZRX-EUR"
);
 
$response = $cb->sendRequest($request, $body, 'POST', true);
 
echo $response; // json message, dá se dále zpracovat

Záměrně jsem kód hodně zjednodušil, aby z něho bylo patrné to podstatné. Například $fee, se dá také načítat aktuálně pomocí API, ale další řádky by zápis jen učinily nepřehledným.

Ta kouzelná formule, kterou „aplikace“ dělá za mě je tento řádek,

$size = round(($totalPrice - ($totalPrice * $fee)) / $price, 2);

kde vycházím z cílové investice např. 500 EUR ($totalPrice), odečtu fee a ze zbytku zjistím, kolik mohu koupit jednotek.

Doufám, že jsem vysvětlil význam API a nastínil základ pro implementaci pro službu COINBASE PRO.

Jako obvykle, kdyby byl jakýkoliv dotaz nebo připomínka, tak komentáře jsou vám k dispozici.

Operátor NEBO v PHP a jeho specifické použití

Výchozí situace:
Mám cyklus, kde volám funkci, která mi vrací TRUE nebo FALSE a potřebuji vědět, zda alespoň jeden průběh vrátil TRUE.

Možná to pro PHP experty nebude žádná novinka, ale pro mě jsem přišel sice na odhadnutelnou, ale i tak zajímavou konstrukci, kterou možná všichni neznáte.

$state = false;
foreach ($array as $value) {
	$state |= setParam($value); // operátor |=, funkce setParam vrací true|false
}

Není to pěkné?

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

    Jak NETTE šetří práci

    Občas potřebuji udělat jen drbku na webu, který na NETTE neběží a pak nastane otázka, zda to dělat ještě v pure PHP a nebo nějak do toho komponovat NETTE. U mě je odpověď jasná. NETTE!

    Zadání:
    Přidat na stávající web formulář pro odeslání mailů.

    Řešení:
    Kdekoliv je libo (někdo plácá HTML s PHP, někdo typy obsahu dělí mezi šablonu a další soubory, …) si vygenerujeme formulář s plnou podporou NETTE.

    include_once $_SERVER['DOCUMENT_ROOT'] . '/_libs/nette.min.php';
     
    use	Nette\Mail\Message,
    	Nette\Forms\Form;
     
    // vytvoření formuláře		
    $form = new Form;
    $form->addText('jmeno', 'Jméno a příjmení *:')
    	->setRequired('Zadejte prosím jméno');
    $form->addText('mail', 'E-mail:');
    $form->addText('tel', 'Telefon:')
    	->addConditionOn($form['mail'], Form::EQUAL, '')
    		->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů');
     
    $form['mail']->addConditionOn($form['tel'], Form::EQUAL, '')
    	->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů')
    	->addRule(Form::EMAIL, 'E-mail nemá správný formát');
     
    $form->addSelect('zajem', 'Mám zájem o:', array('sádrokartony, rekonstrukce' => 'sádrokartony, rekonstrukce', 'malby, nátěry' => 'malby, nátěry', 'ostatní' => 'ostatní'));
    $form->addTextArea('pozn', 'Poznámka:', 40, 4);
     
    // antispam
    // podrobněji toto řešení rozebírám v jiném článku
    $c1 = date('j')+3;
    $c2 = date('N')+2;
    $s = $c1 + $c2;
    $form->addText('soucet', sprintf('Zadejte součet %s+%s *:', $c1, $c2))
    	->setRequired('Je třeba zadat součet jako ochranu proti SPAMu')
    	->addRule(Form::EQUAL, 'Je třeba zadat součet jako ochranu proti SPAMu', $s);
    $form->addHidden('c1', $c1);
    $form->addHidden('c2', $c2);
    $form->addSubmit('send', 'Odeslat');

    Pak kdekoliv chci formulář zobrazit tak napíši jen:

    echo $form;

    Pro JS validaci (není nutná) stačí nahrát a nalinkovat soubor netteForm.js:

    <script type="text/javascript" src="../_js/netteForms.js"></script>
     
    <script type="text/javascript">
    	// řešení pro antispam popisuji jinde
    	document.getElementById("frm-soucet").value = parseInt(document.getElementById("frm-c1").value) + parseInt(document.getElementById("frm-c2").value);
    	document.getElementById("frm-soucet").parentNode.parentNode.style.display = "none";
    </script>

    Zpracování formuláře a odeslání ho na mail jde stejně jednoduše:

    if ($form->isSuccess()) {
    	$val = $form->getValues();
    	$mail = new Message;
     
    	$from = ($val['mail'] == '') ? 'Z webu <web @nejaky-web.cz>' : $val['jmeno'] . ' < ' . $val['mail'] . '>';
    	$mail->setFrom($from)
    		->addTo('Info <info @nejaky-web.cz>')
    		->setSubject('Poptávka z webu')
    		->setBody(sprintf("Dobrý den,\nJméno: %s\nE-mail: %s\nTelefon: %s\nZájem o: %s\nPoznámka:\n%s", $val['jmeno'], $val['mail'], $val['tel'], $val['zajem'], $val['pozn']))
    		->send();
    }</info></web>

    Jak dlouho byste toto dělali v čistém PHP?

    Jak neotravovat uživatele s antispamem? – realizace s podporou NETTE

    V článku jsem nastínil, jak chránit webový formulář antispamem bez zbytečné interakce uživatelů.

    Jak to ale propojit s NETTE a jeho generací formulářů.

    Příklad je napsán bez použití kompletního frameworku. Používám NETTE jen jako knihovnu.

    PHP:

    include_once $_SERVER['DOCUMENT_ROOT'] . '/_libs/nette.min.php';
     
    use	Nette\Mail\Message,
    	Nette\Forms\Form;
     
    // vytvoření formuláře		
    $form = new Form;
    $form->addText('jmeno', 'Jméno a příjmení *:')
    	->setRequired('Zadejte prosím jméno');
    $form->addText('mail', 'E-mail:');
    $form->addText('tel', 'Telefon:')
    	->addConditionOn($form['mail'], Form::EQUAL, '')
    		->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů');
     
    $form['mail']->addConditionOn($form['tel'], Form::EQUAL, '')
    	->addRule(Form::FILLED, 'Zadejte prosím alespoň jeden z kontaktů')
    	->addRule(Form::EMAIL, 'E-mail nemá správný formát');
     
    $form->addSelect('zajem', 'Mám zájem o:', array('sádrokartony, rekonstrukce' => 'sádrokartony, rekonstrukce', 'malby, nátěry' => 'malby, nátěry', 'ostatní' => 'ostatní'));
    $form->addTextArea('pozn', 'Poznámka:', 40, 4);
     
    // antispam
    // Nelze řešit přes RAND, jelikož po odeslání by se nám hodnoty změnily, toto beru jako zajímavé "konstanty", které mají platnost jeden den.
    // Nápady jak toto lépe řešit uvítám v diskuzi.
    $c1 = date('j')+3;
    $c2 = date('N')+2;
    $s = $c1 + $c2;
    $form->addText('soucet', sprintf('Zadejte součet %s+%s *:', $c1, $c2))
    	->setRequired('Je třeba zadat součet jako ochranu proti SPAMu')
    	->addRule(Form::EQUAL, 'Je třeba zadat součet jako ochranu proti SPAMu', $s);
    $form->addHidden('c1', $c1);
    $form->addHidden('c2', $c2);
    $form->addSubmit('send', 'Odeslat');
     
    // zpracování formuláře po odeslání
    if ($form->isSuccess()) {
    	$val = $form->getValues();
    	$mail = new Message;
     
    	$from = ($val['mail'] == '') ? 'Z webu <web @nejaky-web.cz>' : $val['jmeno'] . ' < ' . $val['mail'] . '>';
    	$mail->setFrom($from)
    		->addTo('Info <info @nejaky-web.cz>')
    		->setSubject('Poptávka z webu')
    		->setBody(sprintf("Dobrý den,\nJméno: %s\nE-mail: %s\nTelefon: %s\nZájem o: %s\nPoznámka:\n%s", $val['jmeno'], $val['mail'], $val['tel'], $val['zajem'], $val['pozn']))
    		->send();
     
    	// nesouvisí s příkladem - vložíme poděkování
    	include $_SERVER['DOCUMENT_ROOT'] . '/templates/formular-odeslan.php';
    } else {
    	echo $form;
    }

    Vypíchnu raději bokem samotnou podstatu antispamového řešení:

    // antispam
    // Nelze řešit přes RAND, jelikož po odeslání by se nám hodnoty změnily, toto beru jako zajímavé "konstanty", které mají platnost jeden den.
    // Nápady jak toto lépe řešit uvítám v diskuzi.
    $c1 = date('j')+3;
    $c2 = date('N')+2;
    $s = $c1 + $c2;
    $form->addText('soucet', sprintf('Zadejte součet %s+%s *:', $c1, $c2))
    	->setRequired('Je třeba zadat součet jako ochranu proti SPAMu')
    	->addRule(Form::EQUAL, 'Je třeba zadat součet jako ochranu proti SPAMu', $s);
    $form->addHidden('c1', $c1);
    $form->addHidden('c2', $c2)

    Obslužný JS, který nám v případě zapnutého JS skryje antispamový prvek

    <script type="text/javascript">
    	document.getElementById("frm-soucet").value = parseInt(document.getElementById("frm-c1").value) + parseInt(document.getElementById("frm-c2").value);
    	document.getElementById("frm-soucet").parentNode.parentNode.style.display = "none"; // musíme se dostat až ke značce TR
    </script>

    Jak na multiselect v NETTE + dibi

    Potřeboval jsem použít multiselectu v e-shopu, který právě dělám, pro určení souvisejícího zboží a narazil jsem na pár zádrhelů, které zde popíši.

    1. Jak naplnit multiselect daty, při použití skupin
    Toto má být výsledek
    Multiselect

    Je třeba dodat data ve formátu:

    array (
    "Bombóny" => array (
       15 => "Hašlerky"
       16 => "Bompary"
    ),
    "Lízátka" => array (
       11 => "Chupachups"
    ),
    "Čokolády" => array (
       12 => "Milka"
       17 => "Orion"
    ),
    "Ostatní" => (
       14 => "Špalet"
       18 => "Pendrek"
    )
    )

    Jak tento formát získat záleží zejména na struktuře databáze. Předpokládám, že pokud je „ideální“, tak by to šlo pomocí vhodné asociace (viz příklad níže). Vzhledem k tomu, že moje databáze má jinou strukturu a kýženému výsledku jsem se musel dobrat mnohem složitěji (iterace nad daty, dotahování dalších dat pomocí dalších selectů, rekurze, …), tak následující příklad je jen myšlenka, jak by to mohlo fungovat (chybí tam například výběr IDcek a následující nutné UNSETy).

       $res = dibi::query('SELECT [nazev_menu], [nazev_produktu] FROM [menu] JOIN [zbozi] USING ([kod_menu]) ORDER BY [nazev_menu], [nazev_produktu]');
       $res->fetchAssoc('nazev_menu', '=', 'nazev_produktu');

    Po získání dat stačí již jednoduše zavolat:

        $form->addMultiSelect('items', 'Související produkty:', $items);

    2. Vybrání default hodnot
    NETTE očekává pro formulářový prvek multiselect formát:

        array(id1, id2, id3);

    Ten získáme malým trikem pomocí fetchPairs (příklad mojí konkrétní funkce z modelu):

        public function getRelated($id)
        {
            return $this->connection->select('id_2')
    	                        ->from(self::TABLE_PAGES_RELATED)
    	                        ->where('id_1=', $id)
    	                        ->fetchPairs('id_2', 'id_2'); // tento řádek je ten důležitý
        }

    A následně v presenteru:

    	$model = new RelatedModel;
    	$items = $model->getRelated($id);
     
    	$form = $this->getComponent('itemsForm');
    	$form->setDefaults(array('items'=> $items));

    3. Uložení získaných dat
    NETTE nám z formuláře vrátí data ve formátu:

    "items" => array (
       0 => "15"
       1 => "11"
    )

    ale takto je nelze předhodit SQL dotazu pro INSERT a musím je tedy přetransformovat do tvaru ID=>ID. Což vypadá ve výsledku takto:

    "items" => array (
       array (
          "id_1" => "14",
          "id_2" => "15"
       ),
       array (
          "id_1" => "14",
          "id_2" => "11"
       )
    )

    kde id_1 je id editovaného záznamu a id_2 jsou související id (ze stejné tabulky jako id_1).
    Takový formát se již dá předhodit dibi. Konkrétní funkce z mého modelu vypadá následovně:

        public function setRelated($id, $data)
        {
    	dibi::query('DELETE FROM [pages_related] WHERE [id_1]=%i', $id); // vyčistíme si data pro dané ID
    	dibi::query('INSERT INTO [pages_related] %ex', $data); // uložíme si nové související položky. %ex provede expanzi pole pro multiinsert		
        }

    Zde je i ukázáno jakým způsobem provést multiinsert dat, aby nebylo nutno iterovat nad daty a provádět jednotlivé inserty.

    A nyní už chybí jen kus kódu, který transformuje data na výše popsaný formát. pro přehlednost ukážu celou metodu pro zpracování formulářových dat.

        public function itemsFormSubmitted($form)
        {
    		$data = $form->getValues();
    		$model = new RelatedModel;
     
    		$arr = array();
    		foreach($data['items'] as $key => $value) {
    			$arr[$key]['id_1'] = $data['id'];
    			$arr[$key]['id_2'] = $value;
    		}
    		$model->setRelated($data['id'], $arr);
     
    		$this->flashMessage('Byly aktualizovány související produkty.', 'info');
    		$this->redirect('this');
    	}

    Vše!