ייבוא מ-WordPress ומתאם ייבוא

אשף הייבוא מ-WordPress בניהול, מנוע הייבוא wxr_import, קובץ המיפוי, ההרשאה import, ואיך כותבים מתאם ייבוא לסוג תוכן של אתר.

⏱ 10 דק' קריאה 1819 מילים

אתר שעובר מ-WordPress מביא איתו קובץ ייצוא אחד (WXR, "כלים > ייצוא" בניהול של WordPress). WIZZO CMS קורא את הקובץ, מראה מה יש בו, ומכניס את הרשומות לסוגי התוכן של האתר: פוסטים, עמודים, מוצרים וכל סוג מותאם. המדיה מועתקת לאתר, הכתובות בתוכן נכתבות מחדש, ולכל כתובת ישנה נכתבת הפניית 301.

בעמוד הזה system/ הוא תיקיית הליבה הפרוסה (api/core בקוד המקור). {admin} הוא CONFIG::$admin_url של האתר. המנוע זמין מליבה 5.0.152 והאשף מליבה 5.0.173, עם בדיקות ההרשאה שבעמוד הזה מליבה 5.0.176 (בדקו ב-system/core/version.txt).

שלוש דלתות, מנוע אחד#

דלתלמיאיפה
אשף הייבואמנהל האתרניהול > "ייבוא מ-WordPress" (הפאנל wp_import, כתובת {admin}/wp_import)
הכלים import_analyze, import_run, import_statusקרינגל או סקריפטagent_mcp, ראו נוהל לקרינגל
המחלקה wxr_importקוד של אתרsystem/libraries/wxr_import.php, הפעלים analyze(), run(), status()

שלוש הדלתות קוראות לאותו מנוע. ההבדל ביניהן הוא מי בודק הרשאות: האשף בודק את ההרשאות של המנהל המחובר, הכלים עובדים בשם מנהל הבוט של agent_mcp.

מה האשף עושה, צעד אחרי צעד#

  1. קובץ. העלאה במנות של 4MB (עד 512MB לקובץ), או כתובת של קובץ ייצוא. הקובץ נשמר בשם אקראי, וקובץ עם DOCTYPE או ENTITY נדחה לפני שמפענח XML נוגע בו.
  2. ניתוח. דוח שלא כותב שום תוכן: כמה רשומות מכל סוג, מה הסטטוסים, אילו קודים קצרים (shortcodes) ובלוקים של Gutenberg יש, מה לא יעבור, ולכל סוג תוכן מבנה מוצע עם השדות, השדות המותאמים (postmeta) ודוגמאות מהם.
  3. יעד לכל סוג. סוג תוכן קיים באתר, או סוג חדש שהאשף בונה מהמבנה המוצע (אחרי שהמנהל ערך שמות, סוגי שדות ומקורות). סוג שלא בוחרים לו יעד מדולג.
  4. מיפוי שדות. איזה שדה בטופס מקבל איזה מקור מ-WordPress. האשף ממלא ניחוש לפי שמות השדות, והמנהל מתקן.
  5. הרצה. מנה אחרי מנה, עם התקדמות חיה. אפשר לעצור ולהמשיך מאותה נקודה, גם אחרי סגירת הדפדפן.
  6. דוח סיום. כמה נוצרו, עודכנו, דולגו ונכשלו, לכל סוג, ולכל כישלון הסיבה שלו. מכאן אפשר למחוק את קובץ הייצוא מהשרת.

