Gutenberg: Bloky editovatelné

V minulém článku jsem se ukázal základní principy tvorby bloků na příkladu bloků statických, které toho moc nedělají. Dnes se podíváme tvorbu bloků, které již mají nějaké to kontextové menu pro úpravu a umožní uživateli trochu více interakce.

Errata pro minulé díly: Zde bych také chtěl opravit chybu, která se dostala do předchozích dílů. V rámci Gutenbergu neexistují pouze bloky statické a dynamické. Rozlišujeme 3 typy bloků: statické, editovatelné, a dynamické.

Statické jsme si již ukázali, o editovatelných si povíme v tomto článku, a později se dostanu i k blokům dynamickým. Rozdíl mezi bloky editovatelnými a dynamickými je ten, že bloky editovatelné nepotřebují ke svému vykreslení žádnou komunikaci se serverem. Naopak bloky dynamické v editoru slouží jako “placeholer” pro data, která se později získají ze serveru. Kupříkladu výpis nejnovějších příspěvků.

Marquee

Pro tento tutoriál jsem připravil implementaci bloku reprezentující HTML element <marquee> s možností nastavení vlastního textu, směru a rychlosti pohybu.

Text budeme editovat pomocí textarea, směr pomocí vlastního panelu nástrojů, a rychlost pomocí textového vstupu v inspektoru bloku (panelu na levo od oblasti, kde komponujeme text).

Jak definovat vlastní Gutenberg Blok už jsme si ukázali. Pojďme se tedy zrovna vrhnout na ukázku kódu, který si dnes rozebereme. (Samozřejmě je možné jej nakopírovat a vložit do developer console ve vašem prohlížeči na obrazovce pro editaci příspěvku na webu, kde máte Gutenberg instalovaný a vyzkoušet si jeho funkci).

Velmi podobný kód naleznete také na GitHubu, pro někoho to může být pohodlnější způsob čtení.

( function() {
	var el = wp.element.createElement,
	registerBlockType = wp.blocks.registerBlockType,
	PlainText = wp.blocks.PlainText,
	BlockControls = wp.blocks.BlockControls,
	InspectorControls = wp.blocks.InspectorControls,
	TextControl = wp.components.TextControl;

	registerBlockType( 'david-binda/marquee', { // Název bloku - prefix / jméno.

		// Titulek, ikona a kategorie pro zobrazení v nabíce bloků.
		title: 'Marquee',
		icon: 'controls-repeat',
		category: 'layout',

		/*
		 * Všechny atributy, které chceme moci měnit, musí být zaregistrované.
		 */
		attributes: {
			content: { // Vlastnost "content" typu řetězec.
				type: 'string',
			},
			direction: { // Vlastnost "direction" typu řetězec.
				type: 'string',
				default: 'left', // Výchozí hodnota.
			},
			scrollAmount: {
				type: 'number', // Číselný typ.
				default: 6,
			}
		},

		/**
		 * Funkce, která se zavolá při úpravě bloku
		 */
		edit: function( props ) {

			/**
			 * Callback pro případ, kdy se změní hodnota obsahu.
			 */
			function onChangeContent( newContent ) {
				props.setAttributes( { content: newContent } );
			}

			/**
			 * Callback pro případ, kdy se změní hodnota směru posuvu.
			 * onClick očekává funkci, tudíž musíme vrátit funkci.
			 */
			function onChangeDirection( newDirection ) {
				return function() {
					// Pokud jsme na tlačítko s vybraným směrem klikli opakovaně, nastavení zrušíme na defaultní "left".
					props.setAttributes( { 'direction': props.attributes.direction === newDirection ? 'left' : newDirection } );
				};
			}

			function onChangeLoop( newAmount ) {
				props.setAttributes( { 'scrollAmount': newAmount } );
			}

			/*
			 * Jelikož chceme prvek zobrazit už v editoru, pokud není zrovna upravován,
			 * tak jako na frontendu, pomůžeme si defaultní vlastností isSelected.
			 */
			if ( true !== props.isSelected ) {
				// Zde vracíme prvek prakticky stejně, jako v případě funkce "save".
				return el(
					'marquee', // HTML element
					{
						className: props.className, // Třída generovaná Gutenbergem.
						'direction': props.attributes.direction, // vlastní attribute direction.
						'scrollamount': props.attributes.scrollAmount || 6, // vlastní attribute scrollamount.
					},
					props.attributes.content || props.attributes.placeholder || "Hello world!" // obsah prvku, placeholder, nebo defaultní placeholder.
				);
			} else {
				/*
				 * Pokud s prvkem manipulujeme, chceme zobrazit nějaké ovládací prvky
				 * a také změnit element marquee na textarea, aby uživatel mohl editovat text.
				 */

				// Vrátíme celé pole prvků
				return [
					// Nejprve panel nástrojů obsahující možnost změny směru posuvu.
					el(
						BlockControls, // BlockControls je prvek existující v rámci Gutenbergu.
						{ 'controls': [ { // Jednotlivá tlačítka lze snadno definovat jako pole objektů.
							icon: 'arrow-left', // Ikona tlačítka v panelu nástrojů.
							title: 'Direction Left', // Popisek.
							onClick: onChangeDirection( 'left' ), // Akce vykonávající se při kliknutí.
							isActive: ( 'left' === props.attributes.direction ) // zda-li je tlačítko aktivní či nikoli.
						}, {
							icon: 'arrow-right',
							title: 'Direction Right',
							onClick: onChangeDirection( 'right' ),
							isActive: ( 'right' === props.attributes.direction )
						}, {
							icon: 'arrow-up',
							title: 'Direction Up',
							onClick: onChangeDirection( 'up' ),
							isActive: ( 'up' === props.attributes.direction )
						}, {
							icon: 'arrow-down',
							title: 'Direction down',
							onClick: onChangeDirection( 'down' ),
							isActive: ( 'down' === props.attributes.direction )
						} ] }
					),
					// Inspektor (zobrazí se v záložce "Blok" v pravém sloupci).
					el(
						InspectorControls,
						{},
						el(
							TextControl,
							{
								label: 'Scroll speed (defaults to 6):',
							 	value: props.attributes.scrollAmount,
								onChange: onChangeLoop,
								type: 'number',
							}
						)
					),
					// Dále samotný prvek v editovatelné podobě.
					el(
						PlainText, // Textarea.
						{
							className: props.className,
							onChange: onChangeContent, // Callback.
							placeholder: props.attributes.placeholder || "Hello world!", // Placeholder.
							value: props.attributes.content, // Obsah textového pole.
							isSelected: props.isSelected,
							style: {'display':'inline-block'}, // Marquee je defaultně inline-block, tak toto chceme zachovat.
						}
					),
				];
			}
		},

		/**
		 * Funkce, která se zavolá při ukládání prvku.
		 * Například přechod mezi visual a code editorem.
		 */
		save: function( props ) {
			/*
			 * Všimněte si, že toto je stejné, jako v případě, kdy
			 * isSelected vrací false (ve funkci edit).
			 */
			return el(
				'marquee',
				{
					className: props.className,
					'direction': props.attributes.direction,
					'scrollamount': props.attributes.scrollAmount || 6,
				},
				props.attributes.content || props.attributes.placeholder || "Hello world!" // obsah prvku, placeholder, nebo defaultní placeholder.
			);
		},
	} );
})();

V kódu jsem se snažil okomentovat skoro všechny řádky tak, aby i ten, kdo s Gutenbergem doposud nepracoval, pochopil co se na nich děje. Nyní ještě trochu podrobněji.

Editovatelné atributy

Všechny atributy bloku, které chceme uživateli užmoňit upravovat či nastavovat, je nutné předem registrovat pomocí klíče “attributes”. V přípabě ukázkového kódu se jedná o “content”, “direction” a “scrollamount”. U prvních dvou očekáváme jednoduchý řetězec, u posledního poté číselný vstup.

Attributy “direction”, a “scrollamount” mají nastavenou defaultní hodnotu (“left”, respektive 6).

“Attributes” by měl být objekt (v PHP by to bylo asociativní pole), kde klíč slouží k identifikaci atributu, a jako hodnota je poté další pole obsahující hlavně typ atributu (v našem případě řetězec – string).

Typů atributů existuje ovšem více. Jsou velmi dobře popsány v dokumentaci. Téma je to trochu rozsáhlejší, a než abych se o něm rozepisoval zde, věnuji mu později celý článek.

Funkce edit a props.isSelected

U editovatelných bloků jako tvůrci chceme uživateli v rámci editoru nabídnout dva způsoby zobrazení. Jeden kdy se zobrazí prvek v “edit” módu a druhý kdy se prvek zobrazí tak, jako by byl na frontendu.

