דלגו לתוכן

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

מערכת חדשה — 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 “למה חיפוש‑לפני‑יצירה הוא באחריותכם”

הבעיה שמפילה את רוב המימושים: פורמטי הטלפון

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 — שתי צורות, שתי שכבות”

זו הטעות מספר 1 של מי שעובר מטפסים ל‑REST, ולהפך:

השכבה הצורה של ערך 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 שטוחה בשדה Datecode 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)
  • הכישלונות נשמרים לצד הבקשה המקורית, כדי שיהיה מה לנסות שוב
  • נשאלה השאלה מי מחזיק את הטריגרים של טבלת היעד — לפני ההרצה, לא אחריה
  • הריצה הראשונה על ייצור הייתה על חמש שורות, ונבדק מה יצא