מה קורה לכל רשומה#

  • דרך הטופס. כל רשומה נשמרת דרך הטופס של סוג התוכן (admin_mcp_tools::save_record), כך שכל שדה עושה את העבודה שלו (תמונה עוברת דרך האחסון, עורך מנקה HTML). שכבת אירועי התוכן פולטת אירוע עם source=import, ויומן הביקורת רושם אותו תחת import.
  • סטטוס. הסטטוס של WordPress הופך ל-wz_status (סטטוס וטיוטות): publish הוא published, draft הוא draft, pending הוא review, future הוא scheduled, private הוא hidden. התאריך המקורי נכנס ל-wz_published_at.
  • מדיה. כל קובץ מהאתר הישן מועתק דרך STORAGE::add (עד 50MB לקובץ), והכתובות בתוכן מוחלפות בכתובות החדשות. כתובות מדיה מ-CDN נוסף נכתבות ב-media_hosts.
  • תוכן. הקודים הקצרים caption, gallery, embed, video ו-audio מומרים ל-HTML. קוד שעלול לרוץ באתר (סקריפט, on*, iframe ממקור לא מוכר) מנוקה, והרשומה מקבלת אזהרה שאומרת מה נוקה.
  • הפניות. לכל כתובת ישנה נכתבת הפניית 301 ב-CRM_redirections (ראו הפניות). הפניה לא נכתבת מכתובת שהאתר עונה עליה בעצמו: כתובת שתפוסה כבר בעמוד, השורש של בקר פעיל ב-CRM_modules (למשל /contact) או כל כתובת תחת system/. הרשומה מקבלת אזהרה שאומרת למה.
  • בלי כפילויות. הטבלה CRM_import_items שומרת לכל פריט מקור את היעד שלו. הרצה שנייה, או ייצוא חדש של אותו אתר, מעדכנת את מה שכבר יובא ולא יוצרת עותק.

סוגי המערכת של WordPress (attachment, nav_menu_item, revision וחבריהם) לא מיובאים כרשומות. הקבצים המצורפים מגיעים כמדיה של הרשומות שמשתמשות בהם.

מי רשאי#

  • פתיחת האשף היא בדיקת הפאנל הרגילה של הניהול (השורה wp_import בתפריט, ראו הרשאות).
  • ייבוא לסוג תוכן דורש את ההרשאה import על הפאנל של אותו סוג (ROLES::can("import", $panel)) והרשאת עריכה בפאנל (ADMIN::has_perms("edit", $panel)): קבוצה עם צפייה בלבד לא מייבאת. בתפקידים המובנים, לבעלים ולעורך יש אותה ולכותב אין. האשף מציג כיעדים רק סוגים שהמנהל רשאי לייבא אליהם, וכל פעולה בודקת שוב בצד השרת.
  • פרסום. מי שאין לו publish בפאנל מייבא אליו רק טיוטות ופריטים שממתינים לאישור. אם סומנו בהרצה סטטוסים אחרים, ההרצה לא מתחילה (no_publish) וההודעה אומרת מה לסמן.
  • שדות חסומים. שדה שנמצא ב-ROLES::fields_deny של הפאנל לקבוצה של המנהל לא מתמלא בייבוא, והרשומה מקבלת אזהרה. האשף מעביר את הרשימה ל-run() בארגומנט deny_fields ([panel => [field]]).
  • בניית סוג תוכן חדש (טבלה, פאנל, שורה בתפריט) מותרת רק לבעלים או למפתח.
  • יומן. כל ניתוח, כל סוג שנבנה, כל התחלה ועצירה של הרצה וכל סיום נרשמים ביומן הביקורת עם action=import.

קובץ המיפוי#

המיפוי נשמר על משימת הייבוא (CRM_import_jobs.mapping), ולכן הרצה שממשיכה לא צריכה לשלוח אותו שוב. האשף בונה אותו בשבילכם. בכלים ובקוד כותבים אותו כך:

{
  "types": {
    "post": {
      "adapter": "generic",
      "panel": "articles",
      "form_page": "insert",
      "auto": true,
      "url": "blog/{id}",
      "url_mode": "slug",
      "fields": { "title": "title", "text": "content", "image": "featured_image", "price": "meta:price" }
    },
    "page": { "adapter": "pages", "panel": "pages" },
    "revision_log": { "skip": true }
  },
  "statuses": ["publish", "draft", "future"],
  "media": true,
  "redirects": true,
  "media_hosts": ["cdn.example.com"]
}
מפתחמשמעות
adaptergeneric (ברירת מחדל), pages, או השם של מתאם של האתר
panelהפאנל של סוג התוכן ביעד
form_pageהמתודה של הפאנל שמחזיקה את הטופס, ברירת מחדל insert
autoברירת מחדל true: שדות שהמיפוי לא מזכיר מקבלים מקור לפי שם (title, text, image, tags...), ושדה מותאם שהשם שלו זהה לשם שדה בטופס נכנס אליו
urlתבנית הכתובת החדשה, לבניית ההפניות
url_modeslug (ברירת מחדל): הכתובת החדשה בנויה מה-slug. keep: הנתיב הישן כולו נשמר כ-SeoUrl
fields{שדה בטופס: מקור}
skiptrue מדלג על הסוג

