Velký test PHP frameworků

Petr Daněk 21. 8. 2008

Snad každá platforma pro vývoj webových aplikací poskytuje rozhraní pro oddělení práce s daty od jejich prezentace. V jazyce PHP je však situace jiná, zde neexistuje žádné oficiální MVC řešení. Naštěstí jsou napsány desítky frameworků, které nám ulehčují práci při psaní aplikace a pracují s návrhovým vzorem MVC. Použití některého z nich umožní plné využití výhod MVC architektury a díky vestavěným funkcím také zajistí méně napsaného kódu a tím pádem rychlejší vývoj aplikací.

Úvod

Na první pohled se může zdát, že frameworků pro PHP je velké množství. Pokud se však na výběr zaměříme detailněji, zjistíme, že ne každý vyhovuje našim představám. Některé frameworky mají nepřeberné množství knihoven, které obstarávají téměř vše, na co si vzpomenete. To je ovšem vykoupeno jejich vyššími hardwarovými nároky a nižší rychlostí. Jiné mají knihoven méně, ale dají se, díky své modulárnosti, lehce rozšířit o nové funkce. Velká pozornost se musí věnovat dokumentaci jednotlivých frameworků. Pokud je dokumentace na špatné úrovni, práce s frameworkem trvá mnohem déle, protože je třeba dlouze vyhledávat parametry funkcí či řešení jiných úskalí.

Nikde na internetu není zpracována studie, která by testovala frameworky dle všech výše uvedených aspektů. Dostupné testy jednotlivých frameworků ukazují pouze rychlosti a paměťové nároky při vypsání fráze „Hello World!“. Takový test se nepodobá reálně používaným aplikacím, protože při něm není využita komunikace s databází. Z tohoto důvodu byly udělány testy nejrozšířenějších PHP frameworků, které se více blíží reálné aplikaci. Cílem testů je vybrat framework, který skutečně ulehčí práci a zároveň jsme pomocí něj schopni vytvořit i robustnější aplikaci v kratším čase než při použití čistého PHP.

Na začátek je ještě nutno zdůraznit, že absolutní hodnoty naměřených výsledků vůči reálnému provozu nejsou až tak důležité. A to z důvodu, že měření neprobíhalo na serverech k tomu určených, ale na noteboocích a rovněž rozdíly mezi naměřenými hodnotami jsou velké. V článku však jde hlavně o relativní srovnání výsledků testů mezi jednotlivými frameworky.

Průběh testování

Pro testování byly založeny dvě MySQL tabulky a to tabulka „members“ a „users“. V tabulce „members“ jsou uvedeni imaginární členové sdružení. Tito členové mohou mít pod sebou další uživatele, kteří jsou uloženi v tabulce „users“, kde sloupec „id_member“ poukazuje na člena, ke kterému daný uživatel patří.

Obr. 1. Schéma testovacích tabulek
frameworky1

Testování probíhalo na dvou počítačích. Na prvním z nich běžel testovací program Apache JMeter a na druhém počítači byly umístěny samotné testované stránky. Rozdělení na dva počítače bylo nutné z důvodu oddělení stránek a testovacího programu, který měl vyšší nároky, čímž výrazně ovlivňoval výsledky testů. U použitých počítačů se v obou případech jednalo o notebook Fujitsu Siemens AMILO Pro V2040 v konfiguraci:

  • Procesor: 1.6 GHz Pentium (M725) (Intel Pentium M Dothan (2 MB cache))
  • Paměť: 512 MB (PC4300 SO-DIMM 200pin (DDR2–533))
  • HDD: 60 GB, 5400 otáček

Počítače byly propojeny místní sítí 100 Mbit/s. Program JMeter běžel pod operačním systémem Windows XP Home edition. Testované stránky běžely pod operačním systémem Linux Kubuntu 7.10 s konfigurací:

  • PHP 5.2.3–1
  • MySQL 5.0.45
  • eAccelerator v0.9.5.2

Na každém frameworku byly provedeny testy jak s vypnutým, tak i zapnutým eAcceleratorem. Všechny testy byly prováděny třikrát. Pro následné srovnání byly použity průměrné hodnoty z těchto tří testů.

Jednotlivé kroky testu

