כשסוג תוכן מופעל עם versions, כל שמירה שלו משאירה גרסה: צילום של הרשומה אחרי השמירה, מי שמר, מאיפה ומה השתנה. מכל גרסה אפשר לחזור, וגם רשומה שנמחקה חוזרת עם אותו id. המחלקה שעושה את זה היא CONTENT_VERSIONS, והיא מאזינה של שכבת אירועי התוכן: היא לא יודעת איזו דלת שמרה, ולכן הגרסאות נשמרות מטופס, מעריכה מהירה, מעריכה חיה, מ-MCP ומייבוא באותה צורה.
בעמוד הזה system/ הוא תיקיית הליבה הפרוסה (api/core בקוד המקור), ו-{admin} הוא CONFIG::$admin_url של האתר. הגרסאות זמינות מליבה 5.0.138 (בדקו ב-system/core/version.txt), עם תיקונים ב-5.0.141. מגרסה 5.0.156 יש ב-panel_table מגירת היסטוריה לכל שורה ולשונית "נמחקו לאחרונה".
להפעיל גרסאות לטבלה#
טבלה שומרת גרסאות כשהיא רשומה כסוג תוכן עם versions => 1. הרישום נעשה בקוד הפאנל:
class ADMINMODULE_Events extends bgl_controller
{
public function index()
{
CONTENT::register("events", [
"label" => "אירועים",
"panel" => "Events",
"versions" => 1,
"keep_versions" => 100,
"title_field" => "title",
]);
// ... panel_table / Form כרגיל
}
}
האפשרויות של register() מפורטות ב-שכבת אירועי התוכן. כאן חשובות שלוש: versions מפעילה, keep_versions קובעת כמה גרסאות נשארות, ו-title_field נותנת כותרת לרשומות ברשימת הגרסאות וב"נמחקו לאחרונה". טבלה שלא נרשמה לא נוגעים בה: אין גרסאות ואין עלות.
מתי נשמרת גרסה#
| מצב | מה קורה |
|---|---|
| שמירה ראשונה אחרי שהגרסאות הופעלו | נשמרת קודם גרסת בסיס (baseline, מספר 0, בלי מנהל) עם הרשומה כפי שהייתה לפני השמירה, ואחריה גרסת השמירה. כך גם תוכן שנכתב לפני ההפעלה יש לו דרך חזרה |
| שמירה שלא שינתה כלום | לא נכתבת גרסה |
שמירה אוטומטית (autosave) של אותו מנהל באותה רשומה, עד 10 דקות אחרי שמירה אוטומטית קודמת | מחליפה את הגרסה האוטומטית האחרונה ולא מוסיפה חדשה |
| יצירה | גרסה ראשונה, בלי גרסת בסיס |
| מחיקה | גרסה אחת מסוג delete שמחזיקה את כל השפות, תחת lang_id שווה 0 |
| שחזור | גרסה חדשה מסוג restore, כדי שאפשר יהיה לבטל גם אותו |
| טיוטה שפורסמה | גרסה מסוג publish, ראו סטטוס וטיוטות |
| ייבוא | גרסה מסוג import |
גרסאות נשמרות לכל שפה בנפרד: שמירה בעברית ושמירה באנגלית הן שתי רשומות היסטוריה עם מספרי גרסה משלהן (version_no רץ לכל טבלה, רשומה ושפה).
CRM_content_versions#
| עמודה | טיפוס | משמעות |
|---|---|---|
id | bigint, PK | מזהה הגרסה |
tbl | varchar(64) | שם הטבלה בלי CRM_ |
row_id | int | ה-id של הרשומה |
lang_id | int | השפה. 0 בגרסת מחיקה |
version_no | int | המספר הרץ של הגרסה ברשומה ובשפה. גרסת בסיס היא 0 |
kind | varchar(12) | baseline, save, autosave, restore, publish, import או delete |
data | mediumtext | הרשומה כ-JSON: {"main": {...}, "content": {...}}. ערכים רגישים מוסתרים ("***") כבר באירוע |
changed | varchar(1000) | שמות השדות ששונו, בפורמט |a||b| (מופרדים ב-||, נחתך בשלמות שדה) |
admin_id | int | המנהל ששמר. 0 בגרסת בסיס ובשמירה שאין מאחוריה מנהל |
source | varchar(16) | הדלת (form, inline, mcp, import, restore...) |
note | varchar(255) | הערה, למשל "שוחזר מגרסה 12" |
created_at | datetime | מתי נשמרה |
אינדקס מורכב על (tbl, row_id, lang_id, id) משרת את ההיסטוריה של רשומה, ואינדקס על created_at את הניקוי. הטבלה מגיעה לאתר ב-update_db של עדכון הגרסה (עדכון ליבה). בלי הטבלה כל המתודות מחזירות תשובה ריקה ושום שמירה לא נפגעת: CONTENT_VERSIONS::ready() אומרת אם היא קיימת.
עמודה שהוסתרה ("***") לא נכתבת חזרה. סיסמה או מפתח שהשתנו אחרי הגרסה נשארים כפי שהם עכשיו. כמו כן id, ord וכל עמודה שמתחילה ב-wz_ (סטטוס הפרסום) לא חוזרים, וגם לא טבלאות צד של שדות: תגיות SEO, כתובת ידידותית, גלריות ותגיות שנשמרות בטבלאות נפרדות. השחזור מחזיר את הטבלה הראשית ואת טבלת _content בלבד.
שחזור#
שחזור הוא שמירה חדשה שמחזירה את הערכים של גרסה. הוא עובד בשלושה מקרים:
- רשומה קיימת. העמודות של הגרסה נכתבות מעל הרשומה, לכל שפה שהגרסה מחזיקה. מה שלא השתנה לא נכתב. אם שום דבר לא השתנה התשובה היא
unchanged. - רשומה שנמחקה, מגרסת מחיקה או מכל גרסה שלה. הרשומה נוצרת מחדש עם אותו
idועם כל השפות שהגרסה מחזיקה. אם ה-idכבר תפוס במשהו אחר, השחזור נכשל עםid_taken. - גרסה של רשומה קיימת שהיא גרסת מחיקה. נכשל עם
id_taken(הרשומה חזרה כבר).
אחרי שחזור נשלח אירוע restore (ולכן נכתבת גרסת restore וגם שורה ביומן, ראו יומן ביקורת), ודף הרשומה (preview_url, ואם יש כתובת SEO גם היא) יוצא מה-cache של הדפים.
קוד שעושה שחזור:
$res = CONTENT_VERSIONS::restore(345);
if (empty($res["success"])) {
// version_not_found | type_without_versions | table_missing | version_empty
// | record_missing | id_taken | insert_failed | failed
error_log("restore failed: " . $res["error"]);
} elseif (!empty($res["unchanged"])) {
// הרשומה כבר זהה לגרסה
} else {
// $res["row_id"], $res["langs"], $res["version"] (ה-id של גרסת השחזור)
}
ניקוי יומי#
בלי ניקוי הטבלה גדלה בלי גבול. המדיניות:
- בכל רשומה ושפה נשארות תמיד הגרסאות החדשות ביותר עד המספר
keep, בלי קשר לגילן. - גרסה שגילה עולה על
daysימים ושהיא מעבר ל-keepהחדשות נמחקת. - גרסאות
baselineו-deleteלא נמחקות לעולם.
ערכי המדיניות, לפי הסדר: הפרמטר content_versions_keep של האתר, אחריו keep_versions של סוג התוכן (ברירת מחדל 50), ובסוף 50. הפרמטר content_versions_days קובע את הימים (ברירת מחדל 30). הפרמטרים הם אופציונליים ואין להם שורה מוכנה: כדי לשנות, הוסיפו שורה במסך Params (פרמטרים ושפות) עם sysName זה.
הניקוי רץ כמשימת cron: כתובת system/content_versions_cron/cleanup, מדי יום ב-04:30 (30 4 * * *). המשימה נמצאת בתיקיית משימות המערכת (קטגוריה 1) ומגיעה לאתר בעדכון הגרסה, יחד עם הקונטרולר. אם אתר לא קיבל אותה, הוסיפו אותה ב-cron_manager עם אותה כתובת. הקונטרולר דורש את הטוקן של ה-cron (tk) או מנהל מחובר, ומחזיר שורת טקסט:
OK records=12 deleted=340 keep=50 days=30
records הוא מספר הרשומות והשפות שנבדקו, ו-deleted מספר הגרסאות שנמחקו. בכל הרצה נבדקות עד 5000 רשומות לכל טבלה, והשאר ממתינות להרצה הבאה.
ה-API של ההיסטוריה: content_history#
מסכי ההיסטוריה של הניהול מדברים עם פאנל AJAX בשם content_history. אפשר לקרוא לו גם מ-JS של פאנל שלכם. כל הקריאות ב-{admin}/content_history/<method>?pmode=empg ומחזירות JSON. צפייה דורשת גישה לפאנל של סוג התוכן (העמודה panel ב-CRM_content_types), ושחזור דורש גם את ההרשאה edit שלו (הרשאות).
| שיטה | קלט | מחזיר |
|---|---|---|
history | tbl, id, lang (ברירת מחדל: השפה הנוכחית) | versions חדש ראשון (בלי data), type, policy (keep, days) |
version | id | גרסה אחת עם data מפוענח |
compare | a, b (מזהה גרסה או current; a=0 = הגרסה שלפני b) | fields: שדה אחרי שדה עם before, after, changed, kind (short, long, html) ובטקסט ארוך גם diff ברמת מילה |
deleted | tbl | records: מה שנמחק ולא חזר, החדש ראשון, עם כותרת |
conflict | tbl, id, lang, base | האם מישהו אחר שמר מאז שהטופס נפתח על גרסה base |
csrf | טוקן לשחזור | |
restore | POST: version_id, csrf (או הכותרת X-WZ-CSRF) | תשובת CONTENT_VERSIONS::restore() |
שגיאות חוזרות בצורה {"success": false, "error": "<קוד>"}: missing_tbl, missing_id, versions_not_installed, type_without_versions, forbidden, version_not_found, post_only, csrf.
// השוואה בין הגרסה הקודמת למצב הנוכחי, מתוך פאנל
const r = await fetch(admin_url + "content_history/compare?pmode=empg&a=0&b=current", { credentials: "same-origin" });
ב-compare שדה רגיש מגיע עם sensitive: true וערכיו null. ה-conflict מחזיר conflict: false, או conflict: true עם by_name, at, minutes_ago, changed ו-compare (a, b) לפתיחת מסך ההשוואה. הוא מתעלם מגרסאות של המנהל עצמו, ומגרסאות בסיס.
ב-panel_table של סוג תוכן עם גרסאות יש מגירת היסטוריה בכל שורה ולשונית "נמחקו לאחרונה" (כבויה עם $history = false על הרשימה). אזהרת העריכה המקבילה (conflict) קיימת בצד השרת, וחיבורה לטופס יגיע בגרסה מאוחרת יותר.
מתודות של CONTENT_VERSIONS#
כל המתודות סטטיות ולא זורקות חריגה.
CONTENT_VERSIONS::ready()#
CONTENT_VERSIONS::ready(): bool
האם הטבלה CRM_content_versions קיימת באתר. התוצאה נשמרת לבקשה.
CONTENT_VERSIONS::enabled_type()#
CONTENT_VERSIONS::enabled_type(string $table): array|null
שורת הסוג מ-CRM_content_types כשהטבלה רשומה עם versions=1, אחרת null.
CONTENT_VERSIONS::history()#
CONTENT_VERSIONS::history(string $table, int $id, int $lang, int $limit = 200): array
הגרסאות של רשומה בשפה, החדשות ראשונות, בלי data (בין 1 ל-1000). לכל שורה: id, version_no, kind, admin_id, admin_name, source, changed (מערך), note, created_at. גרסאות המחיקה של הרשומה (שפה 0) כלולות.
CONTENT_VERSIONS::get()#
CONTENT_VERSIONS::get(int $version_id): array|null
גרסה אחת עם data מפוענח למערך, או null.
CONTENT_VERSIONS::last()#
CONTENT_VERSIONS::last(string $table, int $id, int $lang): array|null
הגרסה החדשה ביותר של הרשומה בשפה, בלי data.
CONTENT_VERSIONS::deleted()#
CONTENT_VERSIONS::deleted(string $table, int $limit = 100): array
הרשומות שנמחקו ולא חזרו: גרסת המחיקה האחרונה של כל אחת, עם title מעמודת title_field. החדשות ראשונות, עד 500.
CONTENT_VERSIONS::compare()#
CONTENT_VERSIONS::compare(int|string $a, int|string $b): array
שני מזהי גרסה, או "current" לרשומה כפי שהיא עכשיו. עם $a = 0 מושווה הגרסה שלפני $b. מחזירה success, table, row_id, lang_id, a, b ו-fields. כשל: version_not_found או different_records.
CONTENT_VERSIONS::word_diff()#
CONTENT_VERSIONS::word_diff(string $a, string $b, bool $html = false): array
השוואה ברמת מילה, כרשימת צמדים ["=", "טקסט"], ["-", "טקסט"], ["+", "טקסט"]. עם $html = true מושווה הטקסט בלי תגיות.
CONTENT_VERSIONS::conflict()#
CONTENT_VERSIONS::conflict(string $table, int $id, int $lang, int $base_id, ?int $admin_id = null): array
טופס נפתח על גרסה $base_id (0 = לפני כל גרסה). מחזירה ["conflict" => false, "head" => ...] או conflict: true עם head, by_admin_id, by_name, source, at, minutes_ago, changed ו-compare. שמירה של מנהל אחר, או כל שמירה מ-MCP, נחשבת "של מישהו אחר".
CONTENT_VERSIONS::restore()#
CONTENT_VERSIONS::restore(int $version_id, array $opts = []): array
ראו "שחזור" למעלה. מחזירה success, ובהצלחה גם row_id, langs, version (או undeleted: true, או unchanged: true) ו-not_restored (טקסט שמסביר מה לא חוזר). כשל: error.
CONTENT_VERSIONS::policy()#
CONTENT_VERSIONS::policy(?string $table = null): array
מחזירה ["keep" => int, "days" => int] לפי הסדר שתואר בניקוי.
CONTENT_VERSIONS::cleanup()#
CONTENT_VERSIONS::cleanup(int $max_records = 5000): array
מוחקת גרסאות ישנות לפי המדיניות של כל טבלה, ומחזירה ["records" => נבדקו, "deleted" => נמחקו]. מי שקורא לה בעצמו (למשל ממשימת cron מותאמת) מקבל בדיוק את מה שהמשימה היומית עושה.
גרסאות לקוד שכותב ישירות#
גרסה נכתבת מאירוע. קוד של אתר שכותב ל-DB בלי לעבור בטופס או ב-panel_table צריך לשלוח את האירוע בעצמו, אחרת אין גרסה. הדוגמה המלאה ב-שכבת אירועי התוכן. לפני שמוחקים רשומה בקוד, צלמו אותה בלי שפה (CONTENT::snapshot($table, $id)) ושלחו אירוע delete, כדי ש"נמחקו לאחרונה" ושחזור יחזירו את כל השפות.
ראו גם#
- שכבת אירועי התוכן ו-CONTENT::register
- יומן ביקורת: מי עשה מה, גם בטבלאות שאין להן גרסאות.
- panel_table ו-טפסים: הדלתות שמייצרות את האירועים.
- כל הטבלאות ו-סכמת מסד הנתונים.