המקורות#

מקורמה נכנס
title, content, excerptהכותרת, התוכן אחרי המרה וניקוי, התקציר
dateתאריך הפרסום המקורי
featured_imageהתמונה הראשית, מועתקת לאחסון
categories, tags, term:<taxonomy>שמות המונחים. שדה בחירה מקבל את האפשרות שהתווית שלה זהה לשם
authorהשם המוצג של הכותב
slug, link, wp_idה-slug, הכתובת הישנה, המספר ב-WordPress
seo_title, seo_descriptionהכותרת והתיאור של Yoast או של Rank Math
meta:<key>שדה מותאם (postmeta). תאריך בצורת YYYYMMDD (ACF) או חותמת זמן של יוניקס (תאריכי המבצע של WooCommerce) הופך לתאריך, ומספר בשדה קובץ הופך לקובץ המצורף
value:<טקסט>ערך קבוע לכל הרשומות

שדות מותאמים שמתחילים בקו תחתון#

מפתח שמתחיל בקו תחתון שייך בדרך כלל לתוסף עצמו, והניתוח לא מציע אותו. יוצאים מזה המפתחות המוכרים של WooCommerce ושל The Events Calendar: הניתוח מציע אותם כשדות בשם קריא, והמיפוי האוטומטי מחבר אותם לשדה בשם הזה כשהוא קיים בטופס:

מפתחשדה מוצעסוג
_price, _regular_price, _sale_priceprice, regular_price, sale_priceמספר
_sale_price_dates_from, _sale_price_dates_tosale_from, sale_toתאריך
_sku, _stock_status, _manage_stock, _virtual, _downloadablesku, stock_status, manage_stock, virtual, downloadableטקסט
_stockstockמספר שלם
_weight, _length, _width, _heightweight, length, width, heightמספר
_product_image_gallerygalleryטקסט: מזהי קבצים מופרדים בפסיק, גלריה של קבצים צריכה מתאם
_EventStartDate, _EventEndDateevent_start, event_endתאריך

כל מפתח אחר, גם עם קו תחתון, אפשר למפות במפורש עם meta:<key>. המפתחות של ACF שמתחילים בקו תחתון מחזיקים רק את ההפניה לשדה (field_...); הערך עצמו נמצא במפתח בלי הקו, והוא מוצע כמו כל שדה מותאם.

מתאם ייבוא לסוג תוכן של אתר#

רוב סוגי התוכן לא צריכים קוד: generic ממפה לפי שמות השדות בטופס, ומה שהוא לא מנחש נכתב ב-fields. כותבים מתאם כשהסוג צריך משהו שמיפוי לא נותן:

  • קטגוריות או כותבים בטבלה משלהם, כשהשדה שומר מספר ולא שם;
  • ערך שמחושב מכמה שדות (למשל מחיר המבצע כשהוא קיים, ואחרת המחיר הרגיל);
  • המרה של מבנה מיוחד בתוכן (קוד קצר של תוסף).

איפה ובאיזה שם#

הקובץ הוא application/libraries/wxr_adapter_<name>.php, המחלקה wxr_adapter_<name>, והיא יורשת את wxr_adapter_generic. המנוע טוען אותה לפי השם שבמיפוי ("adapter": "<name>"), ומחלקה או קובץ שחסרים הם שגיאה של ההרצה כולה ולא של רשומה אחת.

שלוש המתודות#