Při testování se nejprve vypsala tabulka se všemi členy. U každého člena byly vypsány základní informace, jako je jméno, adresa a typ členství. Výpis členů můžeme vidět na obrázku Obr. 2, kde je rovněž vidět odkaz na uživatele daného člena, pomocí kterého se dostáváme do dalšího kroku.

Obr. 2. První krok testu – členové sdružení
frameworky2

V dalším kroku byl proveden výběr uživatelů patřících k vybranému členovi. Ke každému uživateli jsou opět vypsány základní informace. V tomto kroku bylo rovněž zaznamenávána paměťová náročnost frameworků (pomocí PHP funkce memory_get_usage). Kompletní tabulku z kroku dva můžete vidět na níže uvedeném obrázku.

Obr. 3. Druhý krok testu – výpis uživatelů vybraného člena
frameworky3

V třetím kroku se editují data uživatele. Tím otestujeme rychlost i při UPDATE dotazu. Formulář s údaji je zpracován kontrolerem, který při úspěšné editaci přesměruje zpět na výpis uživatelů a vypíše informační text.

V každém kroku je navíc prováděn další MySQL dotaz, který vybírá jméno člena, jehož uživatele aktuálně zobrazujeme. Takto získané jméno je použito ve zpětných odkazech.

Obr. 4. Třetí krok testu – editace uživatele
frameworky4

Dále jsme se pokusili přibližně změřit čas, který jednotlivé frameworky potřebují k získání dat z databáze. To jsme realizovali vložením měřící funkce těsně před funkci obstarávající práci s databází a za ní. Rozdílem těchto dvou časů jsme získali hrubý čas potřebný pro komunikaci se samotnou databází. Tento test byl prováděn samostatně po dokončení všech testů, tak, aby neovlivnil celkové výsledky.

Realizace v programu Apache JMeter

Testovací plán v JMeteru se skládal z výše uvedených kroků, včetně odeslání POST dotazu pro změnu údajů uživatele. Testovací schéma je zobrazeno na obrázku Obr. 5. Ramp-up perioda, což je doba, za kterou dojde k vytvoření zadaného počtu uživatelů, byla nastavena na 10 vteřin. Počet uživatelů, kteří přistupují paralelně k aplikaci, byl nastaven na 10 (což při Ramp-up periodě 10 znamená, že se každou vteřinu připojil nový uživatel). Všichni uživatelé vykonají zadané kroky třikrát, přičemž v update dotazu mění každý z nich jméno uživatele na tvar „číslo opakování-číslo threadu“ (tedy například „2–10“). Z toho vyplývá, že pro testovaný framework dostaneme 30 zobrazení jednotlivých kroků.

Při testování byly zaznamenávány především délky trvání jednotlivých akcí, které jsou vyneseny do grafů ukazujících rychlost testovaného frameworku.

Obr. 5. Testovací schéma
frameworky5

Při jednom testu tedy dostáváme 120 vzorků (30× zobrazení členů, 30× zobrazení uživatelů, 30× editační formulář a 30× samotný update dotaz), které ukazují rychlost daného frameworku.

Nyní bude následovat přehled a výsledky jednotlivých frameworků a na konci jejich vzájemné porovnání. Ještě předtím, než budete pokračovat, musím zdůraznit, že nejsme odborníci na testované frameworky. Každý z nich jsme poznávali prakticky od začátku a aplikace jsme tvořili pomocí dostupné dokumentace a tutoriálů. U všech testovaných frameworků nebylo zapínáno cachování a byla vesměs použita základní konfigurace.

Akelos

