Mezinárodní web s distribucí obsahu snadno a rychle

18. 5. 2023 17:30 Mirek Marek

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/wat­ch?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:

  • Firma s působností po celé planetě, ale s centrálou v ČR a potřebuje nový prezentační web.
  • Web bude primárně designový, ale bude obsahovat nějaké editovatelné články, či editovatelný katalog produktů a případně další editovatelný obsah.
  • Správa bude probíhat centrálně – z centrály firmy v ČR.
  • Pro správu webu bude existovat administrační rozhraní šité na míru potřebám klienta a jeho zaměstnanců – uživatelů.
  • Každá národní mutace webu poběží na vlastní doméně. Tedy: firma.cz, firma.de, firma.com, firma.co.uk a tak dále.
  • Web bude generován jako statické stránky pro zajištění maximální rychlosti (s pozitivním vlivem i na bezpečnost). Na serverech ze kterých bude web reálně provozován bude maximálně Nginx (či podobný webserver) bez PHP. 
  • Pro každou národní mutaci bude zřízen vhodný hosting přímo v dané zemi – lepší dostupnost a funkce webů a rovněž opět i bezpečnost.
  • To vše postaveno na PHP Jet

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.

Příprava prototypu webu – fáze projektu 2a

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).

Vývoje administrace a dynamického obsahu – fáze projektu 2b

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 …

Vývoj generování obsahu a jeho distribuce

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:

  • Získat někde seznam toho co se má generovat.
  • Používat pouze URL bez GET parametrů.
  • Umožnit generovat i editovatelný obsah: např. články, či produktový katalog.
  • “Nějak” to HTML vygenerovat.
  • Vše udělat koncepčně tak, aby web bylo možné rozvíjet a udržovat několik let, možná i déle než někoho napadne při vývoji. Tedy: opravdu čistý kód, mající dobré TCO, jednoduchost, přímočarost a udržitelnost.

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.

Seznam toho co se má generovat

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.

Používat pouze URL bez GET parametrů

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.

Umožnit generovat i editovatelný obsah. Např. články, či produktový katalog.

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\Conten­t\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;
} } }

“Nějak” to HTML vygenerovat.

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.

Pozor na možnou chybu!

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();

Jak obsah distribuovat?

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/genera­ted_html/web/cs_CZ/

či

~/application/data/genera­ted_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.

A co například kontaktní formulář, sběr dat a konverzí obecně?

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ě.

A co když firma poroste?

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á.

Závěr

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ří!

Sdílet