Introducerea a fost aici.
Repo se află aici.
Notă de început
Trebuie să menționez că o bună parte din cod este luat direct din Codex-ul WordPress. Scopul acestui articol nu este acela de a-ți oferi cod complet original ci acela de a-ți arăta un mod de lucru: ce să folosești, unde să cauți, cum să organizezi codul, cum să folosești clase șamd.
Nu uita, suntem pe forum ca să învățăm cu toții!
- Dacă ai nelămuriri, întreabă, oricât de ridicolă ți s-ar părea întrebarea. Nu o să râdă nimeni de tine, nu o să te ia nimeni la mișto, nimeni nu s-a născut învățat.
- Dacă observi vreo greșeală în codul meu sau dacă ai vreo idee mai bună de a face un anumit lucru, nu ezita să lași un comentariu!
- Dacă ai de gând să faci miștouri, să glumești, să lași un comentariu offtopic, abtine-te sau comentează aici.
Primii Pași
Primul lucru pe care îl avem de făcut este să facem un fișier numit index.php
în wp-content/plugins/book-review
și să adăugăm minimum de informație pentru ca WordPress să vadă fișierul ca pe un plugin:
<?php //index.php
/*
Plugin Name: Book Review
Author: Ionuț Staicu
Version: 1.0.0
*/
În continuare va trebui să facem o verificare pentru a preveni accesarea directă a fișierului plugin-ului:
//index.php
if (!defined('ABSPATH')) {
exit;
}
Localizare
Mai încărcăm și traducerile (un pas opțional dar util):
add_action('plugins_loaded', function () {
load_plugin_textdomain('book-review', false, dirname(plugin_basename(__FILE__)) . '/lang');
});
Și am terminat ce era mai greu
Facem o paranteză la traduceri pentru a menționa un lucru important ce poate scăpa ușor din vedere: numele fișierelor *.mo
& *.po
trebuie să urmeze următoarea structură: text-domain-lang_LANG
. În cazul nostru vor fi nevoie de fișierele book-review-ro_RO.mo
respectiv book-review-ro_RO.po
. Aceste două fișiere vor fi plasate în directorul lang
.
Pentru că vrem ca plugin-ul să fie ușor de tradus de oricine, vom folosi doar texte în limba engleză. Pentru traducere poți folosi Poeditor (web) sau Poedit (stand alone; versiunea gratuită funcționează foarte bine cu pattern-urile menționate aici)
Poți citi mai multe despre localizări aici
Încărcarea fișierelor
Pentru a include fișierele necesare, avem două posibilități: ori folosim un autoloader ori le includem manual. Pentru că vor fi doar câteva, nu are rost să ne complicăm, deci vom recurge la clasicul require
.
Git
Inițializăm și facem primul commit
git init
git add .
git commit -am "Initial commit"
Taxonomii și Post Types
Prima idee avută de mine a fost să țin totul în meta data, dar mi-am dat seama că unele informații se pot repeta: editură, autor sau genul cărții. În plus, aș fi limitat la altele: de exemplu n-aș putea afișa foarte ușor o arhivă cu toate cărțile citite sau nu aș putea afișa un widget.
Prin urmare, folosim un post type pentru cărți numit books
și taxonomii pentru editură, autor și gen. Restul de informații (anul apariției, ISBN, imagine etc) sunt destul de specifice fiecărei publicații. Anul ar putea fi pus într-o taxonomie, dar consider că ar încărca prea mult UI-ul și ar face mai dificilă o filtrare a publicațiilor apărute între două date.
Pentru că vom folosi numele taxonomiilor în mai multe locuri, vom defini constante:
//index.php
define('BOOK_POST_TYPE', 'book');
define('BOOK_TAX_GENRE', 'book_genre');
define('BOOK_TAX_AUTHOR', 'book_author');
define('BOOK_TAX_PUBLISHER', 'book_publisher');
Apoi le înregistrăm. Practic tot codul de mai jos este exemplul din documentație:
//inc/bookReview/PostTypes.php
<?php
namespace bookReview;
class PostTypes
{
public function __construct()
{
$this->registerPostType();
$this->registerGenre();
$this->registerAuthor();
$this->registerPublisher();
}
protected function registerPostType()
{
$labels = array(
'name' => _x('Books', 'post type general name'),
'singular_name' => _x('Book', 'post type singular name'),
'menu_name' => _x('Books', 'admin menu'),
'name_admin_bar' => _x('Book', 'add new on admin bar'),
'add_new' => _x('Add New', 'book'),
'add_new_item' => __('Add New Book'),
'new_item' => __('New Book'),
'edit_item' => __('Edit Book'),
'view_item' => __('View Book'),
'all_items' => __('All Books'),
'search_items' => __('Search Books'),
'parent_item_colon' => __('Parent Books:'),
'not_found' => __('No books found.'),
'not_found_in_trash' => __('No books found in Trash.'),
);
$args = array(
'labels' => $labels,
'public' => true,
'publicly_queryable' => true,
'show_ui' => true,
'show_in_menu' => true,
'query_var' => true,
'rewrite' => array('slug' => 'book'),
'capability_type' => 'post',
'has_archive' => true,
'hierarchical' => false,
'supports' => array('title', 'editor', 'author', 'thumbnail'),
);
register_post_type(BOOK_POST_TYPE, $args);
}
protected function registerGenre()
{
$this->registerTaxonomy(array(
'singular' => _x('Genre', 'taxonomy general name'),
'plural' => _x('Genre', 'taxonomy general name'),
'taxonomy' => BOOK_TAX_GENRE,
'isHierarchical' => true,
));
}
protected function registerAuthor()
{
$this->registerTaxonomy(array(
'singular' => _x('Writer', 'taxonomy general name'),
'plural' => _x('Writers', 'taxonomy general name'),
'taxonomy' => BOOK_TAX_AUTHOR,
'isHierarchical' => false,
));
}
protected function registerPublisher()
{
$this->registerTaxonomy(array(
'singular' => _x('Publisher', 'taxonomy general name'),
'plural' => _x('Publishers', 'taxonomy general name'),
'taxonomy' => BOOK_TAX_PUBLISHER,
'isHierarchical' => false,
));
}
protected function registerTaxonomy($options)
{
$labels = array(
'name' => $options['plural'],
'singular_name' => $options['singular'],
'search_items' => sprintf(__('Search %s'), $options['plural']),
'all_items' => sprintf(__('All %s'), $options['plural']),
'parent_item' => sprintf(__('Parent %s'), $options['singular']),
'parent_item_colon' => sprintf(__('Parent %s:'), $options['singular']),
'edit_item' => sprintf(__('Edit %s'), $options['singular']),
'update_item' => sprintf(__('Update %s'), $options['singular']),
'add_new_item' => sprintf(__('Add New %s'), $options['singular']),
'new_item_name' => sprintf(__('New %s Name'), $options['singular']),
'menu_name' => sprintf(__('%s'), $options['singular']),
'popular_items' => sprintf(__('Popular %s'), $options['plural']),
'separate_items_with_commas' => sprintf(__('Separate %s with commas'), $options['plural']),
'add_or_remove_items' => sprintf(__('Add or remove %s'), $options['plural']),
'choose_from_most_used' => sprintf(__('Choose from the most used %s'), $options['plural']),
'not_found' => sprintf(__('No %s found.'), $options['plural']),
);
$args = array(
'hierarchical' => $options['isHierarchical'],
'labels' => $labels,
'show_ui' => true,
'show_admin_column' => true,
'query_var' => true,
'rewrite' => array('slug' => $options['slug']),
);
register_taxonomy($options['taxonomy'], BOOK_POST_TYPE, $args);
}
}
Pentru că avem trei taxonomii ce au aceleași proprietăți, am făcut o metodă specială pentru a evita codul duplicat.
Genul cărții ne va permite să avem cărțile organizate într-o structură ierarhică (IT/Programare/Web/PHP). La autor și la editor nu este nevoie de așa ceva.
În index.php
va trebui să includem clasa și să o instanțiem:
//index.php
require_once 'inc/bookReview/PostTypes.php';
add_action('init', function () {
new bookReview\PostTypes;
});
register_activation_hook(__FILE__, function () {
new bookReview\PostTypes;
flush_rewrite_rules();
});
A doua instanțiere a clasei PostTypes
se face pentru a permite rescrierea permalinks. Detalii aici
Git
git add .
git commit -am "Added post types & taxonomies"
Metabox
// inc/bookReview/Metabox.php
<?php
namespace bookReview;
class Metabox
{
public function __construct()
{
add_action('add_meta_boxes', array($this, 'addMetaBox'));
add_action('save_post', array($this, 'saveMeta'));
}
public function addMetaBox()
{
add_meta_box('book_properties', __('Book Properties'), array($this, 'displayMetaBox'), BOOK_POST_TYPE, 'advanced', 'high');
}
public function displayMetaBox($post)
{
wp_nonce_field('book-review-nonce', 'book-review-nonce');
do_action('book-review/metabox/before-fields', $post);
echo $this->addFields($post);
do_action('book-review/metabox/after-fields', $post);
}
protected function addFields($post)
{
return implode("\n", $fields);
}
public function saveMeta($post_id)
{
if (!isset($_POST['book-review-nonce']) || !wp_verify_nonce($_POST['book-review-nonce'], 'book-review-nonce')) {
return;
}
if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) {
return;
}
if (isset($_POST['post_type']) && 'page' == $_POST['post_type']) {
if (!current_user_can('edit_page', $post_id)) {
return;
}
} else {
if (!current_user_can('edit_post', $post_id)) {
return;
}
}
$this->saveFields($post_id);
do_action('book-review/metabox/save', $post_id);
}
protected function saveFields($postID)
{
}
}
Facem o clasă ce adaugă un metabox pentru CPT-ul nostru, adaugă nonce-ul și pregătește terenul pentru adăugarea field-urilor necesare. În mare parte, și aici este codul luat tot din Codex.
După cum observi, am adăugat și câteva do_action
pentru a permite adăugarea de conținut extra din alte plugin-uri sau din functions.php
.
Git
git add .
git commit -am "Added metabox skeleton"
Câmpurile din Metabox
Pentru că o să tot adăugăm câmpuri, vom adăuga întâi o metodă ce ne va ajuta să generăm input
-uri și textarea
foarte ușor:
// inc/bookReview/Metabox.php
protected function getTextField($postID, $name, $label, $textarea = false)
{
$value = get_post_meta($postID, $name, true);
if ($textarea) {
$field = sprintf('<textarea name="%2$s" id="%2$s" class="widefat">%1$s</textarea>', esc_textarea($value), $name );
} else {
$field = sprintf('<input type="text" name="%2$s" id="%2$s" value="%1$s" class="widefat">', esc_attr($value), $name );
}
return sprintf('<p><label for="%s">%s: %s</label></p>', $name, $label, $field);
}
După care vom adăuga field-urile în metoda addFields
:
// inc/bookReview/Metabox.php @ addFields
$fields[] = $this->getTextField($post->ID, '_isbn', __('ISBN'));
$fields[] = $this->getTextField($post->ID, '_publish_year', __('Publish Year'));
$fields[] = $this->getTextField($post->ID, '_buy_book', __('Buying Links'), true););
Evident, nu ar trebui să uităm să adăugăm numele în metoda saveFields
!:
// inc/bookReview/Metabox.php @ saveFields
update_post_meta($postID, '_isbn', sanitize_text_field($_POST['_isbn']));
update_post_meta($postID, '_publish_year', sanitize_text_field($_POST['_publish_year']));
update_post_meta($postID, '_buy_book', wp_kses($_POST['_buy_book']));
Ce nume folosești pentru metafields?
Probabil ai observat un underscore în fața fiecărui nume al field-urilor. Este așa deoarece nu vrem ca aceste meta data să fie vizibile sau editabile în afara plugin-ului (detalii aici).
Git
git commit -am "Added basic meta fields"
Rating & Progres
Pentru că rating-ul și progresul sunt niște chestii foarte fixe, vom adăuga o metodă asemănătoare cu getTextField
dar care va genera un tag select
:
// inc/bookReview/Metabox.php
protected function getSelectField($postID, $name, $label, Array $values)
{
$storedValue = get_post_meta($postID, $name, true);
$options = array();
foreach ($values as $value => $text) {
$options[] = sprintf('<option value="%1$s"%2$s>%3$s</option>', $value, selected($storedValue, $value, false), $text);
}
$field = sprintf('<select name="%1$s" id="%1$s" class="widefat">%2$s</select>', $name, implode("\n", $options));
return sprintf('<p><label for="%s">%s: %s</label></p>', $name, $label, $field);
}
După care, în metoda addFields
adăugăm:
// inc/bookReview/Metabox.php @ addFields
$fields[] = $this->getProgress($post->ID);
$fields[] = $this->getRating($post->ID);
Metoda getProgress
va chema getSelectField
:
// inc/bookReview/Metabox.php
protected function getProgress($postID)
{
$values = apply_filters('book-review/metabox/progress-options', array(
"list" => __('On My List'),
"reading" => __('Currently Reading'),
"read" => __('Read'),
));
return $this->getSelectField($postID, '_book_progress', __('Book Progress'), $values);
}
Trecerea valorilor implicite prin apply_filters
va permite adăugarea de opțiuni ori dintr-un alt plugin ori din functions.php
.
Similar, avem și getRating
:
// inc/bookReview/Metabox.php
protected function getRating($postID)
{
$values = apply_filters('book-review/metabox/progress-options', array(
-1 => __('- Pick One -'),
1 => __('Bad'),
2 => __('Meh'),
3 => __('Mediocre'),
4 => __('Pretty good'),
5 => __('Awesome!'),
));
return $this->getSelectField($postID, '_book_rating', __('Book Rating'), $values);
}
Nu ar trebui să uităm să salvăm toate aceste câmpuri (în metoda saveFields
)::
// inc/bookReview/Metabox.php @ saveFields
update_post_meta($postID, '_book_progress', sanitize_text_field($_POST['_book_progress']));
update_post_meta($postID, '_book_rating', sanitize_text_field($_POST['_book_rating']));
Git
git commit -am "Added rating & book status meta fields"
Javascript
În mod normal, aș folosi pentru coperta cărții featured image. Dar pentru că vrem să învățăm lucruri, voi aborda cealaltă metodă: integrarea cu galeria WordPress-ului.
În plus, putem să extindem puțin toată povestea și să adăugăm mai multe imagini pentru o carte (copertă, câteva poze/scan-uri etc).
Va urma