Tento MVC framework je k dispozici na stránce http://www.ake­los.org/, kde jej naleznete ve verzi 0.8. Akelos je určen jak pro PHP4 tak PHP5. Přebírá některé funkce ze známého frameworku Ruby on Rais určeného pro programovací jazyk Ruby.

  • Pro přístup k databázi používá Active Record, což je druh objektově-relačního mapování. Podporuje databáze MySQL, PostgreSQL (7.4+) a SQLite.
  • Obsahuje několik základních pluginů jako je kalendář, asistent pro vytváření základního administračního rozhraní, či dynamic_finder, který umožňuje magické vyhledávání v Active Record modelech ($Model→find_by_u­ser_and_name(‚Joh­n‘, ‚Smith‘)). Lze jej dále rozšiřovat přidáváním nových pluginů.
  • Pro pohledy používá HTML v kombinaci se Syntags, což je použití zástupných textů pro určité PHP příkazy, čímž se šablony zpřehledňují. Šablony jsou rozděleny dle názvů kontrolerů do adresářů, kde název souboru odpovídá názvu metody a mají příponu .tlp (například uzivatele/edit­.tlp).
  • Chybí pokročilejší validace. Obsahuje pouze validační funkci pro požadované položky, pokud však chceme zajistit minimální velikost nějaké položky jsme nuceni použít rozhodovací příkaz IF.
  • V každém kontroleru definujeme modely, se kterými je svázán. Poté můžeme využívat jejich funkcí.
  • Obsahuje standardní helpery pro práci s formuláři, url, datem a časem, navíc má také JavaScript helpery.
  • Jednoduchá konfigurace za pomoci grafického rozhraní.
  • Možnost vygenerování všech modelů a kontrolerů. Stačí nadefinovat databázi a spustit generátor, který vygeneruje všechny potřebné soubory včetně pohledů.

Dokumentace a uživatelská podpora

Na stránkách se nalézá pouze API dokumentace, ale nikde nenalezneme dokumentaci, která by popisovala jednotlivé helpery a ostatní funkce. Jediným místem kde je k nalezení popis funkcí frameworku je uživatelská wiki, kterou vytváří komunita okolo Akelosu. Nicméně i tato wiki je dosti nepřehledná.

Na stránkách nalezneme video tutoriál s názvem „Vytvoření blogu ve dvaceti minutách“. Tento je dobře zpracován a ukazuje i samotnou instalaci Akelosu.

Komunita zde má své fórum a emailovou konferenci, ale postrádám zde oficiální IRC kanál (existuje několik IRC kanálů, nejsou ovšem uvedeny na oficiálních stránkách).

Ukázka zdrojových kódů

Ukázka práce s pohledem – použití Syntags:

1  {loop users}
2    <tr {?user_odd_position}class="odd"{end}>
3    {loop content_columns}
4      <td class="field"><?php  echo  $user->get($content_column) ?></td>
5    {end}
6      <td class="operation"><?php  echo  $uziv_helper->link_to_show($user)?></td>
7    </tr>
8  {end}

Ukázka kontroleru – funkce obstarávající vykreslení editačního formuláře a následné uložení dat po odeslání formuláře:

9   function edit()
10  {
11    if(!empty($this->params['id'])){
12         if(empty($this->user->id) || $this->user->id != $this->params['id']){
13             $this->user =& $this-> user->find($this->params['id']);
14         }
15     }else{
16         $this->redirectToAction('listing');
17     }
18
19     if(!empty($this->params[user])){
20         $this->uzivatele->setAttributes($this->params[user]);
21         if($this->Request->isPost() && $this-> user->save()){
22             $this->flash['notice'] = $this->t('User was successfully updated.');
23             $this->redirectTo(array('action' => 'show', 'id' => $this->user->getId()));
24         }
25     }
26  }

Model je velice jednoduchý, zde je celý jeho obsah:

1  <?php
2  class Users extends ActiveRecord
3  {
4       var $belongs_to = 'member';
5  }
6  ?>

Výsledky testů bez použití eAcceleratoru

Akce Vzorky Průměr [ms] Median [ms] Min [ms] Max [ms] Sm. Odch. [ms] Modus [ms]
První test
Zobrazení členů 30 995 875 187 2219 644,75 187
Zobrazení uživatelů 30 992 734 172 2547 733,93 172
Editace uživatele 30 1637 906 156 17703 3089,56 1781
Upravení dat 30 2006 1937 328 4860 1143,82 344
Celkem 120 1408 1078 156 17703    
Druhý test
Zobrazení členů 30 1037 782 312 2891 694,65 484
Zobrazení uživatelů 30 1038 922 187 2390 661,29 625
Editace uživatele 30 1159 891 250 2656 752,83 453
Upravení dat 30 2056 2078 610 4766 991,33 N/A
Celkem 120 1323 1109 187 4766    
Třetí test
Zobrazení členů 30 1262 1219 203 3093 784,29 1031
Zobrazení uživatelů 30 1137 859 156 3500 816,19 N/A
Editace uživatele 30 953 750 172 2875 672,41 484
Upravení dat 30 1683 1578 656 3531 741,16 1015
Celkem 120 1259 1125 156 3531    