Mezi těmito módy nemusí být velký rozdíl. Obyčejně se bude jednat pouze o změnu z HTML elementu samotného na PlainText či RichText umožňující tvorbu obsahu. Jindy může jít pouze o přidání panelu nástrojů (toolbar), přidání polí do inspektoru, či kombinaci předchozích.

Pro to, abychom mohli mezi jednolivými módy rozlišovat, máme k dispozici props.isSelected, který vrací true v případě, že prvek uživatel chce editovat – tj. klikl na něj a je vybrán.

V případě, že prvek zrovna není editován, většinou budeme nejspíše vracet stejné elementy jako v případě funkce save. V načem příkladě bychom mohli kód zjednodušit na následující:

edit: function( props ) {

    // nějaké ty pomocné funkce etc.

    if ( true !== props.isSelected ) {
        return $this.save( props );
    }

BlockControls

BlockControls je prvek, pomocí kterého lze tvořit panel nástrojů (toolbar). Stačí jen nadefinovat vlastní tlačíka a přidat jim vlastní callbacks.

Přidat tlačítka lze hned několika způsoby. Asi nejjednodušší je využít druhého parametru ( props ) funkce wp.element.createElement, která vychází z Reactu

props by měl být objekt (a to platí u každého prvku tvořeného pomocí wp.element.createElement, nejen pro BlockControls) a v případě, že chceme zobrazit vlastní tlačíka pro BlockControls prvek, využijeme klíč “controls” a jako hodnotu mu předáme pole objektů:

{ // objekt
    'controls': // klíč controls
    [ // pole objektů
        { //objekt
            icon: 'arrow-left',
            title: 'Direction Left',
            onClick: onChangeDirection( 'left' ),
            isActive: ( 'left' === props.attributes.direction || undefined === props.attributes.direction )
        },
        { // objekt
            icon: 'arrow-right',
            title: 'Direction right',
            onClick: onChangeDirection( 'right' ),
            isActive: ( 'right' === props.attributes.direction )
        }
    ]
}

Pokud bychom chtěli tlačítka v liště nástrojů seskupovat, můžeme namísto “pole objeků” jako hodnotu pro klíč “controls” použít “pole polí objektů”:

{
    'controls': // klíč controls
    [ // pole polí objektů
        [ // pole objektů #1
            { //objekt
                icon: 'arrow-left',
                title: 'Direction Left',
                onClick: onChangeDirection( 'left' ),
                isActive: ( 'left' === props.attributes.direction || undefined === props.attributes.direction )
            },
            { // objekt
                icon: 'arrow-right',
                title: 'Direction Right',
                onClick: onChangeDirection( 'right' ),
                isActive: ( 'right' === props.attributes.direction )
            }
        ],
        [ // pole objektů #2
            { //objekt
                icon: 'arrow-up',
                title: 'Direction Up',
                onClick: onChangeDirection( 'up' ),
                isActive: ( 'up' === props.attributes.direction )
            },
            { // objekt
                icon: 'arrow-down',
                title: 'Direction Down',
                onClick: onChangeDirection( 'down' ),
                isActive: ( 'down' === props.attributes.direction )
            }
        ]
    ]
}

Gutenberg by nám pak měl tyto jednotlivé bloky oddělit separatorem:

Panelů nástrojů lze ovšem také vložít více za sebou. To může být výhodné zvláště v případě, kdy si buď vytvoříme vlastní prvek obsahující tlačíka, která se opakují, nebo budeme chtít využít nějaký, který se v Gutenbergu již nativně objevuje (například AlignmentToolbar), a doplnit jej o vlastní sadu nástrojů:

return [
            el( // První panel nástrojů:
                BlockControls,
                { 'controls': [ {
                    icon: 'arrow-left',
                    title: 'Direction Left',
                    onClick: onChangeDirection( 'left' ), // Akce vykonávající se při kliknutí.
                    isActive: ( props.attributes.direction === 'left' || props.attributes.direction === undefined )
                }, {
		    icon: 'arrow-right',
		    title: 'Direction Right',
                    onClick: onChangeDirection( 'right' ),
                    isActive: ( props.attributes.direction === 'right' )
	        } ] }
            ),
            el( // Druhý panel nástrojů.
                BlockControls,
                { key: 'controls' },
                el(
                    wp.blocks.AlignmentToolbar, // Defaultní panel pro zarovnání.
                    {
                        value: alignment,
                        onChange: onChangeAlignment // Je nutné definovat.
                    }
                )
            ),
            // Dále samotný prvek
];

No a v neposlední řadě lze tlačítka vygenerovat jako samostatné prvky, které se do prvku BlockControls předají pomocí třetího (a následujících) parametrů. Podobně jako jsme u statických bloků tvořili obsah HTML elementů.

Callback pro uložení nastavení

Při vytváření vlastních prvků jsme použili jako callback pro akci (kliknutní na tlačíko) následující zápis:

onClick: onChangeDirection( 'right' ),

Funkce onChangeDirection je poté definována následujícím způsobem:

        /**
         * Callback pro případ, kdy se změní hodnota směru posuvu.
         */
        function onChangeDirection( newDirection ) {
            return function() {
                props.setAttributes( { 'direction': props.attributes.direction === newDirection ? 'left' : newDirection } );
            };
        }

Tedy, funkce onChangeDirection vrací anonymní funkci. To proto, že Reactí onClick očekává funkci, jako svou hodnotu, a navíc se musíme vypořádat s různými hodnotami pro každé tlačítko.

Všimněte si toho, že pokud na to aktivní tlačíko klikneme opakovaně, znamená to, že chceme nastavení zrušit. Jelikož má ale Marquee defaultní hodnotu pro attribut direction, resetujeme jen v takovém případě na “left”.

V případě prvku wp.blocks.AlignmentToolbar naopak definujeme “pouze” onChange parametr. Abstraktní prvek Toolbar se poté postará o podbné řešení skrze onClick sám. Prohlédněte si zdrojový kód.

Inspector

Gutenberg vedle panelu nástrojů umožňuje ještě vložení formulářových polí a dalších ovládacích prvků do tzv. Inspektoru. To je pravý sloupec v editoru, kam by se mělo umisťovat to, co se do to panelu nástorojů nehodí.

Podobně jako přidáváme panel nástrojů, přidáme i inspector, nicméně tentokráte budeme čerpat z wp.components. V ukázce níže naleznete jakým způsobem lze přidat wp.components.TextControl. Další *Control componenty lze nalézt na GitHubu Gutenbergu:

// Inspektor (zobrazí se v záložce "Blok" v pravém sloupci).
el(
	InspectorControls,
	{},
	el(
		TextControl,
		{
			label: 'Scroll speed (defaults to 6):', // Popisek.
			value: props.attributes.scrollAmount, // Hodnota.
			onChange: onChangeSpeed, // save callback.
			type: 'number', // attribut prvku input - lze definovat i další.
		}
	)
),// Následující kód již znáte.

Funkci onChangeSpeed jsem v ukázce nedefinoval, nicméně bude hooodně podobná funkci onChangeContent, která je použita v úvodní úkázce.

Textové vstupy – PlainText a RichText

Vedle panelů nástrojů, je třeba také uživateli umožnit tvořit nějaký ten text. Gutenberg v základu nabízí dva způsoby. Textové pole (textarea) a RichText editor, který není nepopodbný TinyMCE známého ze současného WordPressu.

Jelikož ale RichText pracuje s attributy, kde zdrojem (source) je skupina prvků, dovolím si dnes rozebrat pouze PlainText a k RichText se vrátit v dalším díle. Pro zájemce však uvedu několik odkazů:

PlainText

Jednodušší na vysvětlení tedy, zvláště vzhledem k vazbě na “Attributes” vysvětlované o pár odstavců výše, je PlainText (konkrétně wp.blocks.PlainText). Atribut, který PlainText umožňuje vytvořit může být jednoduchý řetězec, jelikož jako vstupní pole je použita textarea, která se sama přizpůsobuje velikosti vstupu, a do níž, z pricipu, nelze vkládat nic jiného než text.

Tedy, abychom si připomněli kód:

                // Dále samotný prvek
                el(
                    PlainText, // Textarea.
                    {
                        className: props.className,
                        placeholder: props.attributes.placeholder || "Hello world!",
                        value: props.attributes.content, // Obsah textového pole.
                        onChange: onChangeContent, // Callback.
                        isSelected: props.isSelected,
                        style: {'display':'inline-block'}, // Marquee je defaultně inline-block, tak toto chceme zachovat

                    }
                ),

“className” je důležité hlavně proto, že Gutenberg generuje class name z názvu, a prefixu, prvku. Díky predikovatelnosti lze toto využít k tvorbě CSS stylů.

Dále máme “placeholder”. Zmiňoval jsem, že Gutenberg pracuje s placeholdery, aby tak umocnil intuitivnost ovládání. I náš marquee blok by tedy nějaký ten placeholder měl obsahovat.

“value” je hodnota, která je nastavena prvku, když je zobrazen. Připomeňme si, že textové pole se zobrazí pouze, pokud je prvek vybrán uživatelem s cílem s ním něco udělat – přesunout jej, nebo upravit obsah, či použít panel nástorů. Hodnota je tedy generována pomocí props.attributes.content, kam se pomocí callbacku definovaném v “onChange” ukládá.

Dále nalezneme “isSelected”, kterým předáváme hodnotu toho, zda-li je prvek vybrán a jako ukázku toho, že lze nadefinovat libovolný HTML atribut, který se poté vypíše, je použit “style”, kterým se v našem případě snažíme docílit toho, aby textarea měla podobné vlastnosti jako marquee – tedy aby byla zobrazena jako inline-block.

“onChange” je callback pro to, co se má stát, když se hodnota změní. Není volána pokaždé, když je připsáno písmeno, ale poté, co uživatel dokončí editaci. Nejedná se o nic komplikovaného:

        /**
         * Callback pro případ, kdy se změní hodnota obsahu.
         */
        function onChangeContent( newContent ) {
            props.setAttributes( { content: newContent } );
        }

Pomocí props.setAttributes() dostaneme hodnotu do props.attributes.content, kterou používáme i jinde v kódu.

Kam se tato data ukládají?

Všechny hodnoty, které lze v rámci marquee bloku upravovat, se ukládají přímo do post_content. V rámci parsovatelných HTML komentářů. Ty se na frontendu nezobrazují, ale Gutenbergu umožňují vygenerovat příslušné uživatelské rozhraní a skladovat dané hodnoty.

Pro zobrazení, pokud máte blok zaregistrovaný a vytvořený, staří přepnout do módu umožňující úpravu zdrojového kódu (stejné zobrazení existuje i u současného editoru obsahu). Zde je ukázka, jak takový uložený blok vypadá:

<!-- wp:david-binda/marquee {"direction":"right","scrollAmount":"10"} -->
<marquee class="wp-block-david-binda-marquee" direction="right" scrollamount="10">Hello world!</marquee>
<!-- /wp:david-binda/marquee -->

Závěrem

Udržet tento článek v rozumném rozsahu bylo opravdu těžké – některá témata budu muset později ještě jednou a tentokráte více do hloubky rozebrat. Ovšem věřím, že i bez hlubšího zastavení se, by měl kód posloužit jako dobrý základ pro další studium.

Určitě doporučuji si s kódem použitým v tomto článku trochu pohrát – stačí je vložit a upravit přímo v developer console vašeho prohlíže. Připomenu, že před opakovanou registrací prvku, lze použít následující kód pro jeho odstranění:

wp.blocks.unregisterBlockType( 'david-binda/marquee' );

Kód, který jsem v tomto článku použil jsem, více či méně, v nezměněné podobně nahrál také na GitHub. Velmi pravděpodobně se k němu v blízké budoucnosti ještě vrátím, rád bych jej přepsal do JSNext, nicméně v podobně v jaké je distukován zde bude navždy žít v tagu 0.1.0. Použitím ES5 pro tento tutoriál jsem chtěl zacílit i na ty z vás, kteří moderní JavaScript a další nástoje nemají ještě natolik zažité.

Pokud byste se rádi podívali na to, jak lze moderně precovat s Gutenbergem a nevíte jak přesně nastavit Webpack a další, vyzkoušjte projekt create-guten-block

Gutenberg: Bloky statické

Stejně tak jako WordPress těží z možnosti jednoduché rozšiřitelnosti, systém filtrů a ackí je takřka unikátní a snadno použitelný i pro programátora začátečníka, i Gutenberg je připravován tak, aby jej vývojáři mohli snadno doplnit o chybějící funkcionalitu – vlastní bloky.

V současném Gutenbergu lze identifikovat 3 druhy bloků. A sice bloky statické, dynamické, a bloky ukládající data do post meta – ne všechna data chceme nutně mít jen v `post_content`. Naopak, ne všechna data, která dnes ukládáme do post_meta musíme skladovat právě tam. Gutenberg se svým konceptem umožňuje často data, která by skončila jinde, skladovat přímo v post_content, kam patří.

V tomto článku se budeme věnovat pouze blokům statickým. Ačkoli toho moc neumí, poslouží nám jako velmi dobrý úvod do problematiky vytváření vlastních bloků.

Bloky statické

Statické bloky jsou nejjednodušším typem bloků. Pokud jsem vás v předchozím článku s úvodem do bloků a šablon navnadil contextovými panely nástrojů, tak vás statické bloky zklamou. Nicméně i ony mají své opodstatnění, a jsou velmi vhodné pro vysvětlení si anatomie bloku.

Předpokládám, že jste vývojář, a tak nemá cenu vás dlouho zdržovat povídáním. Tady je kód (originál lze nalézt v Gutenberg Handbook):

var el = wp.element.createElement;

wp.blocks.registerBlockType( 'mytheme/red-block', { // registrace bloku s názvem red-block a prefixem mytheme
    title: 'Červený Blok', // Titulek, zobrazuje se ve výpisu bloků
    icon: 'universal-access-alt', // Ikonka bloku, zobrazuje se ve výpisu bloků
    category: 'layout', // Kategorie bloku, ovlivňuje umístění ve výpise bloků
    edit: function() { // Funkce, která generuje HTML během editace příspěvku
        return el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'Toto je červený blok.' );
    },
    save: function() { // Funkce, která generuje HTML zobrazené mimo editaci příspěvku
        return el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'Toto je červený blok.' );
    }
} );

