דלגו לתוכן

או התחילו מנקודה בטוחה: אינדקס הכלים המלא · מפת הטבלאות המרכזיות

מערכת חדשה — 14 יום חינם

דוגמה — מצא‑או‑צור ורשומה מקושרת

עודכן 30.08.2026
GET /parse/classes/Accounts?where=… → POST /parse/classes/Accounts → POST /parse/classes/TARGET

אירוע מבחוץ (הזמנה מחנות, פנייה ממערכת ישנה, שורה מקובץ) דורש לוודא שיש איש קשר, ואז ליצור לו רשומה מקושרת. ארבע החלטות קובעות אם זה יעבוד או ייצר כפילויות: החיפוש כשהטלפון חי בכמה פורמטים, הטיפול ביותר מהתאמה אחת, צורת ה‑Pointer בשכבה הזו, והטריגרים בריצה על 5,000 שורות.

1. חיפוש GET /parse/classes/Accounts?where={"$or":[…כל פורמטי הטלפון…, {"Email":…}]}&limit=5
├─ 0 תוצאות → שלב 2
├─ 1 תוצאה → יש objectId, לשלב 3
└─ 2+ תוצאות → כפילות קיימת במערכת: קחו את הראשונה ו**דווחו**
2. יצירה POST /parse/classes/Accounts {"Name":…, "PhoneNumber":…, "IsAccount":false}
→ 201 {"objectId":"xK9mP2qRsT"}
3. קישור POST /parse/classes/Tasks {"AccountId":{"__type":"Pointer",…}}

למה חיפוש‑לפני‑יצירה הוא באחריותכם

Section titled “למה חיפוש‑לפני‑יצירה הוא באחריותכם”

באותה טבלה חיים במקביל 0501234567, 972501234567, +972-50-1234567 ו‑050-1234567. הפלטפורמה אינה מנרמלת אותם: שאילתה על פורמט אחד לא תמצא את השאר, תחזיר מערך ריק ותייצר איש קשר כפול. רשומה שנשמרה כ‑05… לא תימצא בשאילתה על 972… ולא על 05X-….

מנוע זיהוי הכפילויות של שכבת הטפסים שואל את כל הווריאציות במפורש. חיפוש עצמאי חייב לעשות אותו הדבר:

JSON
{
"$or": [
{ "PhoneNumber": "0501234567" },
{ "PhoneNumber": "972501234567" },
{ "PhoneNumber": "+972501234567" },
{ "PhoneNumber": "050-1234567" },
{ "PhoneNumber": "+972-50-1234567" },
{ "Email": "israel@example.co.il" }
]
}

Pointer — שתי צורות, שתי שכבות

Section titled “Pointer — שתי צורות, שתי שכבות”
השכבה הצורה של ערך Pointer
REST (/parse/classes/…)אובייקט עטוף: {"__type":"Pointer","className":"Accounts","objectId":"xK9mP2qRsT"}
טפסים (getlead / web2table)מחרוזת חשופה בת 10 תווים: "xK9mP2qRsT"

העמוד הזה הוא REST, כלומר אובייקט עטוף. מחרוזת חשופה ב‑REST לא תתאים לדבר בשאילתה (200 עם מערך ריק, בלי שגיאה), ובכתיבה לשדה Pointer קיים היא נדחית: 400 {"code":111,"error":"schema mismatch for Tasks.AccountId; expected Pointer<Accounts> but got String"}. אותו דבר לתאריך: מחרוזת ISO שטוחה בשדה Date → code 111. רק בשם שדה חדש מחרוזת כזו תיצור שדה String בשקט. ראו סוגי נתונים.