Obr. 6. Průměrné časy zpracovávaných akcí v Akelosu bez eAcceleratoru

frameworky6

Paměťové nároky: 6538,49 kB

Akce Průměrný čas [ms]
Výpis všech členů 17,43
Jméno člena 32,51
Všichni uživatelé člena 6,57
Načtení jednoho uživatele 20,15
Update dotaz 86,33
Celkem 27,78
Rychlosti práce s databází – Akelos

Výsledky testů s použitím eAcceleratoru

Akce Vzorky Průměr [ms] Median [ms] Min [ms] Max [ms] Sm. Odch. [ms] Modus [ms]
První test
Zobrazení členů 30 178 78 46 1937 363,54 78
Zobrazení uživatelů 30 77 62 31 203 44,97 47
Editace uživatele 30 75 63 46 172 36,73 47
Upravení dat 30 158 110 62 391 93,94 94
Celkem 120 122 78 31 1937
Druhý test
Zobrazení členů 30 71 63 46 110 20,22 62
Zobrazení uživatelů 30 55 47 31 110 17,56 47
Editace uživatele 30 48 47 31 79 14,75 47
Upravení dat 30 189 172 93 422 87,53 109
Celkem 120 91 63 31 422
Třetí test
Zobrazení členů 30 78 78 47 140 23,08 78
Zobrazení uživatelů 30 51 47 31 109 15,98 47
Editace uživatele 30 52 47 31 109 17,31 47
Upravení dat 30 183 157 93 453 80,75 125
Celkem 120 91 63 31 453

Obr. 7. Průměrné časy zpracovávaných akcí v Akelosu s eAcceleratorem

frameworky7

Paměťové nároky: 6538,49 kB

Framework:  1228,24 kB
eAccelerator:   4536,32 kB
Celkem: 5764,56 kB

Zhodnocení

Na výše uvedených grafech můžeme sledovat, že nejvíce času zabírá upravení dat uživatele. Bez použití eAcceleratoru jsou časy nad jednu vteřinu, což je neúnosné. Framework má své přednosti v podobě generátoru aplikací avšak jeho hlavním nedostatkem je dokumentace, která je na velmi špatné úrovni. Z tohoto důvodu tento framework nedoporučuji pro další práci.

Příklad webu postaveného na tomto frameworku: http://www.theche­micalbrothers­.com

CakePHP

Framework naleznete na stránce http://www.ca­kephp.org/, kde je ve verzi 1.1.19.6305 . Rovněž zde naleznete novější verzi 1.2.0.6311, která je ovšem ve stavu beta verze. Cake je určen pro PHP 4 i PHP 5. Patří mezi jeden z nejvíce rozšířených PHP frameworků, z toho důvodu je na internetu k dispozici i mnoho českých návodů.

Základní vlastnosti

  • Používá ORM knihovnu. Možnost připojení na databáze MySQL (4+), PostgreSQL, ADOdb a od verze 1.2 také na Firebird DB2, MSSQL, Oracle, SQLite, ODBC.
  • Obsahuje komponenty pro práci s e-maily, autentifikaci a mnoho dalších. Je snadné napsat pro něj nové pluginy. Mnoho pluginů je ke stažení na internetu, například také plugin pro administraci phpGACLu.
  • Pohledy jsou rozděleny na několik kategorií – celkový design, samostatná stránka a elementy, které lze chápat jako části stránek. Vše je přehledně rozděleno a není tak problém změnit vzhled celé stránky v několika minutách. Soubory pohledů mají koncovku ctp a obsahují kombinaci HTML a PHP.
  • Velmi dobře zpracovaná validace, která se provádí v modelu. Možno určit pravidla, co se může či nesmí vyskytovat, určit omezení délky a v případě nedodržení pravidel definovat chybovou hlášku.
  • V modelu můžeme definovat callbacky, díky kterým rozšíříme možnosti práce s daty. Callbacky jsou například BeforeSave, AfterDelete a další.
  • Kromě standardních helperů zde můžeme nalézt také helpery pro ajax či RSS.
  • Nastavení se provádí změnou konfiguračních souborů. V php.ini je nutno definovat include_path na složku se systémem, ve které je umístěna systémová část CakePHP.

Dokumentace a uživatelská podpora