Název bloku

Jak jsem již zmiňoval minule, Gutenberg, mimo jiné, velmi dobře pracuje s best practices. Namísto toho, aby pouze doporučoval bloky prefixovat, užití prefixu přímo vyžaduje. Zde je ukázka toho, co se stane, pokud odstraním mytheme/ z názvu bloku v kódu zmíněném výše:

Takže, stejně jako je velmi vhodné si pro svůj plugin zvolit prefix, který se používá pro všechno (ano, pokud používáte v PHP namespaces, není třeba prefixovat názvy funkcí, ale určitě je vhodné prefixovat také názvy meta dat, názvy options, unikátní název by měly mít i lokalizační stringy, a tak dále, a tak dále), tak tento prefix by se měl objevit i v blocích, které vaše šablona či plugin nabízí.

Druhá část názvu bloku je jasná – unikátní název v rámci daného prefixu / namespace.

CSS třídy

Z názvu bloku Gutenberg také generuje třídu, která je vygenerovanému bloku přidělena. A tudíž je snadné připravit potřebné selectory. V případě našeho “Červeného Bloku” s názvem mytheme/red-block bude vygenerována třída s prefixem wp-block- a / bude nahrazeno pomlčkou -. Kaskádový styl pak může vypadat nějak takto:

.wp-block-mytheme-red-block {
    border: 2px solid #9c9;
    padding: 20px;
}

Titulek, Ikona, a Kategorie

Jak již bylo řečeno, Gutenberg počítá s tím, že bude snadno a často rozšiřován bloky třetích stran. Z TinyMCE víme, že toolbar není nafukovací. I když velmi často vídám instalované plugin, které přidávají další a další řady nástrojů jen proto, aby uživatel měl po ruce všechna myslitelná tlačítka. Nicméně Gutenberg se nesnaží všechny bloky zobrazit najednou. Nýbrž nabízí možnost bloky filtrovat pomocí našeptávače. Pokud blok stále uživatel nenachází, je možné navštívit záložku, která vypisuje bloky dle kategorií. V současné době jsou k dispozici následující kategorie:

  • Common Blocks (common) – bloky, které se v obsahu vyskytují často. Kategorie je první z vypsaných.
  • Formatting (formatting) – formátovací bloky, jako tabulka, zdrojový kód, citace
  • Layout Blocks (layout) – horizontální čára, more (aka zobrazit více), tlačítko, sloupce
  • Widgets (widgets) – shortcode, výpis nejnovějších příspěvků …
  • Embed (embed) – všechny embed služby – Twitter, Facebook, you name it!

Všechny bloky jsou ve výpisu a vyhledávání zobrazeny spolu se svým názvem a ikonkou. Ikonku si lze zvolit ze všech, které jsou k dispozici v rámci Dashicons – nejspíš znáte z vlastní položky menu, ikonky custom post type, či z vlastní konfigurační stránky.

Všechny tyto parametry, ačkoli se to může zdát malicherné, jsou důležité právě pro to, aby je uživatelé snadno nalezli. Rozhodně se podívejte do jakých kategorií jaké bloky patří, než plácnete svůj blok někam, kde by jej nikdo nehledal. Název by měl být intuitivní, jelikož si dokážu představit, že nikoli jen power-users, ale všichni uživatelé se pokusí blok nejdříve vyhledat pomocí klíčového slova.

