WordPress hook

WordPress egyedi felhasználói szintek és admin felületre belépés utáni alapértelmezett oldal megváltoztatása

WordPress egyedi felhasználói szintek és admin felületre belépés utáni alapértelmezett oldal megváltoztatása 960 600 bacsoa

Egyedi felhasználói szintet kellett létrehozzak és mivel ennek a felhasználó típusnak egy csomó mindent nem szabad látnia, kézen fekvő volt máshova irányítani őt bejelentkezés után, hogy ne is keresgéljen a WordPress vezérlőpulton.

A WordPress admin felületen (hívhatnám egyszerűsítve backendnek is, de azért ez nem teljesen azt jelenti) egy külön világ van, mint a frontenden (amit a látogató lát), külön CSS beállítással, sokszor elkülönített lekérdezésekkel és sok minden mással is. Aki bele akar mélyedni a felhasználói szintekbe, az itt tudja ezt megtenni.

Első lépésben csináljunk egy új felhasználói szintet. Ehhez az add_role metódust használjuk, aminek három paramétere van:

  • egyedi azonosító slug (ékezetek és szóköz nélkül), itt organizer
  • felhasználó szint neve, ez jelenik majd meg az admin felületen a felhasználóknál, itt Szervező
  • jogok, vagyis képességek, hogy az adott felhasználó mihez férhet hozzá, itt az adminisztrátori jogokat kapja meg. Ez egy elég bonyolult képességfa, akit érdekel az összes, az erre nézelődjön
add_role(
    'organizer',
    __( 'Szervező'  ),
    get_role( 'administrator' )->capabilities
);

A fenti kód csinál egy ‘organizer’ nevű felhasználói szintet, aki gyakorlatilag egy adminisztrátori jogkörrel (aka képességekkel) rendelkező felhasználótípus.

Sok bővítménnyel, vagy egyedi eszközzel meg lehet azt csinálni, hogy mit lásson ez a felhasználó és mit sem, erre jó például a remove_menu_page, vagy a remove_submenu_page metódusok, de egy sima admin oldali inline CSS-sel is el tudunk tüntetni szinte minden menüt attól függően, hogy ki van bejelentkezve.

$user = wp_get_current_user();
$roles = ( array ) $user->roles;
$role = $roles[0];

// Megvizsgálja, hogy a bejelentkezett felhasználó típusa 'organizer' és ha igen, akkor az azon belüli rész fut le
if ( $role == "organizer" ) {

	// Elrejti a felső fekete adminisztrációs sávot
	show_admin_bar(false);

	// Beállítja az alább megadott inline CSS-t, az admin_head action csak az adminisztrációs felületen érvényes CSS stíluslapokat állítja be, tehát ennek nincs határsa a frontenden lévő sablonra
	add_action('admin_head', 'my_custom_css');
	function my_custom_css() {
	  echo '<style>
			li#menu-media, li#menu-settings {display:none;}
	  </style>';
	}

	// Ez teszi lehetővé, ha a bejelentkezett felhasználó 'organizer' típusú, akkor az itt található két speciális metódusban lévő dolgok fussanak le
	add_action( 'admin_init', 'stop_access_profile_organizer' );
	add_action( 'admin_enqueue_scripts', 'load_defaults_prevents_organizer' );

}

// Ez a rész megtiltja a hozzáférést néhány admin felületen található oldalhoz. Az előző rész csak elrejti CSS segítségével a menüpontokat, ez viszont azt akadályozza meg, ha valaki szemfüles és tudja az URL-eket fejből és oda próbál menni, akkor egy hibaüzenet kíséretében megtiltja azt
function load_defaults_prevents_organizer($hook)
{
	if ( $hook=="options-general.php" || $hook=="options-writing.php" || $hook=="options-reading.php" || $hook=="options-discussion.php" || $hook=="options-media.php" || $hook=="options-permalink.php" || $hook=="tools.php" || $hook=="widgets.php" || $hook=="customize.php" || ($hook=="post-new.php" && $_GET['post_type']=="page") || $hook=="themes.php" ) {
		wp_die( 'Nincs jogod megtekinteni ezt az oldalt.' );
	}
}

// Ezek le fogják szedni az admin felületről a bal oldalon megjelenő menüpontokat, az első a Megjelenés / Sablonok részt, a második az Oldalak menüt fogja eltüntetni
function stop_access_profile_organizer()
{
    remove_menu_page( 'themes.php' );
    remove_menu_page( 'edit.php?post_type=page' );
}