PHP
<?php
declare(strict_types=1);
const API = 'https://api.mbapps.co.il/parse';
function headers(): array
{
return [
'Content-Type: application/json',
'X-Parse-Application-Id: ' . getenv('MB_APP_ID'),
'X-Parse-API-Key: ' . getenv('MB_API_KEY'),
];
}
function call(string $method, string $path, ?array $body = null, array $query = []): array
{
$url = API . $path . ($query ? '?' . http_build_query($query) : '');
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => headers(),
CURLOPT_POSTFIELDS => $body === null ? null : json_encode($body, JSON_UNESCAPED_UNICODE),
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($raw === false) throw new RuntimeException('שגיאת רשת מול ה-API');
if ($status >= 400) throw new RuntimeException("REST $status: $raw");
return json_decode($raw, true) ?: [];
}
/** כל הפורמטים שבהם מספר ישראלי עשוי להיות שמור. */
function phoneVariants(string $raw): array
{
$digits = preg_replace('/\D/', '', $raw) ?? '';
if (str_starts_with($digits, '972')) $digits = '0' . substr($digits, 3);
if ($digits === '') return [];
$intl = '972' . substr($digits, 1);
return array_values(array_unique([
$digits, // 0501234567
$intl, // 972501234567
'+' . $intl, // +972501234567
substr($digits, 0, 3) . '-' . substr($digits, 3), // 050-1234567
'+972-' . substr($digits, 1, 2) . '-' . substr($digits, 3), // +972-50-1234567
]));
}
/** מחזירה objectId של איש קשר קיים, או null. */
function findAccount(string $phone, ?string $email): ?string
{
$or = array_map(static fn ($v) => ['PhoneNumber' => $v], phoneVariants($phone));
if ($email) $or[] = ['Email' => $email];
if (!$or) return null;
$res = call('GET', '/classes/Accounts', null, [
'where' => json_encode(['$or' => $or], JSON_UNESCAPED_UNICODE),
'keys' => 'objectId,Name,PhoneNumber,Email',
'limit' => 5, // תמיד מפורש
]);
$hits = $res['results'] ?? [];
if (count($hits) > 1) {
error_log(sprintf('duplicate accounts for one contact: %d matches', count($hits)));
}
return $hits[0]['objectId'] ?? null;
}
function createAccount(string $name, string $phone, ?string $email): string
{
$body = array_filter([
'Name' => $name,
'PhoneNumber' => $phone,
'Email' => $email,
'IsAccount' => false, // false = ליד · true = לקוח
], static fn ($v) => $v !== null && $v !== '');
return call('POST', '/classes/Accounts', $body)['objectId'];
}
function createTask(string $accountId, string $title, string $dueIso): string
{
$body = [
'Name' => $title,
'Date' => ['__type' => 'Date', 'iso' => $dueIso], // אובייקט, לא מחרוזת
'AccountId' => ['__type' => 'Pointer', // אובייקט, לא מזהה חשוף
'className' => 'Accounts',
'objectId' => $accountId],
];
return call('POST', '/classes/Tasks', $body)['objectId'];
}
// הזרימה המלאה
$accountId = findAccount('0501234567', 'israel@example.co.il')
?? createAccount('ישראל ישראלי', '0501234567', 'israel@example.co.il');
createTask($accountId, 'התקשר ללקוח', '2026-09-15T10:30:00.000Z');

וריאנט batch — כמה רשומות בבת אחת

Section titled “וריאנט batch — כמה רשומות בבת אחת”

ליצירת הרבה רשומות מקושרות, למשל 300 משימות מעקב אחרי ייבוא, POST /parse/batch אורז כמה פעולות לבקשת HTTP אחת. ההמלצה: עד 50 פעולות לבקשה.

PHP
<?php
/**
* יוצר עד 50 משימות בבקשה אחת. מחזיר [הצלחות, כישלונות].
* @param list<array{accountId:string,title:string,dueIso:string}> $items
*/
function createTasksBatch(array $items): array
{
if (count($items) > 50) {
throw new InvalidArgumentException('batch: מומלץ עד 50 פעולות — חתכו בצד הלקוח (השרת אינו אוכף)');
}
$ops = array_map(static fn ($it) => [
'method' => 'POST',
'path' => '/parse/classes/Tasks', // הנתיב כולל /parse — בלעדיו: 400 code 107 "cannot route batch path"
'body' => [
'Name' => $it['title'],
'Date' => ['__type' => 'Date', 'iso' => $it['dueIso']],
'AccountId' => ['__type' => 'Pointer',
'className' => 'Accounts',
'objectId' => $it['accountId']],
],
], $items);
$res = call('POST', '/batch', ['requests' => $ops]);
$ok = $failed = [];
foreach ($res as $i => $item) {
if (isset($item['success'])) {
$ok[] = $item['success'];
} else {
// שומרים את הבקשה המקורית לצד הכישלון — כדי שיהיה מה לנסות שוב
$failed[] = ['request' => $ops[$i], 'error' => $item['error'] ?? null];
}
}
return [$ok, $failed];
}
// חיתוך ל-50 והשהיה בין קבוצות
function createAllTasks(array $items, float $pause = 0.3): array
{
$created = $errors = [];
foreach (array_chunk($items, 50) as $group) {
[$ok, $bad] = createTasksBatch($group);
$created = array_merge($created, $ok);
$errors = array_merge($errors, $bad);
usleep((int) ($pause * 1_000_000));
}
return [$created, $errors];
}

כל כתיבה מפעילה טריגרים