No a ikonka? Vsadím se, že správná volba pomůže uživateli právě ve chvíli, kdy vyhledávání pomocí klíčového slova, tedy titulku, selže. Takže i zde věnujme chvíli času. Prohlédněte si galerii dostupných ikonek.

Funkce edit a save

V naší ukázce toho tyto funkce nedělají zrovna mnoho. Nicméně mají na svědomí HTML, které je blokem generováno a na frontendu nakonec vytištěno (to platí především pro funkci save).

HTML element vrácený z funkce edit je použito během editace příspěvku, naopak HTML element vrácený z funkce save je HTML, které je uloženo do post_content a nakonec tedy zobrazeno na frontendu (nebo při přepnutí do HTML, aka Code, editoru).

wp.element.createElement

Gutenberg nabízí jazykově neutrální API, abstrakci nad knihovnou, jenž je interně použita. Onou interní knihovnou je v současnoti React. Abstrakce na Reactem není pouze proto, aby bylo možné React v případě potřeby nahradit, ale také proto, aby bylo případně možné překlenout změny mezi jednotlivými verzemi Reactu bez toho, aniž by vývojáři šablon a pluginů museli přepisovat své implementace. Ano, i Gutenberg se řídí WordPress filozofií zpětné kompatiblity.

V našem příkladě:

var el = wp.element.createElement;
...
 el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'Toto je červený blok.' );

Je vytvořen element div s atributy style nastavující červenou barvu pozadí, bílý text a padding. Poslední parametr funkce je poté innherHTML takového elementu. Záměrně uvádím innerHTML – nemusí totiž nutně obsahovat pouze text, ale také další element, či několik elementů:

return el(
    'div',
    { style: {
        backgroundColor: '#900',
        color: '#fff',
        padding: '20px'
    } },
    el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'Toto je červený blok.' ),
    el( 'div', { style: { backgroundColor: '#900', color: '#fff', padding: '20px' } }, 'Toto je červený blok.' ) );

Použité ve funkcích edit a save vygeneruje následující HTML:

<!-- wp:mytheme/red-block -->
<div style="background-color:#900;color:#fff;padding:20px" class="wp-block-mytheme-red-block">
    <div style="background-color:#900;color:#fff;padding:20px">Toto je červený blok.</div>
    <div style="background-color:#900;color:#fff;padding:20px">Toto je červený blok.</div>
</div>
<!-- /wp:mytheme/red-block -->

Všimněte si v JS kódu konstrukce, kdy druhý vnořený blok div je použit jako 4. parametr – netřeba ze 3. parametru dělat pole, každý další vnořený element předáme jako další parametr.

Pro více informací o wp.element zavítejte do dokumentace přímo v GitHub repozitáři

Domácí úkol

Pokud jste si Gutenberg stále ještě nenainstalovali, učiňte tak nyní. Lze jej jednoduše nainstalovat přímo z plugin repozitáře. Po instalaci a aktivaci si budete moci vyzkoušet tvorbu obsahu, ale hlavně také vyzkoušet tvorbu bloků.

Navíc, tvorbu bloků zmíněných v tomto článku lze provést přímo z webového prohlíže tím, že kód vložíme přímo do konzole! Jednoduše nakopírujte kód zmíněný výše, otevřete konzoli, vložte kód a stiskněte enter. “Červený blok” se objeví v nabídce bloků a lze jej do stránky vložit.

Vyzkoušejte si změnu generovaných elementů a také změnu elementů mezi jednotlivými stavy (funkce edit a save). Kupříkladu změna textu či barvy pozadí.

Určitě si všimnete, že nelze jednoduše zaregistrovat jeden blok vícekrát. V tom případě se vám bude hodit následující funkce pro deregistraci existujícího blocku. Její zavolání vám umožní ukázkový kód zavolat znovu:

wp.blocks.unregisterBlockType( 'mytheme/red-block' );

Další zdroje ke studiu:

Gutenberg: Bloky a šablony obsahu

Projekt Gutenberg přichází s konceptem bloků, pomocí kterých uživatelé WordPressu budou moci nově skládat obsah jednotlivých příspěvků, namísto aby v rámci jednoho jediného WYSIWYG editoru vytvářeli obsah, ze kterého se již při tvorbě stává jakýsi blob.

Poznámka: pokud jste Gutenberg ještě netestovali, rozhodně si jej nainstalujte a vyzkoušejte.

Bloky a kontextové panely nástrojů

Bloky, které uživatel nově bude mít k dispozici, umožní pohodlný a jednoduchý způsob jakým lze s obsahem manipulovat. To uživatelé ocení pokaždé, když se budou muset k nějaké části příspěvku vrátit – ať již během úvodní tvorby příspěvku, nebo poté při jeho další úpravě.

To, že samotný obsah příspěvku, stránky, či vlastního typu obsahu (custom post type; CPT) bude rozdělen do jednotlivých bloků umožňuje zcela nový rozměr úprav. Tvůrci bloků (základní sada je součástí Gutenbergu, a tedy je/bude dostupna bez dalšího) mohou nadefinovat nástroje a nastavení, které se daného bloku týkají. To je zásadní rozdíl oproti TinyMCE, který poskytuje jeden globální panel nástrojů “pro všechno”.

Takový kontextový panel nástrojů může u odstavce vypadat následovně:

Pro odstavec opravdu většinou nepotřebujeme nic jiného než zarovnání, důraz (tučné písmo, kurzíva), přeškrtnutí, či možnost vytvořit odkaz. Naopak pro nadpis využijeme spíše možnost změnit úroveň nadpisu:

Takovýto kontextový panel nástrojů by měl uživatelům vyhovat více než jeden globální. Navíc Gutenberg jde dál, a v rámci jednoho bloku může existovat více oblastí, kde se panel nástrojů zobrazí, a tvůrci bloků tak mohou vytvářet i komplexnější struktury, přičemž je zachována přívětivost úprav a přímá zpětná vazba s tím, jak bude jejich obsah nakonec vypadat. Podívejme se na blok “cover image”:

Šablony příspěvků a vlastních typů obsahu

Gutenberg vedle bloků samotných, které uživatelé mohou přidávat, přináší také koncept šablon. Pro začátek se jedná “pouze” o šablony obsahu jednotlivých typů obsahu (příspěvek, stránka, CPT). Vývojář šablony či pluginu může ke svému, či některému z výchozích typů obsahu, nadefinovat šablonu v podobě bloků, které jsou při tvorbě nového příspěvku předvyplněny do obsahu.

Takové šablony mohou být zaregistrovány dokonce jako neměnné! To je, dle mého názoru, celkem silný nástroj pro tvůrce šablon (a nyní myslím vzledů/témat pro celý web, nikoli pro obsah příspěvku v rámci Gutenbergu), zvláště pak pokud tyto mají složitější design a předpokládají nějaký specifický obsah.

Placeholders

Na tomto místě si dovolím malou odbočku. Gutenberg se snaží tvůrce nových bloků trochu více dirigovat. A to nejen co se týče stylu programování, ale i kvality bloků z pohledu uživatelů. Osobně takovou snahu jen kvituji. Jeden z designových principů, které Gutenberg přináší jsou placeholders ( ale je toho vícero, a rozhodně doporučuji přečíst celé Gutenberg Design Principles & Vision ).

Placeholder je obsah, ať již textový či obrazový, kterým se předvyplní blok ve chvíli kdy je přidán do obsahu stránky. Tím dá okamžitě tvůrci obsahu jasnou představu o tom, jak který blok bude sám o sobě vypadat, a jak bude fungovat s okolím.

Navíc, takový placeholder může třeba obsahovat i krátký návod k tomu, co se po uživateli očekává, případně i vysvětlení jaký obsah má doplnit.

Registrace vlastních šablon

Šablony obsahu příspěvků jsou, po samotné instalaci pluginu, asi nejjednodušší způsob jakým lze začít s Gutenbergem jakožto vývojář. V zásadě se jedná jen o další parametr funkce register_post_type.

Zde si dovolím zkopírovat ukázku z Gutenberg Handbook, kde je zaregistrován nový CTP pro knihy, a je mu předpřipravena šablona obsahující blok pro obrázek, nadpis, a odstavec:

function register_post_type() {
    $args = array(
        'public' => true,
        'label'  => 'Books',
        'show_in_rest' => true,
        'template' => array(
            array( 'core/image', array(
                'align' => 'left',
            ) ),
            array( 'core/heading', array(
                'placeholder' => 'Add Author...',
            ) ),
            array( 'core/paragraph', array(
                'placeholder' => 'Add Description...',
            ) ),
        ),
    );
    register_post_type( 'book', $args );
}
add_action( 'init', 'register_post_type' );