CakePHP má několik zdrojů informací. Jedním z nich je výborně zpracovaný API manuál. Dále je k dispozici manuál popisující začátky práce v Caku. Orientace v manuálu chvíli potrvá, ale po větším prozkoumání zde nalezneme téměř vše potřebné. Na oficiálním webu je k dispozici tutoriál „jak vytvořit vlastní blog“.

Jelikož je komunita okolo tohoto projektu velká, můžeme hledat rady také na IRC kanálu či na Google Groups. Na internetu rovněž nalezneme velké množství materiálů v češtině a také hotové projekty, ve kterých můžeme hledat inspiraci.

Ukázka zdrojových kódů

Jak již bylo řečeno výše, pohled se skládá z HTML a PHP, případně je možno použít také Smarty. Níže je zobrazena ukázka pohledu. Jednotlivé proměnné se předávají v kontroleru pomocí příkazu set (například  $this->set('members', $members)).

1   <?php foreach( $members as $member) { ?>
2   <tr>
3       <td><?php echo $member['Member']['ID'] ?></td>
4       <td><?php e($member['Member']['name']) ?></td>
5       <td><?php echo $member['Member']['type'] ?></td>
6       <td><?php echo $member['Member']['town'] ?></td>
7           <td><?php echo $html->link('edit', '/members/edit' . $member['Member']['ID'])." ".$html->link('uživatelé', '/users/index' . $member['Member']['ID']); ?></td>
8   </tr>
9   <?php } ?>

Funkce pro zpracování editovaných údajů – kontroler users:

1    function edit($id_user, $id_member) {
2       if(empty($this->data)) {
3           $this->pageTitle = 'Editace uživatele';
4           $this->set('users', $this->User->find("id=$id_user"));
5           $this->set('id_mem', $id_member);
6           $this->render();
7       } else {
8           $this->cleanUpFields();
9           if($this->User->save($this->data, $id_user)) {
10              $this->Session->setFlash('Uživatel byl úspěšně upraven.');
11              $this->redirect('/users/view/'.$id_member);
12          } else {
13              $this->Session->setFlash('Data se nepodařilo upravit.');
14              $this->redirect('/users/view/'.$id_member);
15          }}}

Ukázka modelu:

1  class User extends AppModel
2  {
3   var $name = 'User';
4
5   function save($data = null, $id) {
6       $this->id = $id;
7        $returnval = parent::save($data, false);
8        return $returnval;
9    }
10 }

Výsledky testů bez použití eAcceleratoru

Akce Vzorky Průměr [ms] Median [ms] Min [ms] Max [ms] Sm. Odch. [ms] Modus [ms]
První test
Zobrazení členů 30 206 172 62 1078 187,29 63
Zobrazení uživatelů 30 173 172 62 390 79,70 78
Editace uživatele 30 198 187 78 625 131,72 94
Upravení dat 30 483 360 141 3265 588,90 156
Celkem 120 265 188 62 3265
Druhý test
Zobrazení členů 30 156 140 62 563 99,19 94
Zobrazení uživatelů 30 165 140 78 359 78,03 110
Editace uživatele 30 170 156 78 359 76,58 109
Upravení dat 30 300 297 141 500 95,98 297
Celkem 120 198 172 62 563
Třetí test
Zobrazení členů 30 125 125 62 250 44,83 125
Zobrazení uživatelů 30 142 125 78 360 67,39 78
Editace uživatele 30 148 141 62 312 56,52 125
Upravení dat 30 318 297 141 828 141,76 188
Celkem 120 183 141 62 828

Obr. 8. Průměrné časy zpracovávaných akcí v CakePHP bez eAcceleratoru

frameworky8

Paměťové nároky: 2867,82 kB

Akce Průměrný čas [ms]
Výsledky testů pro CakePHP – zapnutý eAccelerator 

Výsledky testů s použitím eAcceleratoru