Nézzük azt a részt, hogy mit csináljunk az ‘organizer’ típusú felhasználókkal, miután sikeresen bejelentkezett. Mondjuk a Hozzászólások részhez. Ez azért jó, mert így nem egyből a Vezérlőpult részre kerül (ahova alapból mindenki), ahol még vagy 100 minden WordPress telepítésnél más és más részt kell elrejteni, hanem oda, ahova mi akarjuk:

add_action('wp_login', 'ba_new_dashboard_home', 10, 2);
function ba_new_dashboard_home($username, $user)
{
  if (array_key_exists('organizer', $user->caps)) {
    wp_redirect(admin_url('edit-comments.php', 'http'), 301);
    exit;
  }
}

Ha nem akarunk annyit inline CSS-ezni, akkor hasznos lehet, ha az ‘organizer’ típusú felhasználók esetén az admin oldal body elem osztályát kiegészítjük valamivel, így könnyebb lesz rá hivatkozni. Erre van egy speciális filter, az admin_body_class nevű. Ez teljesen ugyanazt csinálja, mint a body_class nevű filter, csak míg utóbbi a frontenden módosítja a body tag osztályait, előbb az admin felületen csinálja ugyanezt.

// Hozzáad egy egyedi osztályt a body taghez, ha egy organizer típusú felhasználó van bejelentkezve

add_filter( 'admin_body_class', 'ba_admin_body_classes' );
function ba_admin_body_classes( $classes )
{  
	$user = wp_get_current_user();
	$roles = ( array ) $user->roles;
	$user_role = $roles[0];

	if ( $user_role == 'organizer' ) {
		$classes .= ' logged_organizer ';
	}

  return $classes;
}

Ez a pár hasznos kódrészlet kiválóan alkalmas arra, hogy 2-5 féle plugin feltelepítése helyett saját kóddal, elegánsan és gyorsan oldjuk meg pontosan azt a problémát, amit akarunk, és ne szemeteljük tele a WordPress oldalunkat szuper, mindent megoldó bloatware pluginokkal.

WordPress REST API egyedi mező hozzáadása a /posts végponthoz

WordPress REST API egyedi mező hozzáadása a /posts végponthoz 1000 563 bacsoa

Azért jó fullstack fejlesztőnek lenni, mert mindenből kapsz egy kicsit. Imádom ezt a munkát, mert változatos, minden nap új kihívással. A mai menü egy meglévő WordPress REST API bővítés. A feladat csak annyi, hogy egy már létező szabvány WordPress REST API végponthoz egy új mezőt kellett létrehoznom.

Akit a WordPress saját REST API-ja érdekel, az itt mindent megtud róla. Aki nem tudja, mi az a REST, az pedig innen induljon, vagy esetleg a Wikipedia oldalon. Röviden egy olyan egységes interfész, amelynek különböző elveknek és követelményeknek kell megfelelnie, nekünk fejlesztőknek pedig alkalmazkodnunk kell hozzá. Ez jó, mert így egy elfogadott szabványhoz kell programozni, amit mindenki (el)ismer.

A lényeg, hogy most egy Cikk / Bejegyzés (post) részletes végpontjába fogunk beszúrni plusz mezőt. Mihez is kell ez? Például alkalmazásfejlesztéshez, vagy bármilyen olyan külső alkalmazásokhoz, ami REST-alapon működik és JSON-ban várja az adatokat, majd azt dolgozza fel.