Co se kódu týče, všimněte si, že názvy všech bloků (kupříkladu core/heading, či core/paragraph) obsahují prefix. Ono core/, který je vyhrazený pro defaultní sadu bloků. Gutenberg neumožňuje vývojáři zaregistrovat blok, který by prefix neměl. Je skvělé vidět, že jsou best practices jako “prefix everything” vynucovány.

Výše zmíněný kód by měl vyprodukovat něco podobného následujícímu rozhraní:

Všimněte si onoho “Add Author…” či “Add Description…”, což jsou přesně ony nápovědy toho, co se po uživateli očekává zmiňované v předchozím odstavci. Lze si třeba představit šablonu pro tiskové zprávy, kdy se uživatel z placeholderů jednotlivých bloků dozví, co kam patří, a šablona mu tak pomůže vytvořit kvalitní tiskovou zprávu:

Více ukázek, včetně toho jak takovou šablonu uzamknout pro editaci, či jak přidat šablonu již existujícím typům obsahu (které třeba nemůžete modifikovat), určitě si projděte příslušný handbook.

Závěrem

Jak již bylo zmíněno, šablony jsou asi nejjednodušší způsob jak začít s Gutenbergem něco dělat. Jejich tvorba je jednoduchá a defaultní nabídka bloků je dostatečně bohatá na to, aby bylo možné s jejich pomocí připravit nějaký funkční prototyp.

Tak jako je to v Gutenbergu se vším, je nutné hledět i do budoucna. Podobným způsobem jakým šablony definují obsah příspěvku či stránky, bude v dalších fázích projektu možné definovat celé layouty webu.

Osobně se mi nikdy nepodařilo, bez menších či větších kódovacích zásahů, nastavit šablonu na svém webu tak, jak jsem ji viděl v demu. Nicméně to, jakým způsobem Gutenberg umožňuje šablonovat a vést uživatele k tomu, aby jen “doplňoval prázdná místa” v layoutu, a přináší tak jiskřičku naděje, že jednou opravdu na svůj web dostanu šablonu přesně tak, jak jsem ji viděl v demu, jen si dosadit vlastní obsah.

WP_Cron

Důležité pro pochopení akcí naplánovaných pomocí WP_Cron je to, že WP_Cron není cron takový, jaký známe ze světa serverů – spolehlivý pracovník, který v daný čas udělá to, co mu řeknete. V případě WP_Cron se nelze spoléhat na to, že se naplánovaná akce spustí v čas. Nicméně se můžete spolehnout na to, že se nespustí dříve.

Je to dáno tím, že WordPressu se snaží veškerou svou funkcionalitu poskytnout i na tom nejlevnějším sdíleném hostingu ihned po světoznámé 5 minutové instalaci.

WP_Cron je systém postaven čistě na záznamu v MySQL databázi, systému zámků využívajícího Transients API (tedy buď databázi či objektovou cache), HTTP požadavcích a PHP. V takovém setupu jsou proto stále přítomny timeouty, PHP memory limit či max-execution time.

Základ pro funkcionalitu, kterou WP_Cron nabízí tedy není zcela ideální, ale i tak je WP_Cron užitečným pomocníkem – pokud tedy víme jak se k němu chovat a co od něj očekávat. To vše se pokusím aspoň nastínit v tomto článku.

Jak naplánovat akci ze svého kódu

Na webu prakticky okamžitě naleznete obstojný tutorial o tom, jak nějakou událost naplánovat. Já budu v dalším výkladu spoléhat na to, že jste si tu práci již dali a níže uvádím jen kód, který by měl být výsledkem takových tutoriálů. Poté přejdu rovnou k popisu mechanismu, který tento kód spouští. Začínáme tedy tam, kde tutoriály končí.

//následující funkce je spouštěna nějakou uživatelskou akcí, typicky odesláním formuláře
function my_example_function_called_as_a_result_of_user_action( ... ) {
    $post_id = 2; //typicky dynamicky získaná proměnná.
    $variable = 'Hello WP_Cron World!';
    wp_schedule_single_event( time(), 'trigger_my_cron_function', array( $post_id, $variable ) );
}

add_action( 'trigger_my_cron_function', 'run_my_cron_function', 20, 2 );

function run_my_cron_function( $post_id, $variable ) {
    update_post_meta( intval( $post_id ), 'cron_set_variable', sanitize_text_field( $variable ) );
}

Pokud vám ukázka kódu nic neříká, je nejvyšší čas si přečíst některý z tutoriálů, či se podívat do kodexu

Jak WordPress plánuje události

Pokaždé, když je z kódu zavolána funkce wp_schedule_single_event, dojde k aktualizaci záznamu v tabulce wp_options. Jedním ze záznamů v této tabulce jsou naplánované události – uložené jsou všechny pěkně pohromadě v option s názvem cron podobně serializovaného pole s údajem o čase na místě klíče a naplánovaných volání funkce v hodnotě záznamu pole. Zde je podoba cron option na mé lokální instalaci založené na VVV získaná pomocí WP_CLI a příkazu shell