מתודהמחזירה
map_fields($item)[שדה בטופס => ערך] לרשומה אחת
taxonomy($item, $taxonomy, $field)הערך של שדה מונחים. $taxonomy הוא category, post_tag או אחר
author($item, $field)הערך של שדה כותב

$item הוא פריט אחד מהקובץ: id, type, status, title, content, excerpt, date, slug, link, author (שם המשתמש), meta (לכל מפתח, רשימת ערכים) ו-terms (רשימה של {taxonomy, slug, name}). $field הוא תיאור השדה מהטופס (name, type, select_options...).

במתאם זמינים: $this->fields (שדות הטופס לפי שם), $this->map (המיפוי של הסוג), $this->engine (המנוע: convert_content(), attachment_storage(), author_name(), item_lookup()), $this->warnings[] (אזהרה שתוצג על הרשומה בדוח), ו-$this->dry (הרצת ניסיון, בלי כתיבה).

דוגמה: מוצרים של WooCommerce#

<?php
// application/libraries/wxr_adapter_shop.php
class wxr_adapter_shop extends wxr_adapter_generic
{
	function map_fields($item)
	{
		$out = parent::map_fields($item);      // title, text, image, sku ושאר המיפוי
		$m = $item["meta"];
		$price = $m["_sale_price"][0] ?? "";
		if ($price === "") $price = $m["_regular_price"][0] ?? "";
		if ($price !== "") $out["price"] = (float)$price;   // המחיר בפועל: המבצע כשיש, אחרת הרגיל
		return $out;
	}

	// הקטגוריות של האתר בטבלה משלהן: השדה שומר מספרים
	function taxonomy($item, $taxonomy, $field)
	{
		$ids = [];
		foreach ($item["terms"] as $t)
		{
			if ($t["taxonomy"] !== $taxonomy) continue;
			$id = DB::get_val("shop_categories", ["slug" => $t["slug"]], "id");
			if ($id) $ids[] = (int)$id;
			else $this->warnings[] = "אין באתר קטגוריה " . $t["name"];
		}
		return $ids ?: null;
	}
}

ובמיפוי: "product": { "adapter": "shop", "panel": "products", "fields": { "categories": "term:product_cat" } }.

הרצת ניסיון
import_run עם dry_run: true מחזיר מה הטופס היה מקבל לשלוש הרשומות הראשונות, בלי לשמור ובלי להוריד מדיה. כך בודקים מתאם חדש לפני הרצה אמיתית.

מה לא לעשות במתאם#

  • לא לכתוב לטבלאות התוכן ישירות. הערך חוזר מ-map_fields, והטופס שומר אותו, כדי שהאירועים, הגרסאות והיומן יעבדו.
  • לא להוריד קבצים בעצמכם. attachment_storage() ו-convert_content() מורידים רק ממקורות האתר הישן, בגבולות הגודל, ונמנעים מכפילויות.
  • לא להחזיר HTML גולמי מהקובץ לשדה עורך. convert_content() הוא מה שמנקה אותו.

בניית סוג תוכן חדש#

כשאין באתר סוג שמתאים, האשף בונה אחד דרך content_type_builder (system/libraries/content_type_builder.php): הטבלה CRM_<t> והטבלה CRM_<t>_content עם עמודות הסטטוס, קובץ פאנל application/admin/<t>.php על content_type_panel, השורה ב-CRM_content_types (CONTENT::register) והשורה בתפריט הניהול. סוגי השדות האפשריים: text, editor, textarea, datetime, image, tags, url, number. שם שכבר תפוס באתר (טבלה או פאנל) נדחה.

הבנייה מחזירה גם רשומת מיפוי מוכנה (adapter: generic, השדות והמקורות שלהם), והאשף ממשיך איתה ישר למיפוי.

הפניות לסוג חדש

ההפניות נכתבות לכתובת לפי url (למשל products/{id}). הכתובת עונה באתר רק כשיש בקר אתר לחלק הראשון שלה (שורה ב-CRM_modules). content_type_builder::build() מחזיר אז front_ready: false, וההפניות נשמרות ומחכות לבקר שיבנו בשביל הסוג.

התיעוד נכתב מתוך הקוד של ליבה 5.0.185.