A cikk részletes WordPress REST API URL alapértelmezett URL-je a következő (nyilván a https://domain.hu az a te weboldalad tetszőlegesen behelyettesíthető URL-je), a 999 pedig a post_id (bejegyzésed WordPress-ben tárolt egyedi azonosítója) jelöli:

https://domain.hu/wp-json/wp/v2/posts/999

Ide akarunk egy plusz mezőt, mondjuk a szerző nevét. Alapból ugyanis csak a szerző (author) user_id-ja szerepel itt, de miért ne szeretnénk egyből a szerző nevét megjeleníteni? Abból is mondjuk a display_name részben beállított értéket. Nyilván ez tetszőlegesen változtatható, bővíthető. Az én megoldásom:

add_action( 'rest_api_init',  'exibio_register_custom_field' );
function exibio_register_custom_field() {

    // Register author_name field

    register_rest_field( 'post', 'author_name',
        array(
            'get_callback'          => 'get_author_name',
            'show_in_rest'          => true,
            'auth_callback'	        => 'permission_check',
        )
    );

	function get_author_name( $post,  $field_name, $request ) {
		
		$post_id = (int) $post['id'];
		$user_id = (int) $post['author']; // author user_id
		$author = get_userdata( $user_id ); // author object
		$user_name = (string) $author->display_name;
	    
	    return $user_name;

	}

}

Először inicializáljuk a rest_api_init nevű hook-ot, majd hozzáadunk egy új author_name nevű mezőt (ezen a néven fogjuk megtalálni a /v2/posts végpontban lévő JSON-ban) és végül megmondjuk, hogy az új mezőben mi szerepeljen. (get_author_name nevű függvény). A függvény a felhasználó nevével fog visszatérni (azon belül is a display_name, azaz a Nyilvánosan megjelenő névvel), ami a WordPress-ből már ismert user object-ből származik.

Rövid és eredményes kódolást!

Az egyik leghasznosabb ármódosító WooCommerce metódus, a wc_price()

Az egyik leghasznosabb ármódosító WooCommerce metódus, a wc_price() 1600 700 bacsoa

Minden héten szembejön valami, de amivel ma találkoztam, azt muszáj megosztanom, mert ugyan nekem új volt (mint a múltkori Chrome Easter Egg), de attól még lehet, hogy másnak is az lesz és az egyik leggyakrabban használt dologgal kapcsolatos: a termékek árával.

A feladat, hogy változtassunk meg egy termék árát bizonyos feltételek mentén, majd írjuk ki az eredeti és a módosított árat is. A mostani példában annyi a feladat, hogy a bejelentkezett felhasználók kapjanak 10% kedvezményt.

Ez a bejegyzés nyilván nem a pluginokról fog szólni, habár biztos meg lehetne oldani legalább két különbözővel 🙂

A következő két WooCommerce hookra van szükségünk:

  1. woocommerce_get_price
  2. woocommerce_get_price_html

Az első magát az árat, mint numerikus értéket állítja, a 2. pedig a megjelenített, tehát lerenderelt HTML kimenetet. Igazából ha csak a desszert érdekel, akkor a 2. pontot elhagyhatod, de azért olvass tovább, hátha érdekel a köret is.

Először nézzük az 1. példát, állítsuk be az árat:

add_filter('woocommerce_get_price', 'exibio_return_custom_price', 10, 2);
function exibio_return_custom_price( $price, $product ) {

    $post_id = $product->get_id(); # a termék azonosító (post_id)
    $user_id = get_current_user_id(); # a felhasználói azonosító (user_id)
    $original_price = $price; # az eredeti ár

    if ( is_user_logged_in() ) {
        $discount = $price / 100 * 10; # a kedvezmény mértéke: 10%
        $price -= $discount; # a kedvezménnyel csökkentett új ár
    }

    return $price;

}

Igazából a post_id és  a user_id nem kell ehhez a feladathoz, de odaírtam, hátha valaki csak egy konkrét termékre, vagy felhasználóra akarja megadni a kedvezményt. A fenti hook módosítja a WooCommerce terméknek admin felületen beállított árát. A megoldás ennyi, viszont nem ez volt a heti újdonság, hanem az, hogy szeretném kiírni az új árat úgy, hogy mellé/fölé kiírom az eredeti árat.

Ehhez kell a 2. hook, a woocommerce_get_price_html.

A probléma azzal van, hogy az árat numerikus (float azaz lebegőpontos) formátumban kapjuk meg, majd  formázottan szeretnénk megjeleníteni, pl. ezres elválasztóval, devizanem megjelenítéssel, stb. Tehát bejön ez:

15000

És ezt szeretnénk kiírni:

15 000 Ft

Ekkor jön az, hogy az a számot elkezdjük a jól ismert number_format függvénnyel alakítgatni, majd hozzátesszük a devizanemet, de ez elég macerás (újabb WooCommerce függvény kell hozzá, amivel a beállított devizanemet is le kell kérni a get_woocommerce_currency() metódussal). Plusz, ha esetleg kétnyelvű a webáruházunk és az egyikben HUF-nak, a másikban Ft-nak hívjuk a devizanemet, vagy esetleg eleve két különböző devizanem van, akkor még tovább kell bonyolítani. Arról nem is beszéltem, hogy néha £400 kell kiírnunk, nem pedig 400 £, ugye.

Ezt az egész problémát megoldja egy lépésben a wc_price nevű metódus. Egyetlen paramétert vár, ez pedig az ár numerikus (lebegőpontos vagy integer) formátumban, majd szépen visszaadja a WooCommerce-ben beállított formátumú devizában, ezres elválasztó karakterrel, szépen HTML formátumban.

add_filter( 'woocommerce_get_price_html', 'exibio_return_custom_price_html', 10, 2 );
function exibio_return_custom_price_html( $price, $product ) {
    
    $post_id = $product->get_id();
    $user_id = get_current_user_id();
    $original_price = (float) get_post_meta( $post_id, '_price', true ); // az eredeti ár numerikus formátumban

    if ( is_user_logged_in() ) {
        
        // az eredeti adatbázisban tárolt ár formázva, mielőtt még a 10% kedvezményt megadtuk volna
        $output = "<abbr class='original_price_label'>Eredeti ár:</abbr> <del class='original_price' aria-hidden='true'>" . wc_price($original_price) .  "</del>";
        
        $output .= "<abbr class='discount_price_label'>Akciós ár 10% kedvezménnyel:</abbr>";

        // ez pedig már a 10%-kal csökkentett / módosított ár        
        $output .= $price;
        $price = $output;
    }

    return $price;

}

A woocommerce_get_price előbb fut le, mint a woocommerce_get_price_html hook. Az eredeti ötletet innen nyúltam.

Összegezve, röviden így működik. Nézzük ezt a számot, mint eredeti ár:

15000

Hívjuk meg a wc_price metódust:

echo wc_price( 15000 );

Majd megkapjuk a generált WooCommerce formátumú HTML kimenetet:

<span class="woocommerce-Price-amount amount">
	<bdi>15 000&nbsp;<span class="woocommerce-Price-currencySymbol">Ft</span></bdi>
</span>

Örülünk Vincent. Rövid és eredményes kódolást!

WooCommerce rendelés utáni item meta adatok mentése, hook különbségek

WooCommerce rendelés utáni item meta adatok mentése, hook különbségek 1440 960 bacsoa

Ma futottam bele egy érdekes problémába, gondoltam megosztom másokkal is, mert szerintem elég nagy a zavar a fejekben – legalábbis az internet népét és a Stackoverflow felhasználóit tekintve.

Adott egy WooCommerce (4.8, de amúgy szerintem majdnem mindegy). A feladat, hogy a WooCommerce-ben leadott rendelés után hajtsunk végre valamit.

Erre több lehetőségünk is van. Mivel a WooCommerce rendelés gyakorlatilag egy egyedi WordPress bejegyzés (shop_order), ezért van neki post_status mezője. Ezeknek a szabványos WordPress bejegyzésekhez hasonlóan többféle státusza van, sőt lehet neki egyedit is adni. Általában alapértelmezetten a wc-processing (feldolgozás alatti) státuszt kapja meg. Egy SQL lekérdezéssel ellenőrizhetjük, hogy egy adott WooCommerce webáruházunkban hány fajta státuszú rendelés létezik:

SELECT post_status
FROM `wp_posts` 
WHERE `post_type` = 'shop_order' 
GROUP BY post_status

A Woocommerce rendelés státuszokról és magáról a Order objektumról itt olvashatsz bővebben.

Azért kellett ez a rövid bevezető, mert nem mindegy, hogy a rendelések mely státuszánál, mi fusson le. A Woocommerce erre többféle beépített hook-ot kínál. Mi az a hook? Ez gyakorlatilag egy callback. Mi az a callback? Na igen, ez az amikor 15 dolgot el kell magyarázni, hogy a végén egy szót megérts 🙂 A Callback-ről magyarul itt olvashatsz bővebben. A WordPress hook-okról pedig itt.

A rendelés leadása utáni tranzakciókezelésre a Woocommerce többféle hook megoldást is nyújt, például ezeket:

  1. woocommerce_checkout_order_processed
  2. woocommerce_payment_complete
  3. woocommerce_thankyou

A teljes listát itt találod.

Több, eltérő leírás van erről a neten, sőt a Woocommerce verziók között is vannak eltérések. Arra is kell vigyázni és ezt sokan elrontják, hogy minden hook-nak eltérő mennyiségű paramétere van. Ha többet adsz át az nem gond, de ha kevesebbet, akkor le sem fut. Sajnos vannak olyan hook-ok, amik nem dobnak hibát, csak egyszerűen nem működnek. Az alábbi példában például a 10, 3 a végén azt jelenti, hogy 3 paramétert vár és 10 prioritással fusson le. Mivel többet is egymásba ágyazhatsz, és az egyik felülírhatja a másikat, hogy ne legyen olyan egyszerű az élet. Főleg akkor jó ez, ha egy másik plugin már használja valami unintelligens 99999 prioritással és te meg nem jössz rá, hogy a tiéd miért nem működik… 😀

Amibe ma belefutottam, az egy elég egyszerűnek tűnő probléma. Én általában a woocommerce_checkout_order_processed hook-ot használom. Ez három paramétert vár. Az 1. a Woocommerce rendelés azonosító (wc_order_id), a 2. az elküldött személyes adatok (posted_data), a 3. pedig maga a rendelés objektum (order class):


add_action( 'woocommerce_checkout_order_processed', 'exibio_woocommerce_order_processed', 10, 3 );
function exibio_woocommerce_order_processed( $wc_order_id, $posted_data, $order ) {
    // ez fut le a rendelés leadása után
}

Ez a hook akkor fut le, ha a rendelés a processing státuszára már átváltott (tehát a wc-processed után). Sajnos vannak olyan bővítmények, amelyek rosszul használják a rendeléshez tartozó és elmentett item meta data, azaz a rendelés tétel meta adatok (ezek általában a rendeléshez tartozó egyedi mezők, paraméterek, akár a felhasználók által átadott változók, stb.) elmentését és nem a woocommerce_checkout_order_processed hook-ot használják, hanem a woocommerce_thankyou hook-ot. Nem ugyanarra való, de sajnos sokan összekeverik vagy helytelenül használják.

 

Emiatt hiába vártam egy plugin által összegyűjtött mezőit a rendelés meta adatait között, az csak a rendelés leadása utáni pillanatban vált elérhetővé. Ez azért volt, mert a plugin nem a processed utáni pillanatban mentette el a meta adatokat, hanem a thank you endpoint utáni pillanatban. Ez azt okozta, hogy a hook utáni iterálásnál on-the-fly hiába kérdeztem le az order_itemmeta táblát, nyoma sem volt a plugin által elmentett adatoknak.

Először a woocommerce_checkout_order_processed hook után egy sima SQL lekérdezést használtam (az item_id egyébként az order objektumon belüli változó). Az $items objektum iterálása SQL lekérdezéssel:

global $wpdb;
$items = $order->get_items();
foreach ($items as $item_id => $item) {
$sql = "
	SELECT meta_value
	FROM wp_woocommerce_order_itemmeta
	WHERE `order_item_id` = {$item_id}
	AND (meta_key = '_sumo_pp_payment_id')
";

$result = $wpdb->get_results($sql, ARRAY_A);
if ( !empty($result) ) {
	$sumo_pp_payment_obj = $result[0];
	$sumo_pp_payment_id = (int) $sumo_pp_payment_obj['meta_value'];
}

}

Ugyanez, dedikált Woocommerce függvénnyel lényegesen egyszerűbb, de egyik sem adott vissza találatot.

$items = $order->get_items();
foreach ($items as $item_id => $item) {
	$sumo_pp_payment_id = (int) wc_get_order_item_meta( $item_id, '_sumo_pp_payment_id', true );
}

Sehol semmi. Aztán amikor már lezajlott a rendelésleadás (és lefutott a woocommerce_thankyou hook filtere is, ami kb 1 mp múlva már megtörtént), akkor már ott volt a _sumo_pp_payment_id nevű rekord tartalma. Tanulság: ha legközelebb belefutsz egy ilyenbe és nem találod az adott meta mezőt, próbáld ki mindhárom hook-ot, hátha az adott megoldásban az lesz a nyerő.

Ajánlott irodalom:

  1. Egy WooCommerce hiba, amikor a woocommerce_checkout_order_processed hook előbb lefut, mint kéne: https://github.com/woocommerce/woocommerce/issues/28113
  2. A woocommerce-checkout-order-processed miért nem működik? https://stackoverflow.com/questions/45997729/any-idea-why-woocommerce-checkout-order-processed-not-firing-when-user-is-not-lo
  3. A woocommerce_thankyou hook használata https://stackoverflow.com/questions/43301940/woocommerce-thankyou-hook-not-working
  4. A woocommerce_payment_complete értelme, vagyis amikor az a fontos, hogy mikor van kifizetve egy rendelés: https://stackoverflow.com/questions/46686075/woocommerce-checkout-order-processed-hook-executing-function-twice