Section titled “כל כתיבה מפעילה טריגרים”
הדרך ההגבלה
POST /parse/batch אין דגל השתקה כלל (skipTriggers ברמת הבקשה — השרת מתעלם ממנו)
הכלי Create-Many של MCP עם skipTriggers: true מכובד רק באימות Master Key (רשומת Accounts עם IsAccount: true נוצרת בלי שטריגר ההמרה כותב LeadConversionDate; בלי הדגל — הטריגר רץ). ראו כלי נתונים
כיבוי זמני של הטריגרים בממשק דורש תיאום עם מי שמחזיק את האוטומציות — ואל תשכחו להדליק בחזרה

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

מרוץ בין שתי בקשות במקביל

Section titled “מרוץ בין שתי בקשות במקביל”

מצא‑או‑צור אינו אטומי: בין החיפוש ליצירה יש חלון. שני תהליכים שמעבדים את אותו איש קשר בו‑זמנית ימצאו שניהם “אין”, ושניהם ייצרו.

הפתרון מתי מתאים
תור בעל עובד יחיד לכל אינטגרציה הפתרון הפשוט והנכון ברוב המקרים
מפתח פירוק לפי איש קשר (מפצלים תורים לפי טלפון מנורמל) כשצריך מקביליות אמיתית
ExternalId + חיפוש חד‑משמעי מקטין את החלון, לא סוגר אותו
בדיקה חוזרת אחרי היצירה ומיזוג אם נוצרה כפילות תיקון בדיעבד, לא מניעה

ExternalId — לוותר על ניחוש טלפונים

Section titled “ExternalId — לוותר על ניחוש טלפונים”

בסנכרון ממערכת חיצונית שיש בה מזהה משלה, כתבו אותו לשדה משלכם (למשל ExternalId) ביצירה. הזיהוי הופך לשאילתה אחת חד‑משמעית, בלי ווריאציות ובלי $or:

JSON
{ "ExternalId": "SHOP-84213" }

זה גם מה שהופך ריצה חוזרת לאידמפוטנטית: מצאתם, עדכנו; לא מצאתם, צרו. ראו עבודה עם רשומות.

מה קורה למה מה עושים
השאילתה מחזירה מערך ריק למרות שהרשומה קיימת סינון Pointer במחרוזת חשופה במקום באובייקט, או תאריך שנשלח שטוח ב‑where שאילתות
404 code 101 Object not found הרשומה לא קיימת — או שההרשאות מסתירות אותה עם API Key זה תמיד “לא קיימת” (GET על מזהה שאינו קיים → 404 code 101)
נמצאו 3 התאמות לאותו טלפון כפילות שכבר קיימת במערכת מלפני האינטגרציה קחו את הראשונה, ודווחו. מיזוג הוא החלטה אנושית
הרשומה נוצרה, ה‑Pointer ריק ערך Pointer שאינו אובייקט עטוף בשכבת ה‑REST סוגי נתונים
מחיקת איש קשר שיש לו משימות נכשלת במערכת חדשה מופעל checkDependenciesBeforeDelete: 400 code 119 Pointer to Object … exists in table Tasks in key AccountId. Delete is not allowed. זו הגנה רצויה. העדיפו ארכיון על מחיקה. כשההגדרה כבויה — המצביע נשאר תלוי באוויר. עבודה עם רשומות
500 Internal server error על שאילתה where שאינו JSON תקין (תוצאה טיפוסית של שרשור ידני) — השרת מחזיר 500 במקום 400. רווח גולמי ב‑URL → 400 Bad Request בדף HTML תנו לספרייה לקודד, ואל תנסו שוב אוטומטית על 500 לפני שבדקתם את ה‑where
בדיוק 100 תוצאות, תמיד לא צוין limit — זו ברירת המחדל של REST ציינו תמיד
  • החיפוש מכסה את כל פורמטי הטלפון, ולא רק את הפורמט שהמערכת השולחת
  • יותר מהתאמה אחת → נרשם ביומן, לא נבלע
  • המפתח מגיע ממשתנה סביבה או ממנהל סודות
  • limit מפורש בכל שאילתה
  • ערך Pointer הוא אובייקט עטוף (שכבת REST), ותאריך הוא {"__type":"Date","iso":…}
  • batch — חיתוך ל‑50 בצד הלקוח (השרת אינו אוכף), path מתחיל ב‑/parse, וכל איבר בתשובה נבדק בנפרד (וגם איבר שגיאה בלי code)
  • הכישלונות נשמרים לצד הבקשה המקורית, כדי שיהיה מה לנסות שוב
  • נשאלה השאלה מי מחזיק את הטריגרים של טבלת היעד — לפני ההרצה, לא אחריה
  • הריצה הראשונה על ייצור הייתה על חמש שורות, ונבדק מה יצא