הבעיה של פרסום כאוטי
בחברת מדיה גדולה, מנהל תוכן פרסם בטעות טיוטה עם נתונים חסויים — מה שהוביל לדליפה ולקנס של חצי מיליון דולר. תקריות כאלה אינן נדירות כשאין בקרת גרסאות ומערכת הרשאות. פיתחנו מערכת זרימת עבודה שמבטלת את הגורם האנושי: כל מעבר סטטוס דורש הרשאות ומתועד.
ביותר מ-15 פרויקטים יישמנו גישה זו, והזמן מטיוטה לפרסום ירד בממוצע מ-5 ימים ל-4 שעות. במאמר זה — איך אנחנו עושים זאת על Laravel. כאבים אופייניים: גרסאות אבודות, פרסומים לא מבוקרים, מחזורי אישור ארוכים. המערכת שלנו פותרת אותם באמצעות שרשרת סטטוסים קפדנית והקצאה אוטומטית של עורכים. מערכת זרימת עבודה היא לא רק קבוצת סטטוסים, אלא תקנון שכל המשתתפים פועלים לפיו. אנחנו בונים אותה על Laravel באמצעות ארכיטקטורה מונעת אירועים.
איך להגדיר את שרשרת הסטטוסים?
זרימת העבודה מבוססת על סטטוסים ומעברים. שרשרת בסיסית: טיוטה → סקירה → מאושר → פורסם → ארכיון. נוסף: נדחה, revision_needed, מתוזמן. כל מעבר בודק הרשאות משתמש. חשוב לעצב את הגרף נכון — לדוגמה, אי אפשר לעבור מטיוטה ישירות לפרסום, תוך עקיפת הסקירה. זה מבטל פרסומים מקריים.
הנה המבנה של טבלת הסטטוסים:
content_states (
id,
content_type,
content_id,
status: draft | review | approved | published | rejected | archived | scheduled,
assigned_to (editor/moderator id),
comment,
scheduled_at,
published_at,
archived_at,
transitioned_by,
transitioned_at
)
content_state_history (
id,
content_type,
content_id,
from_status,
to_status,
changed_by,
comment,
changed_at
) יישום על Laravel
אנחנו משתמשים במחלקת content_states ( id, content_type, content_id, status: draft | review | approved | published | rejected | archived | scheduled, assigned_to (editor/moderator id), comment, scheduled_at, published_at, archived_at, transitioned_by, transitioned_at ) content_state_history ( id, content_type, content_id, from_status, to_status, changed_by, comment, changed_at ) עם מערך של מעברים מותרים והרשאות. דוגמה:
class ContentWorkflow {
private array $transitions = [
'draft' => ['review'],
'review' => ['approved', 'rejected', 'revision_needed'],
'approved' => ['published', 'scheduled'],
'rejected' => ['draft'],
'revision_needed' => ['draft'],
'published'=> ['archived', 'draft'],
'scheduled'=> ['published', 'draft']
];
private array $permissions = [
'draft → review' => 'content.submit_for_review',
'review → approved' => 'content.approve',
'review → rejected' => 'content.approve',
'approved → published' => 'content.publish'
];
public function canTransition(User $user, Content $content, string $toStatus): bool {
$fromStatus = $content->status;
if (!in_array($toStatus, $this->transitions[$fromStatus] ?? [])) {
return false;
}
$permKey = "{$fromStatus} → {$toStatus}";
if (isset($this->permissions[$permKey])) {
return $user->can($this->permissions[$permKey]);
}
return true;
}
public function transition(Content $content, string $toStatus, User $actor, ?string $comment = null): void {
if (!$this->canTransition($actor, $content, $toStatus)) {
throw new WorkflowException("Переход {$content->status} → {$toStatus} недоступен");
}
DB::transaction(function () use ($content, $toStatus, $actor, $comment) {
ContentStateHistory::create([
'content_type' => get_class($content),
'content_id' => $content->id,
'from_status' => $content->status,
'to_status' => $toStatus,
'changed_by' => $actor->id,
'comment' => $comment
]);
$content->update([
'status' => $toStatus,
'published_at' => $toStatus === 'published' ? now() : $content->published_at
]);
event(new ContentStatusChanged($content, $toStatus, $actor, $comment));
});
}
} למה היסטוריית מעברים חשובה?
כל מעבר נשמר ב-ContentWorkflow. זה מאפשר לעקוב אחר מי, מתי ולמה שינה את הסטטוס. ההיסטוריה עוזרת לפתור קונפליקטים ולעמוד בתקנונים. המערכות שלנו שומרות היסטוריה ללא הגבלת זמן. לדוגמה, בפרויקט אחד, ההיסטוריה עזרה להוכיח שפרסום היה מורשה, ולא תוצאה של פריצה.
הקצאת עורכים
הקצאה אוטומטית של העורך הזמין הראשון היא תכונה מרכזית. אנחנו משתמשים באלגוריתם פשוט: בחירת העורך עם הכי פחות משימות פעילות.
class AssignReviewer {
public function assign(Content $content): User {
$reviewer = User::where('role', 'editor')
->withCount(['assignedContent' => fn($q) => $q->where('status', 'review')])
->orderBy('assigned_content_count')
->first();
$content->update(['assigned_to' => $reviewer->id]);
$reviewer->notify(new ContentAssignedForReview($content));
return $reviewer;
}
} מועדים ותזכורות
אנחנו מגדירים תזכורות אוטומטיות אם תוכן נשאר בסטטוס 'בסקירה' יותר מ-24 שעות. במקרה כזה, נשלחות הודעות לעורך ולעורך הראשי. אסקלציה אפשרית גם כן. מוגדר דרך class ContentWorkflow { private array $transitions = [ 'draft' => ['review'], 'review' => ['approved', 'rejected', 'revision_needed'], 'approved' => ['published', 'scheduled'], 'rejected' => ['draft'], 'revision_needed' => ['draft'], 'published'=> ['archived', 'draft'], 'scheduled'=> ['published', 'draft'] ]; private array $permissions = [ 'draft → review' => 'content.submit_for_review', 'review → approved' => 'content.approve', 'review → rejected' => 'content.approve', 'approved → published' => 'content.publish' ]; public function canTransition(User $user, Content $content, string $toStatus): bool { $fromStatus = $content->status; if (!in_array($toStatus, $this->transitions[$fromStatus] ?? [])) { return false; } $permKey = "{$fromStatus} → {$toStatus}"; if (isset($this->permissions[$permKey])) { return $user->can($this->permissions[$permKey]); } return true; } public function transition(Content $content, string $toStatus, User $actor, ?string $comment = null): void { if (!$this->canTransition($actor, $content, $toStatus)) { throw new WorkflowException("Переход {$content->status} → {$toStatus} недоступен"); } DB::transaction(function () use ($content, $toStatus, $actor, $comment) { ContentStateHistory::create([ 'content_type' => get_class($content), 'content_id' => $content->id, 'from_status' => $content->status, 'to_status' => $toStatus, 'changed_by' => $actor->id, 'comment' => $comment ]); $content->update([ 'status' => $toStatus, 'published_at' => $toStatus === 'published' ? now() : $content->published_at ]); event(new ContentStatusChanged($content, $toStatus, $actor, $comment)); }); } } ו-content_state_history. תזכורות אוטומטיות הפחיתו את מספר החומרים התקועים בסקירה ב-70%.
פרסום מתוזמן
class PublishScheduledContent implements ShouldQueue {
public function handle(): void
{
Content::where('status', 'scheduled')
->where('scheduled_at', '<=', now())
->each(function (Content $content) {
app(ContentWorkflow::class)->transition(
$content,
'published',
User::find($content->created_by)
);
});
}
}המשימה רצה כל 5 דקות דרך מתזמן המשימות.
איך אנחנו מיישמים זרימת עבודה: מבדיקה ועד פריסה
התהליך כולל שישה שלבים:
- בדיקת תהליכים קיימים — ראיונות עם עורכים, ניתוח לוגים, מפת סטטוסים.
- עיצוב סכמה — הגדרת סטטוסים, מעברים, הרשאות, סוגי התראות.
- פיתוח על Laravel — יישום מחלקות Workflow, אירועים, מאזינים.
- אינטגרציה עם CMS קיים — חשיפת API, עטיפת CRUD קיים.
- בדיקות — בדיקות יחידה לכל מעבר, בדיקות עומס.
- פריסה והדרכה — השקה ומפגש עם עורכים.
ציר זמן: מ-3 עד 5 שבועות תלוי במורכבות ההרשאות ובמספר סוגי התוכן.
ממשק לוח בקרה לעורכים
עמודות לפי סטטוס (תצוגה דמוית Kanban) או רשימה עם פילטרים. עבור כל רשומה: סטטוס נוכחי וממונה, כפתורים למעברים זמינים, תגובות מנחה, היסטוריית שינויי סטטוס.
השוואה: עם זרימת עבודה לעומת בלי
| קריטריון | בלי זרימת עבודה | עם המערכת שלנו |
|---|---|---|
| מהירות פרסום | תלויה במקריות | פי 3 מהר יותר בזכות אוטומציה |
| שגיאות מודרציה | לעתים קרובות מפספסים תוכן באיכות נמוכה | הפחתה של 90% |
| שקיפות | אף אחד לא יודע את הסטטוס | היסטוריה מלאה והתראות |
השוואת שיטות הקצאת עורכים
| שיטה | זמן הקצאה | סיכון לשגיאות | שקיפות |
|---|---|---|---|
| ידנית | 5–10 דקות | גבוה | נמוכה |
| אוטומטית (שלנו) | מיידית | נמוך | גבוהה |
מידע נוסף על הרשאות גישה
כל מעבר מקושר להרשאת Laravel. לדוגמה, `content.approve` יכול להיות מוקצה רק לעורך. אנחנו משתמשים בדגלי `content.publish` עבור מנהלים. זה מבטיח שרק משתמשים מורשים יכולים לשנות סטטוס.מה כלול בעבודה
אנחנו מיישמים את המערכת במפתח מלא תוך 3–5 שבועות. התפוקות כוללות:
- תיעוד של סכמת הסטטוסים וההרשאות.
- קוד מקור עם בדיקות.
- הגדרת התראות (אימייל, Telegram).
- הדרכה לעורכים ומנהלים.
- תמיכה ל-30 יום לאחר ההשקה.
אנחנו מבטיחים יציבות: הפתרונות שלנו רצים על 20+ פרויקטים. נבצע הערכה לפרויקט שלך — פשוט צור קשר. כדי לקבל פתרון דומה לאתר שלך, פנה אלינו — נכין הצעה תוך יום אחד.







