Jedno zajímavé video od kolegů mi poskytlo dobrý námět na článek a další porovnání jak se co dá dělat v PHP Jet. Ono video je zde:
https://www.youtube.com/watch?v=fWAbMmr4g2c
Zatím jsem tu předváděl různá hejbláktka, administrace a psal o věcech vesměs dynamických.
Co se takhle vrátit ke staré dobré poctivé webařině, vlastně (téměř) statickému webu , ovšem také velice zajímavému projektu.
Modelová situace:
Poslední roky dělám hlavně ona “hejblátka”, ale pochopitelně s klasickou webařinou mám také své nezanedbatelné zkušenosti. Někdy velice pozitivní, ale i pár nezdarů – i z těch se ale člověk poučí a to asi nejvíc. A ty úspěchy i nezdary se týkají i právě takového typu projektu a celá koncepce PHP Jet nese otisk zkušeností i z toho druhu projektů.
Poznámka: Nechci tvrdit, že klasická webařina je něco méně. Naopak. Je to o skvělé grafice, uživatelském testování, UX a pochopitelně i o frontend vývojařině, ale i dobré technické koncepci. Grafika a frontend development však nejsou mé obory. Ale hodně ty lidi obdivuji a jsme kolegové co dohromady nerozdílně tvoří celek.
Řekl bych to takhle: Geniální designér Jozef Kabaň a jeho tým vytvořili krásné nadčasové auto jako je třeba Superb 3. generace. Ale tým lidí jako je například Martin Hrdlička (nezmiňuji náhodou, on i jeho otec jsou pro mne velice inspirativní osoby) se starají o to aby to jezdilo a fungovalo a to vše dobře.
Já jsem rozhodně spíš ten druhý – technik. Tedy článek bude čistě o tom, aby to “jezdilo”, ale velimi krátce zmíním i kroky návrhu uspořádání webu a jeho designu a o roli jakou v tom PHP Jet může hrát. Ostatně i když nakreslím sotva tak domeček jedním tahem, tak schůzky, kde se řešila grafika, koncepce, uspořádání a to všechno již nespočtu a zastávám názor, že i když je schůzka s klientem o grafice (nebo údajně pouze o grafice), tak na ni má jít i zástupce backenďáků, protože každá informace je důležitá a každá maličkost může vše zásadně ovlivnit.
Tak pomyslný projekt začíná. Vezmeme to po krocích. S tím, že fázi 0 (získání zakázky, příprava prvotních návrhů do výběrka) a fázi 1 (příprava grafických návrhů na základě připomínek klienta) vynechávám.
Díky Jet MVC je možné začít ihned připravovat strukturu webu, layouty a tak dále. Jet sice vůbec není CMS, ale díky jeho koncepci je možné si prostě a jednoduše naklikat stránky do předpokládané struktury.
Pro předchystané stránky je možné rovnou nakódovat layout skripty, připravit CSS, nařezat grafiku. Na PéHáPkaře vlastně není vůbec nutné čekat. Stačí jen základní znalosti Jetu co se dají osvojit velice rychle.
Práci je možné průběžně ukazovat klientovi a reagovat na jeho připomínky. Vůči klientovi je možné prokázat velkou agilitu. Je možné klidně i na schůzce provádět řadu úprav, třeba i měnit strukturu.
Frontenďáci a grafici si mohou dělat to své, frontenďákům stačí naprosto minimální znalosti PHP a Jetu a pochopitelně velké znalosti jejich oboru. Vše co se stránek a struktury aplikace týká je velice jednoduché – je tedy možné soustředit se na to co je důležité.
Na nic se nečeká. Tam kde má být něco dynamického mohou frontenďáci doplnit zatím statické HTML, které bude snadno oživeno až PéHáPkáři budou mít to své hotové, či téměř hotové (což může být také velice rychle).
Druhá část týmu si zatím může naklikat prototypy entit – budoucího editovatelného obsahu a připravit administrační moduly. Tak jak jste měli možnost vidět už mnohokrát.
Vše je samozřejmě možné ihned konzultovat s klientem a věci ladit na plně funkčních prototypech ze kterých se postupně stane přesně to co klient potřebuje.
Tento tým nemusí čekat na tým první. Vývoj může běžet odděleně. Ovšem je pochopitelně nutná koordinace a výměna informací o funkčnosti, kterou požaduje klient. To je jasná věc, komunikace je základ všeho. Ale jeden tým nemusí v mnoha věcech čekat na druhý, ovšem mohou posílat své zástupce na schůzky / online schůzky (dle mých zkušeností je to rozhodně lepší).
Co to obnáší už jsem ukazoval opakovaně.
Ale web má být generován jako statický a distribuovaný na servery rozmístěné různě po naší planetě. Na PéHáPkaře tedy čeká malá výzva …
A teď ta výzva – generování obsahu jako statického HTML, který bude distribuován na další servery. Ovšem Jet má takové projekty usnadňovat. A ani zde to nebude jinak.
Co obnáší tato výzva:
Tak a co s tím? Spoiler alert: Tohle už umí ukázková aplikace, se kterou je PHP Jet distribuován. Stačí tedy velice málo.
Tak v základu je to velice jednoduché. PHP Jet má systém stránek – aby ne, weby jsou poskládány ze stránek (a jejich bází), od času co je web webem. Tedy už z principu vím z čeho se web skládá a jaká je struktura. Mohu klidně napsat toto:
public function generate() : void { foreach($this->base->getLocales() as $locale ) { $this->generatePage( $this->base->getHomepage( $locale ) ); } } public function generatePage( MVC_Page_Interface $page ) : void { // ... generování ... foreach($page->getChildren() as $sub_page) { $this->generatePage( $sub_page ); } }
Tedy seznam stránek je k dispozici už z principu a jakmile do struktury weby přibude nová stránka, není třeba nic řešit. To se pochopitelně týká i rušení stránek a změn struktury. Prostě struktura webu / webové aplikace je známá už z principu. Není tedy třeba řešit – framework to již vyřešil.
Pokud si prohlédnete ukázkovou aplikaci PHP Jet, tak zjistíte jednu dosti zásadní věc (kterou já však – dosti hloupě – nikde nezdůrazňuji). Už tam jsou nějaké články či obrázková galerie.
A nic z toho nepoužívá GET parametry. Vše používá části URL. A ukázková aplikace jasně ukazuje že to je správný směr.
Proč? No proto aby bylo možné řešit takové projekty, jako například ten zde popisovaný.
Nikde to nezdůrazňuji, protože mi to prostě za těch “pár let” co jsem v oboru přišlo jako něco naprosto samozřejmého a zcela přirozeného. Prostě to tak z mnoha dobrých důvodů má být a jeden z těch dobrých důvodů je právě to co řeším zde ve článku.
Prostě Jet toto má už z výroby a v ukázkové aplikaci je jasně demonstrován způsob jak to dělat. A pokud budou moduly našeho pomyslného webu vyvinuté obdobně jako ty ukázkové, tak není co řešit.
Tedy vyřešeno – škrtám.
Fajn. Máme URL stránek ve všemožných mutacích. Máme modul článků (a další moduly, ale články pro teď berme jako příklad) co se řídí pomocí částí URL (cest v URL). Jedna malá výzva tu je: Pro vygenerování celého webu bude nutné tyto části URL znát.
To už bude chtít malinko programování, ale opět s tím výrazně pomůže samotná SW architektura a koncepce PHP Jet a objektově orientované programování.
Jak již víte, tak jednotlivé komponenty stránky generují aplikační moduly. Každý aplikační modul má samozřejmě svůj kontroler (či kontrolery), své view a tak dále. Prostě je to mikroaplikace sama o sobě. Ale ta má i hlavní vstupní třídu, která je mandatorní a která se vždy jmenuje Main.
Tedy víme, že každý modul tuto třídu má – je možné se na to spolehnout. Dále víme, že je možné zjistit jaké aplikační moduly jsou na stránku asociované – prostě “co na stránce běží” a jsou tedy k dispozici instance těchto tříd Main jednotlivých modulů.
V tom případě stačí vytvořit vhodný interface a nechat jej implementovat ty třídy Main těch modulů, které operují s částmi URL a tedy logicky i musí vědět jaké části URL mají pro danou stránku existovat. To rozhraní může být zcela triviální:
namespace JetApplication; use Jet\MVC_Page_Interface; interface PageGenerator_URLProvider { public function getPageURLs( MVC_Page_Interface $page ) : array; }
A například třída JetApplicationModule\Content\Articles\Browser\Main může rozhraní implementovat například takto:
namespace JetApplicationModule\Content\Articles\Browser; use Jet\Application_Module; use Jet\MVC_Page_Interface; use JetApplication\Content_Article_Localized; use JetApplication\PageGenerator_URLProvider; class Main extends Application_Module implements PageGenerator_URLProvider { public function getPageURLs( MVC_Page_Interface $page ): array { $articles = Content_Article_Localized::fetchInstances([ 'locale' => $page->getLocale() ]); $URLs = []; $pg_count = ceil( count($articles) / 20); for( $p=2 ; $p<=$pg_count ; $p++) { $URLs[] = 'page:'.$p.'/'; } foreach($articles as $article) { $URLs[] = $article->getURIFragment(); } return $URLs; } }
(Ano, šlo by to napsat i hezčeni a např. počet článků na stránku stanovit na jednom místě, ale teď jde o ukázku principu.)
Každý aplikační modul si “sám řekne” jaké URL může očekávat.
Je tedy možné vyvíjet další moduly pro další stránky, moduly samozřejmě mít na libovolném množství lokalizací a stránek a tak dále.
Logika vždy náleží k danému modulu. Tedy nenastává syndrom jedné hromady a žádný hard coding.
Vše jasně dané a přímočaré a primitivní.
Kousek kódy budoucího generátoru stránek, který se postará o sběr URL stránek pak může vypadat takto:
$URLs = [ $page->getURL() ]; foreach($page->getContent() as $content) { if( ($module_instance = $content->getModuleInstance()) && ($module_instance instanceof PageGenerator_URLProvider ) ) { foreach($module_instance->getPageURLs($page) as $path ) { $URLs[] = $page->getURL().$path;
} } }
V tento moment už je vlastně jasné, že lze získat seznam všech URL, které budou tvořit web.
A teď z nich udělat HTML. V PHP Jet maličkost:
$router = MVC::getRouter(); $router->resolve( $URL ); $html = $page->render();
A je to. Vlastně ne. Jet umí takové věci jako je zneaktivnění stránek, či celých webů, autorizace a tak dále. Ale je maličkost to ošetřit. Třeba takto:
$router = MVC::getRouter(); $router->resolve( $URL ); $base = $router->getBase(); $locale = $router->getLocale(); $page = $router->getPage(); if( !$base->getIsActive() || !$base->getLocalizedData( $locale )->getIsActive() || !$page->getIsActive() || $page->getIsSecret() ) { return; } $html = $page->render();
Celá třída generátoru, které se předá instance báze a jako závislost nějaký ukládač (ano, zde v této situaci je vhodné DI použít takto) vypadá pak takto:
namespace JetApplication; use Jet\Http_Request; use Jet\MVC; use Jet\MVC_Base_Interface; use Jet\MVC_Page_Interface; class PageGenerator { protected MVC_Base_Interface $base; protected PageGenerator_PageSaver $saver; public function __construct( MVC_Base_Interface $base, PageGenerator_PageSaver $saver ) { $this->base = $base; $this->saver = $saver; } public function generate() : void { foreach($this->base->getLocales() as $locale ) { $this->generatePage( $this->base->getHomepage( $locale ) ); } } public function generatePage( MVC_Page_Interface $page ) : void { $page_URL = $page->getURL(); $URLs = [ $page->getURL() ]; foreach($page->getContent() as $content) { if( ($module_instance = $content->getModuleInstance()) && ($module_instance instanceof PageGenerator_URLProvider ) ) { foreach($module_instance->getPageURLs($page) as $path ) { $URLs[] = $page_URL.$path; } } } foreach($URLs as $URL) { $this->generateURL( $URL ); } foreach($page->getChildren() as $sub_page) { $this->generatePage( $sub_page ); } } public function setupHttpRequest( string $URL ) : void { $parsed_URL = parse_url( $URL ); $_SERVER['HTTP_HOST'] = $parsed_URL['host']; $_SERVER['SERVER_PORT'] = 80; $_SERVER['REQUEST_URI'] = $parsed_URL['path']; $_POST = []; $_GET = []; Http_Request::initialize(); } public function generateURL( string $URL ) : void { $this->setupHttpRequest( $URL ); $router = MVC::getRouter(); $router->resolve( $URL ); $base = $router->getBase(); $locale = $router->getLocale(); $page = $router->getPage(); if( !$base->getIsActive() || !$base->getLocalizedData( $locale )->getIsActive() || !$page->getIsActive() || $page->getIsSecret() ) { return; } $html = $page->render(); $this->saver->save( $URL, $router, $html ); } }
A použití generátoru jednoduché:
$base = Application_Web::getBase(); $generator = new PageGenerator( $base, $saver); $generator->generate();
S tím, že server je instance třídy implementující rozhraní JetApplication\PageGenerator_PageSaver.
Celé jsem to pro vás nachystal na GitHub.
Pochopitelně vše se dá dále vylepšovat. Toto je pouhá ukázka koncepce a demonstrace.
Musím se přiznat, že v ukázkové aplikaci jsem měl chybu. Tak je již opravena a bude součástí vydání 2023.6. V kontroleru článků bylo toto:
$path = MVC::getRouter()->getUrlPath(); $this->router->addAction( 'list' ) ->setResolver( function() use ($path) { $path = MVC::getRouter()->getUrlPath();
To sice funguje, ale neumožnilo by to realizovat generátor, protože hodnota $path by v resolverech stále odpovídala stavu při vytvoření instance routeru kontroleru. Tedy resolvery akcí musí vždy reagovat na aktuální stav. Tedy takto:
$this->router->addAction( 'list' ) ->setResolver( function() { $path = MVC::getRouter()->getUrlPath();
Nástroj pro generování je koncipován jako CLI skript. Je tedy možné jej například spouštět periodicky (jednoduché řešení) třeba každé 2 minuty (dle rozsahu webu, generování však může být velice rychlé), nebo na vyžádání.
Skript by pochopitelně bylo nutné obohatit o zpracování chyb. Buď zachytit výjimky, nebo ještě lépe udělat si pro daný účel vlastní ErrorHandler, který by aktivně upozorňoval (mail, či jinou formou) na případné problémy s generováním obsahu.
Ale když už je obsah vygenerován, tak co s ním. Ukázkový skript uloží HTML do adresáře (např)
~/application/data/generated_html/web/cs_CZ/
či
~/application/data/generated_html/web/sk_SK/
a tak dále. Tam bude HTML. Dále v adresáři ~/images by měly být soustředěny veškeré obrázky (i ty nahrané přes administraci) a v adresářích ~/css/packages a ~/js/packages/ pak vygenerované CSS a JS balíčky (i o to se Jet umí postarat, ale o tom třeba jindy).
Tedy teď už stačí pouze tyto soubory dostat na cílové servery.
Možností je řada. Ale nabízí se třeba nástroj rsync + SSH. To se mi v praxi velice osvědčilo. Ale zde už se dostávám mimo PHP k jinému tématu.
Je také otázka co budou ony servery v jednotlivých zemích. Nabízí se vše možné. Záleží na tom jaké to budou země a tak dále. Ale dobrý VMS se dá sehnat vždy a všude a za pár drobných.
Tedy pronajmout si VMS a použít rsync berte jako možný námět na jednoduché a univerzální řešení.
Pochopitelně to není řešení jediné. Ale je to řešení, které umožňuje rychlou reakční dobu.
Kolega ve videu z úvodu článku zmiňuje situaci, kdy klient publikoval co nechtěl. Dané řešení může umožňovat velice rychlou reakci. Klient si obsah spravuje sám, ví jak dlouho trvá aktualizace a ví, že ani nemá cenu hledat v telefonu číslo, protože za pár minut je problém vyřešen.
Pochopitelně firma bude chtít od webu ideálně nějaké konverze. Není problém. Na subdoménách api.firma.* mohou běžet například jednoduchá REST API. To je v PHP Jet také hračka. Ale k tomu se dostanu někdy v budoucnu. Zatím viz ukázková aplikace, kde je jak REST server, tak klient pro testování.
Například u odeslání kontaktního formuláře by nemusela vadit drobná latence spojení daná případnou geografickou vzdáleností. Tedy instance REST API by mohly běžet na stejném místě jako administrační systém. Lze i předpokládat, že právě v administračním systému bude nástroj minimálně na export nasbíraných dat, různé analýzy, statistiky a tak dále.
Dále s tím může pomoci kamarád JavaScript. Takový poptávkový formulář může být JavaScript aplikace operující s REST API. Nic složitého … Zde má JavaScript určité své zasloužené a nezastupitelné místo.
Ale jak jsem říkal – námět, respektive náměty na příště.
Dejme tomu, že je projekt spuštěn. Z CZ centrály si firma vše řídí. Ale za pár let expanduje třeba do oblasti severní Ameriky. Dejme tomu že v USA vznikne další řídící centrála firmy, která bude potřebovat jistou autonomii. Například si budou potřebovat sami spravovat web a plně za to odpovídat.
Maličkost … Projekt stačí naklonovat, na serveru v USA rozběhnout novou instanci administrace a API, přesměrovat DNS, v CZ databázi odstranit severoamerické mutace a vice versa.
Vůbec nic složitého. Vše se odehraje hlavně na úrovni změn konfigurací a “vyčištění” databáze a poměrně běžných operací. Ovšem bez zásadních úprav v logice projektu.
Ano, práce to je, ale opět je to něco s čím celá koncepce počítá.
Dovolím si připomenout, že kompletní generátor máte připravený na GitHub k nahlédnutí.
Pochopitelně celé to lze dále rozvíjet. Tohle berte jako základní myšlenku a ukázku toho co Jet umí.
Celá koncepce ale není pouze teoretická, ale ryze praktická. I když web nemusí zahrnovat distribuci obsahu na jiné servery, tak koncept maximálního možného generování HTML a jeho ukládání používám (v různých formách, ale koncepčně stále totéž) běžně. PHP Jet toto chování totiž předpokládá. Proto umožňuje označovat co je kešovatelné a co ne. Tedy i když nebudete řešit právě takovou situaci jako zde popisuji, tak je možně dělat weby, které budou mít 90 stránek provozovaných de facto z keše a 10 stránek dynamických (s formuláři a tak dále).
No a pokud by jste se do něčeho takového chtěli pustit a chtěli něco prodiskutovat, tak není problém. Napište mi třeba e-mail.
Tak zase za týden či dva (jak mi můj absolutně nejdůležitější projekt v životě dovolí – můj syn) se ozvu s dalším článkem. A na PHP Jet se pochopitelně také neustále pracuje 😉
Mějte se krásně a ať se daří!
Proč by někdo měl ke generování statických stránek používat tohle, když existuje například Hugo?
Co má znamenat
$page_URL = $page->getURL();
$URLs = [ $page->getURL() ];
Metoda napoprvé vrátí string a napodruhé array?
$module_instance = $content->getModuleInstance()
$module_instance instanceof PageGenerator_URLProvider
Generovat modul z obsahu, snad z URL, ne? Proč má být modul instancí providera URL?
Ta metoda vrátí pokaždé string, v druhém případě jej akorát vloží do pole. Jen asi má být místo druhého volání prosté předání $page_URL.
Jinak ten kód je celkově prostě... já nevím... za mne ultra nepřehledný, nechtěl bych to číst ani psát. Každopádně souhlas, že na statické generování HTML už přece máme nástroje.
Jo, asi jsem to napsal nepřesně, ale ta druhá proměnná se jmenuje $URLs, tedy množné číslo .. tedy jakoby se vrátilo víc hodnot.
Je to nepřehledný a hlavně ta logická struktura objektů je hrozná.
Přiznám se že mě dost děsí design těch metod třeba ten resolver
1) metoda resolve mění vnitřní stav objektu místo aby vracela jaký immutable result
2) tím že se k resolveru přistupujeme jako ke globální proměně není problém mít kód ve stylu
$router = MVC::getRouter();
$router->resolve( $URL );
$base = $router->getBase();
someMethodCall();
$base2 = $router->getBase();
No a v metodě someMethodCall mi může kdokoli změnit stav $routeru já se to nedozvíme bude to pro mě wtf, protože v autorově podání je dependency injection jen sada globálních proměnných. Takže se ve výsledku dostanu do stavu že
$base != $base2;
Přitom by stacilo si uvědomit že úkolem routeru je routovat, ne zodpovídat "jaké je aktuální locale".
Tak tak.
Autor je asi dobrý manažer/podnikatel a vytvořil si něco, co mu pomáhá rychle dodávat klientům hodnotu. Na tom samozřejmě není vůbec nic špatnýho.
Ale potom bohužel dostal pocit, že ten jeho slepenec je "framework", který by měl zveřejnit. A nedokáže přijímat kritiku nebo si připustit, že ten jeho výtvor možná není až tak geniální, jak si myslel. Že jeho "design patterny" fungují jenom díky tomu, že se v PHP všechno vytváří znova na každý request a jsou principielně špatně.
Použít tohle na skutečný projekt by mohl snad jen sebevrah.
Přesně tak, autor zapomíná, že pro něj je ten Jet jednoduchý a přehledný, protože jej 10+ let vyvíjí. Stejně, jako on hned nechápe Laravel nebo Nette, tak ostatní nechápou Jet. Ostatně - on skoro každý, kdo dělá v PHP dlouho a aktivně tvoří weby, má nějaký svůj FW nebo alespoň sadu knihoven - včetně mne :). V PHP je toto dané historicky, protože dřív zkrátka téměř nic nebylo... Fakt jej ale tady nebudu prezentovat, protože určitě jsem jej nenavrhl líp než Nette nebo Laravel - to ani při komunitním vývoji nelze.
Přečteno 20 709×
Přečteno 18 548×
Přečteno 17 777×
Přečteno 17 521×
Přečteno 16 219×