$ wp shell
wp> get_option( 'cron' );
array(4) {
  [1451331840]=>
  array(1) {
    ["wp_maybe_auto_update"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
  }
  [1451332812]=>
  array(1) {
    ["wp_scheduled_delete"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(5) "daily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(86400)
      }
    }
  }
  [1451333427]=>
  array(3) {
    ["wp_version_check"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
    ["wp_update_plugins"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
    ["wp_update_themes"]=>
    array(1) {
      ["40cd750bba9870f18aada2478b24840a"]=>
      array(3) {
        ["schedule"]=>
        string(10) "twicedaily"
        ["args"]=>
        array(0) {
        }
        ["interval"]=>
        int(43200)
      }
    }
  }
  ["version"]=>
  int(2)
}

Ukládání do wp_options, namísto použití transients API či jiného dočasného mechanismu je zvoleno proto, že tyto záznamy musí zůstat zachovány až do té doby, dokud nedojde k jejich zpracování. V případě, že by na webu byla použita externí cache využívaly by zmíněné transients tuto cache a ta nám nikdy negarantuje minimální dobu, po kterou je záznam dostupný, pouze maximální – tedy tu, od kdy už záznam dostupný nebude.

autoload options, cron a object cache

Skutečnost, že WP_Cron využívá wp_options má ještě minimálně jeden háček, který je nutné brát v potaz. WordPress využívá options pro skladování informací o svém nastavení, uživatelských rolí, nastavení šablony, pro transients a stejná tabulka také slouží často jako úložiště dat pro pluginy. Protože některá z těchto nastavení jsou důležitá při každém načtění WordPressu a některá nikoli, WordPress využívá sloupeček autoload pro uchovávání informace o tom, které options mají být automaticky z databáze načteny v samotném začátku načítání aplikace. WP_Cron je jednou z takových autoloaded options:

$ wp shell
wp> global $wpdb
wp> $wpdb->get_results( "SELECT * FROM {$wpdb->options} WHERE option_name = 'cron'" );
array(1) {
  [0]=>
  object(stdClass)#82 (4) {
    ["option_id"]=>
    string(3) "103"
    ["option_name"]=>
    string(4) "cron"
    ["option_value"]=>
    string(828) "a:4:{i:1451331840;a:1:{s:20:"wp_maybe_auto_update";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}}i:1451332812;a:1:{s:19:"wp_scheduled_delete";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:5:"daily";s:4:"args";a:0:{}s:8:"interval";i:86400;}}}i:1451333427;a:3:{s:16:"wp_version_check";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}s:17:"wp_update_plugins";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}s:16:"wp_update_themes";a:1:{s:32:"40cd750bba9870f18aada2478b24840a";a:3:{s:8:"schedule";s:10:"twicedaily";s:4:"args";a:0:{}s:8:"interval";i:43200;}}}s:7:"version";i:2;}"
    ["autoload"]=>
    string(3) "yes"
  }
}

Proč to říkám. Říkám to proto, že funkce wp_load_alloptions využívá WP_Cache a ta může být poháněná pomocí externí objektové cache a to může mít svá úskalí. Například defaultní maximální velikost objektu skladovaného v Memcache je 1M. Může se tak velice snadno stát, že tenhle 1M záznamy WP_Cron zaplníte a externí objektová cache v případě alloptions přestane fungovat. A přitom je alloptions často jedním z prvních důvodů, proč se člověk k Memcache či jiné objektové cache dostane.

Kdy WordPress vykoná naplánovanou událost

Jak jsem již zmínil, WP_Cron je jakýsi kvazi cron, nikoli cron v pravém slova smyslu. To, jakým způsobem je realizován v rámci WordPressu není nic neobvyklého a vlastně dost lidí překvapí, že využívá stejný mechanismu, jako vše ostatní – filtry a akce.

Vezměme si modelový problém. Není to problém čistě akademický, ale něco, co skutečně vývojáře trápí.

Naplánujeme publikaci článku na půlnoc. Klikneme tedy na tlačítko “Schedule”. Záznam o této budoucí akci je zapsán do tabulky wp_options a WordPress vás přesměruje zpět na výpis článků. To je vše, nic dalšího se nekoná a WordPress i server spí.

A spí tak dlouho, dokud jej nikdo nenavštíví. A dokud WordPress spí, nemůže vykonávat žádné akce. WordPress vzbudíte tím, že navštívíte nějakou stránku – ať již na frontendu či v administraci. Ale pozor, v případě frontendu se musí jednat o necachovanou stránku. Pokud tedy web pro nepřihlášené uživatele cachujete pomocí nějaké full page cache, je nutné se přihlásit.

No a pokud vy, ani nikdo jiný web kolem půlnoci nenavštíví (a nebo navštíví, ale je mu předložena cachovaná verze), může se dost dobře stát, že druhý den ráno si vzpomenete, že jste na půlnoc měli naplánovanou publikaci článku a rádi byste se podívali na komentáře.

Ale jelikož jste prvním návštěvníkem webu od doby, kdy jste dokončili článek a naplánovali jej, žádné komentáře nečekejte. WordPress se právě probudil a hodlá dohnat vše, co zameškal. Včetně publikace vašeho článku, která nastane až krátce poté, co jste web navštívli. Již bychom měli mít představu o tom, proč nedošlo k publikaci článku o půlnoci – protože se na web nikdo nepodíval nebo se podíval na cachovaný frontend a WordPress samotný tedy spal. Proč ale nedojde k publikaci článku ihned ve chvíli, kdy se přihlásím do administrace?

WP_Cron a výkon

Jelikož WP_Cron není odbavován přesně v čas, kdy to bylo naplánováno, může se stát, že na pořadu je v danou dobu více akcí, které již měly proběhnout, nicméně kvůli tomu, že web nikdo nenavštívil, neproběhly. Kdyby se WordPress snažil okamžitě vše zpracovat, došlo by k tomu, že by takové načtení stránky trvalo nesnesitelně dlouho a dost pravdědopodobně by došlo k timeoutu požadavku. Tomu ale WordPress předchází.

Asynchronní PHP

Asynchronní PHP je něco, co by mnoho vývojářů velmi ocenilo. Ovšem ještě stále nejsme tak daleko, abychom jej mohli plně využívat na libovolném hostingu. A tak WP_Cron využívá techniky, která spoléhá na HTTP requesty.

Nejspíš jste si všimli, že v rootu vaší WordPress instalace naleznete soubor wp-cron.php. Ten hraje klíčovou roli v tom, jak WordPress spouští cron tak, aby neblokoval načítání stránky pro daného návštěvníka.

Vždy když navštívíte necachovanou verzi stránky,

// WP Cron
if ( !defined( 'DOING_CRON' ) )
	add_action( 'init', 'wp_cron' );

WordPress vyšle HTTP požadavek na http://example.com/wp-cron.php?doing_wp_cron a přidá k tomu nějaké parametry.

Relevantní kód: https://github.com/WordPress/WordPress/blob/77e365efbf2e499e2ed11d29c101ea466cf1ceed/wp-includes/cron.php#L352

Důležité je, že se jedná o tzv. non-blocking HTTP request. Tedy WordPress odešle požadavek, ale již nečeká na odpověď. Ta jej vůbec nezajímá a načítání vaší stránky může nerušeně pokračovat.

Jak WordPress vykonává naplánované akce

WordPress vykonává naplánované akce tak, že vybere již zmíněný záznam z tabulky wp_options (cron) obsahující serializované pole z databáze a prochází celé pole, položku po položce a vždy porovnává časový záznam s aktuálním časem. Pokud je naplánovaný čas v minulosti je tato položka z pole odebrána a zavolá se funkce, která je vedle času v dané položce uložena a předají se ji uložené argumenty.

Teoreticky se tedy může stát, že mezi odstraněním položky z cronu a vykonáním akce dojde k timeoutu či vyčerpání PHP memory limitu. V takovém případě je akce navždy ztracena a již se nikdy nevykoná. Je to ovšem lepší varianta, než aby se naplánovaná akce spustila opakovaně.

Vždy běží jen jeden proces

Jak jsem zmínil, vždy, když někdo navštíví necachovanou stránku, je odeslán dotaz na wp-cron.php. Nebýt systému zámků, docházelo by k tomu, že vedle sebe poběží dva a více konkurenčních procesů zpracovávajících záznam o naplánovaných akcích. To by opět vedlo v problémům – ať již výkonnostním, tak i funkčním, jelikož by se některé akce vykonávali vícekrát, než bylo původně myšleno.

Systém zámků jsem již nastínil v úvodu. WP_Cron využívá transients API (ať jsou již ukládané do databáze, či do object-cache). Pokud existuje specifický transients záznam, wp-cron.php svou činnost ukončí a nepustí se do zpracování naplánovaných akcí. Čili velké množství requestů na http://example.com/wp-cron.php?doing_wp_cron končí velmi záhy.

Relevantní kód:

Nasazení podpory opravdového cronu

Nicméně pokud je váš web hojně navštěvován, může i velké množství HTTP requestů spouštějícíh wp-cron.php způsobit problémy na straně serveru. A zde se již dostáváme k technice, která umožňuje zajistit, že WP_Cron se bude vykonávat periodicky i tehdy, pokud web nikdo nenavštíví (nebo je vhodně a silně cachován) a navíc zajistí, že návštěva necachované stránky nevytvoří nový HTTP požadavek na wp-cron.php, tedy na server.

WordPress umožňuje mechanismus HTTP requestů vypnout pomocí konstanty DISABLE_WP_CRON.

//file: wp-config.php
define(‘DISABLE_WP_CRON’, true);

Pokud je definována s hodnotou true ve vašem wp-config.php, žádné požadavky na wp-crong.php se neprovádí. To ovšem znamená, že se žádná z naplánovaných akcí nikdy neprovede.

Je totiž třeba dalšího kroku. A sice nastavení opravdového cronu tak, aby odesílal HTTP požadavky na wp-cron.php:

wget -q -O - http://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

Podrobnější návod jak na to určitě naleznete na googlu.

Závěrem

Tento článek by měl pomoci vysvětlit mechanismus stojícím za WP_Cron a to, co tato konkrétní implementace způsobuje. Určitě jste se všichni někdy setkali s “Missed Schedule” hláškou v administraci označující příspěvek, který nebyl doposud publikován, ačkoli měl být. Důvody pro existenci tohoto fenoménu by vám nyní již měly být jasné.

Určitě doporučuji podívat se nyní do souboru wp-cron.php. To se v něm odehrává by vám nyní mělo být jasnější.

Pokud je pro vás pravidelnost publikování příspěvků či provádění některých akcí, zkuste popřemýšlet o podpoře WP_Cronu skrze reálný cron. U více navštěvovaných webů tak navíc můžete ulehčit vašim serverům.

Zrádné počty zobrazení příspěvku

K napsání tohoto příspěvku mě inspiroval blogpost “Admin-ajax.php zpomaluje stránky”, který správně uvádí proč byl daný web pomalý a jak byl problém, aspoň částečně, odstraněn. Proč je problém odstraněn jen částečně rozvádím ke konci tohoto příspěvku.

Vývojář se čas od času setká s poždavkem na zobrazování počtu zobrazení příspěvku na frontentu. Ovšem taková feature je celkem zrádná. V čem je problém?

Na otázku kam s daty si většinou vývojář celkem rychle odpoví, že přeci logicky patří do post_meta.

Tím velmi jednoduše získá místo, kam své údaje může ukládat. Pokud navíc nechce skladovat žádné další údaje o tom, kdo kdy odkud a jak se na web podíval, bude se jednat jen o číslo a takové číslo moc místa v databázi nezabírá. Jednoduché.

První nástřel fungování může vypadat takto:

//Uložíme shlédnutí do databáze
add_action( 'wp_head', function() {
    if ( is_single() ) {
        //získáme uložená data
        $pageviews = get_post_meta( get_the_ID(), 'myawesome_pageview', true );
        //zvětšíme o jedno
        $pageviews = intval( $pageviews ) + 1;
        //znovu uložíme
        update_post_meta( get_the_ID(), 'myawesome_pageview', $pageviews );
    }
}, 10, 0 );

//funkce pro zobrazování počtu shlédnutí
add_filter( 'the_content', function( $content ) {
    if ( ! is_single() ) {
        return $content;
    }
    $content .=  "\nZobrazeno: " . get_post_meta( get_the_ID(), 'myawesome_pageview', true ) 'x';
}, 10, 0 );

Těhle pár řádků kódu dělá přesně to, co vývojář řekl. Ukládá a zobrazuje počty načtení příspěvku.

Jak na page level cache

Pokud ovšem web, na kterém je takový kód přítomný, používá nějaký cachovací plugin, nebude nepřihlášenému uživateli (tedy většině) kód fungovat, jelikož se akce wp_head nespustí, nic se neuloží a zobrazovat se bude stále stejný počet zobrazení uložený spolu s celou stránkou ve statickém HTML.

Ovšem i tento problém lze celkem jednoduše vyřešit pomocí AJAXu:

<?php
add_action( 'wp_ajax_add_pageview', 'myawesome_pageview_add_pageview' );
add_action( 'wp_ajax_nopriv_add_pageview', 'myawesome_pageview_add_pageview' );

//Uložíme shlédnutí do databáze
function myawesome_pageview_add_pageview() {
    $post_id = $_REQUEST['post_id'];
    //získáme uložená data
    $pageviews = get_post_meta( $post_id, 'myawesome_pageview', true );
    //zvětšíme o jedno
    $pageviews = intval( $pageviews ) + 1;
    //znovu uložíme
    update_post_meta( $post_id, 'myawesome_pageview', $pageviews );
    die( $pageviews );
}

//funkce pro zobrazování počtu shlédnutí
function myawesome_pageview( $post_id = null ) {
    if ( false === empty( $post_id ) && true === is_singular() ) {
        $post_id = get_the_ID();
    } else {
        return;
    }
    echo '<div id="myawesome_pageviews">'.get_post_meta( $post_id, 'myawesome_pageview', true ).'</div>';
}

add_action( 'wp_head', function(){
    if ( ! is_singular() )
        return;
    $ajax_nonce = wp_create_nonce( "my-special-string" );
?>
<script type="text/javascript">
jQuery(document).ready(function($){
	var data = {
		action: 'add_pageview',
		security: '<?php echo $ajax_nonce; ?>',
		post_id: '<?php echo get_post_ID(); ?>'
	};
	$.post( '<?php echo admin_url( "admin-ajax.php" ); ?>' , data, function(response) {
		$( "#myawesome_pageviews" ).html( response );
	});
});
</script>
<?php
} );

Tohle řešení bude fungovat i s libovolným cachovacím pluginem. Ovšem nastane jiný problém. A to s výkonem. Ten sice existoval již předtím, nicméně zůstal skryt cachovacím pluginem.

Co se nyní děje při požadavku na wp-admin/admin-ajax.php

WordPress, k tomu aby mohl fungovat, musí načíst jádro, pluginy, šablony, options z databáze a další data. To neplatí jen a pouze o necachovaném pageview, ale také o AJAX requestech, které se v podobně POST requestu určitě necachují.

Takže každé načtení stránky na daném webu produkuje nyní dva requesty. Jeden cachovaný a druhý přímo mířící na origin – tedy váš server, kde se WordPress načítá s veškerou parádou. Takový AJAX request nebude blokovat načtení stránky, uživatel se dostane k obsahu teoreticky celkem rychle a na ten počet zobrazení v hlavičce článku si holt počká. Ale server bude přetížen.

Tohle je ta část problému, jejíž řešení je uvedeno v článku odkazovaném v úvodu. Nicméně to není vše.

Nekončící zápisy do databáze

Už zmiňované necachované requesty na server jsou špatné, ale je to ještě horší. Daný AJAX request nejen, že načítá data z databáze, on také data zapisuje. To znamená, že co načtení stránky, to zápis do databáze. V malých objemech to sever nejspíš ustojí, ale v případě nějakého spiku v návštěvnosti se ukáže, kdo používá MyISAM a kdo InnoDB jako úložiště dat pro MySQL.

Zatímco stále ještě nejpoužívanější MyISAM zamyká při zápisu celé tabulky, InnoDB zamyká jen řádky. InnoDB pomůže hlavně v tom, že administrátor či editoři nebudou mít problémy s vytvářením novým příspěvků, kvůli tomu, že by tabulka wp_post_meta byla zamknutá kvůli všem těch zápisům z pageviews pluginu. Ale problém to stejně neřeší.

Když jsem nakousl MySQL, tak cítím, že je vhodné také zmínit pro nefunguje cache, kterou má MySQL zabudovanou. MySQL cachuje dotazy a v případě identického dotazu dokáže vrátit data bez toho, aniž by dotaz vykonala. Ovšem MySQL tuto cache maže vždy, když dojde ke změně v relevantních tabulkách. Takže i jednoduché čtení z databáze se nám začne prodražovat.

Nepomůže změnit cahovací plugin

Věřím, že řada provozovatelů webu na WordPressu se začne poohlížet po lepším cachovacím pluginu. Neznám podrobně všechny page level cachovací pluginy, ale dokážu si představit že mažou cache pro stránku v případě updatu relevantní post_meta. Aspoň tak tomu je u různých object cache pluginů, které znám lépe.

Uživatel se dost možná dočte o něčem, čemu se říká object cache a zkusí nějaký ten redis či memcache nasadit. Ovšem kýženého výsledku se opět nedosáhne. Jednoduše proto, že object cache pro daný post, který obsahuje nejen údaje z tabulky wp_posts, ale také z tabulky wp_post_meta, se bude s každým zápisem do databáze mazat, tedy vždy, když někdo spustí daný AJAX request – tedy navštíví danou stránku. Takže vlastně pořád. To je opět stejný problém, jako s MySQL cachí.

Opět se vracím k problému a řešení popsaném ve zmiňovaném blogpostu. Ten správně odstranil AJAX request, ovšem plugin/šablona použitá na webu není tak “elegantní”, pokud se o eleganci ještě vůbec dá hovořit, a neprovádí čtení i zápis v rámci jednoho requestu, nýbrž ve dvou. “Zapisovací” request je v době psaní tohoto článku stále ještě aktivní a vesele zapisuje do databáze, znovu a znovu.

Správné řešení?

Jediné, co můžeme provést je celý takovýto návrh smést ze stolu.

Statistiky jako pageviews patří na systémy, které jsou na nápor dat připraveny. A můžeme si vybírat. Jmenujme aspoň Google Analytics, Piwik či Jetpack a jeho modul Stats.

Pluginy na zobrazování počtu shlédnutí daného příspěvku poté budou muset využít API zmíněných nástrojů a získat tak kýžená data. Jaké pluginy to jsou a jak obecně na to je nad rámec tohoto článku, jehož cílem je především poučit o tom, proč se s page/post views pluginy většinou spálíte a proč.

PS: Původně jsem nechtěl žádný plugin jmenovat a veškerý kód jsem napsal jen ilustračně (aniž bych jej testoval), nicméně když jsem zjistil, že plugin WP-PostViews má více jak 200000 stažení a jeho kód je velmi podobný tomu, co jsem zde nastínil, nedalo mi to. Schválně se na kód pluginu podívejte.

KSES odstraňuje zlé skripty

WordPress je distribuován s knihovnou KSES Švédského autora Ulf Harnhammar. Knihovna je HTML/XHTML filtrem implementovaným v PHP. Odstraňuje nežádoucí HTML elementy a atributy a provádí kontrolu jejich hodnot.

Knihovna slouží k zamezení Cross-Site Scripting (XSS) útokům. KSES přitom zdaleka není jediným, ani jediným dostačujícím, způsobem jak takovým útokům bránit. Nicméně poslouží velmi dobře v případech, kdy chceme povolit uživatelům či vzdáleným serverům poskytovat našemu kódu text s podporou HTML.

V případě WordPressu samotného je to například psaní příspěvků uživatelům s různým druhem oprávnění, či podpory HTML v komentářích.

Název “KSES” je rekurzivní zkratka zmanenající

KSES Strips Evil Scripts

, což v překladu znamená KSES odstraňuje zlé skripty. S rekurzivními zkratkami se ve světě open source a free software můžeme setkat poměrně často.

Knihovnu naleznete v adresáři wp-includes/kses.php.

Rozdíl mezi wp_kses a strip_tags

PHP vývojář se může ptát, proč by měl využívat wp_kses namísto strip_tags s povolenými HTML elementy. Důvod je jednoduchý. Funkce strip_tags, ačkoli umožňuje definovat které elementy nemají být z textu odstraněny, neumožňuje definovat které attributy jsou povoleny.

A to může být pro některé případy celkem problém. Jeden příklad za všechny. Takový element img pro obrázek, zdánlivě neškodný, má attribut onerror, který spouští javascript v něm definovaný v případě, že se obrázek nepodaří načíst. Následující kód tudíž nelze považovat za dostatečné zabezpečení:

$post_content = '<h1>Title</h1><p>Lorem ipsum sit dolor ament</p><img src="x" alt="haxxored" onerror="alert(this.alt)"/>';
echo strip_tags( $post_content, '<h1><p><img>' );

wp_strip_all_tags

Zmínil jsem jeden důvod, proč strip_tags není vhodné používat, proč tedy ve WordPressu nalezneme funkci wp_strip_all_tags?

Důvodem je, že strip_tags odstraňuje pouze html tagy, nikoli celé elementy včetně textu. Podívejte se na tento kód:

$post_content = '<p>Lorem ipsum</p>';
echo strip_tags( $post_content, '' ); //vypíše "Lorem ipsum"

Toto chování může být v řadě případů žádoucí, ovšem existuje případ, kdy tomu tak není. A sice, pokud vstupní text obsahuje element style či script. Jejich obsah většinou opravdu nechat vypsat nechceme. Ačkoli se ve většině takových případech nejdená o bezpečnostní chybu, rozhodně to není něco, čeho bychom úmysleně chtěli docílit. Proto WordPress nabízí funkci wp_strip_all_tags, která před samotným odstraňováním HTML tagů kompletně odstraní script a style elementy i s textem, který je uvnitř.

Funkce wp_kses a její parametry

Knihovna obsahuje celou řadu funkcí, ne všechny jsou ovšem vhodné k přímému použití. Jsou využity v rámci větších celků. Přímo použitelná je funkce wp_kses, případně některý z wrapperů (funkce volající funkci s předdefinovanými atributy), například wp_kses_post.

Parametry funkce wp_kses jsou “$string”, “$allowed_html” a “$allowed_protocols”, který je jako jediný nepovinný (výchozí hodnota je prázdné pole – array):

wp_kses( $string, $allowed_html, $allowed_protocols = array() )

$string

“$string” je text, který chceme filtrovat. Ve WordPressu samém je to typicky obsah příspěvku – post_content. Obsah příspěvku je, například, zkontrolován funkcí wp_kses_post před tím, než je uložen do databáze. Pokud jste si někdy všimli, že uživatel s nižším oprávněním než administrátor nemůže vkládat do příspěvku JavaScript, tak za to může právě sanitizace pomocí funkce wp_ksess.

V rámci pluginu bude uživatel chtít sanitizovat text/vstup získaný od uživatele či z odpovědi vzdáleného serveru před tím, než jej vypíše na stránku či uloží do databáze.

$allowed_html

Tento parametr určuje, jaká sada HTML elementů a atributů se má použít. Vývojář může v rámci vlastního volání funkce definovat svou vlastní sadu, nebo využít sady již předdefinované – takovým potom říkáme kontext.

Mezi kontexty, neboli předdefinované sady, patří “post”, “user_description”, “pre_user_description”, “strip”, “entities” a “data” (který je také defaultním kontextem). Zde se vracíme k funkci “wp_kses_post”, která jediné co dělá, je že za vývojáře předává hodnotu “post” parametru $allowed_html funkce wp_kses.

$allowed_protocols

Tento parametr vyjmenovává povolené protokoly pro atributy, jejichž hodnotou je URI (typicky, “href” či “src”). Cílem je v textu ponechat jen protokoly, které považujeme za bezpečné, a odstranit například výskyty protokolou “javascript:”.

Vlastní sada HTML

Vlastní sadu povolených HTML elementů a atributů definuje vývojář v podobě pole (array), jehož klíče označují HTML elementy a hodnotami jsou opět pole (array), kde jsou klíči jednotlivé attributy a hodnotou je boolean hodnota (true či false). Definovat false není nutné, někdy ovšem může sloužit k názornému zakázání daného atributu:

Vlastní sadu lze definovat podle následujícího vzoru, ve kterém povolíme, aby text obsahoval elementy “img” a “a”. Pro “img” povolíme atributy “src”, “alt” a “title”. Pro odkaz poté “href”, “title”, “rel”, “class” a “id”:

$allowed_html = array(
   'img' => array( //obrázek
        'src' => true, //zdroj
        'alt' => true, //alternativní popisek
        'title' => true, //titulek
    ),
   'a' => array( //odkaz
        'href' => true, //url
        'title' => true, //titulek
        'rel' => true, //vztah k odkazovanému dokumentu
        'class' => true, //třída
        'id' => true, //id
    ),
);
$text = wp_kses( $text, $allowed_html );

Je důležité mít na paměti, že KSES opravdu ponechá v daném HTML textu jen a pouze ty elementy a atributy, které jsou vyjmenovány. Takže například atribut “class” pro obrázek (“img”) bude nemilosrdně odstraněn. Stejně tak budou z textu předaném v atributu “$string” odstraněny všechny nadpisy, odstavce, tučné písmo, kurzíva … na co si jen vzpomenete a co jste zapomněli uvést v proměnné “$allowed_html”.

Odvozená vlastní sada

Ovšem abyste nemuseli vždy vypisovat celý seznam existujících HTML elementů a jejich atributů, lze využít funcke wp_kses_allowed_html, které lze předat kontext (například již zmíněnmé “post”) a funkce poté vrací seznam předdefinovaných elementů a jejich atributů. To lze poté využít k modifikaci defaultních nastavení a jejich následné využití pro vlastní účely:

$allowed_html = wp_kses_allowed_html( 'post' ); //defaultní sada pro příspěvek
$allowed_html['script'] = array( //přidáme element script
    'src' => true, //povolíme zdroj (atribut src)
    'type' => true, //a typ (type)
);
$allowed_html['a']['rel'] = true; //povolíme elementu "a" mít attribut "rel"
$text = wp_kses( $text, $allowed_html );

Ovšem pozor, pokud povolíme elemet script v neznámém textu, tak jsou poté všechny snahy o zamezení XSS útoku v atributech jiných prvků naprosto zbytečné – útočník nemusí hledat zadní vrátky, když jsou vchodové dveře otevřeny dokořán. Výše uvedený kód, povolující element script, tedy v žádném případě nepoužívejte.

Filtry

WordPress sám funkce wp_kses využívá na mnoha místech. Ukládání příspěvku do databáze již bylo zmíněno několikrát.

V případě, že bychom chtěli předefinované sady HTML při použití v interních procesech naší WordPress instalace rozšířit, nebo některou naopak zůžit, přijdou nám vhod filtry ve funkci wp_kses_allowed_html.

Říkám filtry, ale v zásadě se jedná jen o jeden filtr. A sice “wp_kses_allowed_html”.

Důležité je vždy zkontrolovat o jaký kontext je jedná. V případě, že vývojář definoval vlastní sadu HMTL, tak druhým parametrem předaným našemu callbacku bude string “explicit”, defaultní hodnotou je poté string “data” a další hodnoty jsou rezervovány pro jednotlivé kontexty, ve kterých je funkce využívána:

CUSTOM_TAGS

Pokud byste chtěli, můžete si definovat vlastní základní sady elementů a atributů v rámci wp-config.php.

V takovém případě je třeba, ideálně dle vzoru, definovat následující proměnné: $allowedposttags, $allowedtags, $allowedentitynames a nezapomenout definovat konstantu “CUSTOM_TAGS” s hodnotou “true”.

Závěrem

Knihovna KSES je velmi robustní a dává vývojáři velmi detailní kontrolu nad tím, jaké HTML elementy s jakými atributy se mohou v textu přijatém od uživatele či cizího serveru vyskytovat v případě, že chceme těmto zdrojům povolit používat HTML v jejich vstupu.

Taková kontrola před tím, než je s textem nějak naloženo – například je uložen do databáze či zobrazen na stránce, efektivně zamezí XSS útokům, které si mohou brát na paškál povolené attributy typu “onerror” u obrázků, či “onclick” u odkazů a podobně.

Každý vývojář WordPress pluginů a šablon by funkci wp_kses měl znát a hlavně využívat ve svém kódu.

Ovšem “wp_kses” není jedinou funkcí, která je třeba k zabezpečení uživatelského vstupu. Pokud nechcete čekat na to, až se k napsání dalšího článku na téma “esc_*” funkcí dostanu, přečtěte si, co o takových funkcích říká kodex.

Dokumentace Best Practice pro WordPress

WordPress dlouho postrádal aktivitu podobnou příručce PHP The Right Way, kterou má k dispozici komunita PHP vývojářů. Ještě nedávno platilo, a obávám se, že stále ještě platí a nějakou dobu platit bude, že kdo se chtěl ponořit do WordPressu, musel začít studiem kodexu, prokousávat se zdrojovým kódem (vedle tradičního trac je tento již nějakou dobu také na GitHubu, což potěší určitě ty, kteří SVN opovrhují) a studovat řadu tutoriálů, mezi kterými navíc musel vybírat ty, které jsou kvalitní, správné a aktuální. Situace se ovšem, a hodí se říci i naštěstí, pomalu mění k lepšímu.

Continue reading Dokumentace Best Practice pro WordPress