Akce Vzorky Průměr [ms] Median [ms] Min [ms] Max [ms] Sm. Odch. [ms] Modus [ms]
První test
Zobrazení členů 30 47 16 15 766 133,66 31
Zobrazení uživatelů 30 30 16 15 187 31,43 16
Editace uživatele 30 25 16 15 47 11,63 16
Upravení dat 30 58 47 31 141 22,86 47
Celkem 120 40 31 15 766
Druhý test
Zobrazení členů 30 19 16 0 62 12,63 15
Zobrazení uživatelů 30 26 16 15 47 13,55 16
Editace uživatele 30 21 16 15 32 7,66 16
Upravení dat 30 44 47 31 62 7,67 47
Celkem 120 28 31 0 62
Třetí test
Zobrazení členů 30 37 16 15 485 83,49 16
Zobrazení uživatelů 30 24 16 15 47 10,36 31
Editace uživatele 30 18 16 15 47 7,10 16
Upravení dat 30 46 47 31 94 13,55 47
Celkem 120 31 31 15 485

Obr. 9. Průměrné časy zpracovávaných akcí v CakePHP s eAcceleratorem

frameworky9

Paměťové nároky:

Framework:  561,11 kB
eAccelerator:   1996,80 kB
Celkem: 2557,91 kB

Zhodnocení

CakePHP dosahuje velmi dobrých výsledků, jak s použitím eAcceleratoru, tak bez něj. Jeho paměťová náročnost není tak vysoká. Obsahuje velkou softwarovou výbavu a jeví se jako vhodný pro práci na velkých i malých projektech. Jeho nevýhodou je, že jsou vyvíjeny paralelně dvě verze, které nejsou navzájem plně kompatibilní. Z toho důvodu je lepší začít používat novější verzi, která je prozatím ve stádiu beta verze. Většina tutoriálů a dalších pomocných materiálů je však určena pro starší verzi 1.1.

Příklad webu postaveného na tomto frameworku: http://mark-story.com/

Další díl

V dalším díle seriálu se můžete těšit na testy frameworků CodeIgniter, Jelix a Kohana.

Našli jste v článku chybu?
DigiZone.cz: ČTÚ červenec: rušení trochu vzrostlo

ČTÚ červenec: rušení trochu vzrostlo

DigiZone.cz: Vláda schválila digitální vysílání ČRo

Vláda schválila digitální vysílání ČRo

Měšec.cz: Kurzy platebních karet: vyplatí se platit? (TEST)

Kurzy platebních karet: vyplatí se platit? (TEST)

DigiZone.cz: Evropa 2: od září nové vedení

Evropa 2: od září nové vedení

Podnikatel.cz: Česká pošta vycouvala ze služby ČP Cloud

Česká pošta vycouvala ze služby ČP Cloud

Podnikatel.cz: Youtuber? Za 15 tisíc dělat nebude

Youtuber? Za 15 tisíc dělat nebude

Měšec.cz: Udali ho na nelegální software a přišla Policie

Udali ho na nelegální software a přišla Policie

Měšec.cz: Se stavebkem k soudu už (většinou) nemusíte

Se stavebkem k soudu už (většinou) nemusíte

Vitalia.cz: Ženy, které milují příliš, jsou neštěstí

Ženy, které milují příliš, jsou neštěstí

Vitalia.cz: Vakcína Cervarix je oficiálně i pro chlapce

Vakcína Cervarix je oficiálně i pro chlapce

Měšec.cz: Do ostravské MHD bez jízdenky. Stačí karta

Do ostravské MHD bez jízdenky. Stačí karta

Lupa.cz: Sdílíte veřejně běhání a jízdu na kole?

Sdílíte veřejně běhání a jízdu na kole?

Podnikatel.cz: OSA zdražuje poplatky. Zaplatíte o polovinu víc

OSA zdražuje poplatky. Zaplatíte o polovinu víc

Vitalia.cz: 9 potravin, které nesmí chybět v jídelníčku těhotné

9 potravin, které nesmí chybět v jídelníčku těhotné

Lupa.cz: Co vzal čas: internetové kavárny a herny

Co vzal čas: internetové kavárny a herny

Vitalia.cz: Galerie: Strouhanka ze starých rohlíků? Kdepak

Galerie: Strouhanka ze starých rohlíků? Kdepak

DigiZone.cz: E! a zákulisí turné Mariah Carey

E! a zákulisí turné Mariah Carey

DigiZone.cz: Další rána pro piráty: 6 měsíců

Další rána pro piráty: 6 měsíců

Podnikatel.cz: Týká se vás EET? Chtějte od berňáku posudek

Týká se vás EET? Chtějte od berňáku posudek

Vitalia.cz: Je bílý kokos fakt tak úžasný? Ano, je!

Je bílý kokos fakt tak úžasný? Ano, je!