|
| 1 | +--- |
| 2 | +name: kayako-api-function |
| 3 | +description: Adds a new procedural API function to `src/api.php` following the project's validation-first, try/catch-per-operation pattern. Initializes Kayako SOAP config, wraps each Kayako call in its own try/catch, returns status/status_text arrays. Use when user says 'add API function', 'new ticket operation', 'create endpoint in api.php', or 'add support function'. Do NOT use for modifying `src/Plugin.php` hook registration or for class-based API work. |
| 4 | +--- |
| 5 | +# kayako-api-function |
| 6 | + |
| 7 | +## Critical |
| 8 | + |
| 9 | +- **Never** interpolate `$_GET`/`$_POST` directly into queries — always `$db->real_escape($input)`. |
| 10 | +- **Every** Kayako SDK call must be in its own `try/catch (Exception $e)` block — one operation per block. |
| 11 | +- **Always** check `$GLOBALS['tf']->ima != 'admin'` AND `account_lid` before any mutation that touches another user's ticket. |
| 12 | +- **Always** call `function_requirements('class.kyConfig')` before the SOAP init try/catch — not inside it. |
| 13 | +- Return the `$result` array on every path — never let the function fall off the end without returning. |
| 14 | + |
| 15 | +## Instructions |
| 16 | + |
| 17 | +1. **Define the function** in `src/api.php` with a PHPDoc block. Use camelCase names matching existing functions (`openTicket`, `viewTicket`, `ticketPost`, `getTicketList`). |
| 18 | + |
| 19 | + ```php |
| 20 | + /** |
| 21 | + * One-line description. |
| 22 | + * |
| 23 | + * @param int $ticketID the ticket id |
| 24 | + * @param string $content the reply body |
| 25 | + * @return array status/status_text result |
| 26 | + */ |
| 27 | + function myNewFunction($ticketID, $content) |
| 28 | + { |
| 29 | + ``` |
| 30 | + |
| 31 | +2. **Initialize the result array** as the first statement. Include only keys this function actually returns — do not add speculative keys. |
| 32 | + |
| 33 | + ```php |
| 34 | + $result = [ |
| 35 | + 'status' => 'incomplete', |
| 36 | + 'status_text' => '', |
| 37 | + ]; |
| 38 | + ``` |
| 39 | + |
| 40 | +3. **Validate all required inputs** with early-return guards before touching Kayako. Return `'Failed'` (capital F) for user-input errors. |
| 41 | + |
| 42 | + ```php |
| 43 | + if (!$ticketID) { |
| 44 | + $result['status'] = 'Failed'; |
| 45 | + $result['status_text'] = 'Ticket Reference ID is required. Please try again!'; |
| 46 | + return $result; |
| 47 | + } |
| 48 | + if (!$content) { |
| 49 | + $result['status'] = 'Failed'; |
| 50 | + $result['status_text'] = 'Content is required. Please try again!'; |
| 51 | + return $result; |
| 52 | + } |
| 53 | + ``` |
| 54 | + |
| 55 | + Verify all validation guards return before proceeding to Step 4. |
| 56 | + |
| 57 | +4. **Check ownership** for any read/mutate of another user's ticket (skip for creation functions): |
| 58 | + |
| 59 | + ```php |
| 60 | + if ($GLOBALS['tf']->ima != 'admin' && |
| 61 | + $GLOBALS['tf']->accounts->data['account_lid'] != kyTicket::get($ticketID)->getUser()->getEmail()) { |
| 62 | + $result['status'] = 'Failed'; |
| 63 | + $result['status_text'] = 'Access denied. Please try again!'; |
| 64 | + myadmin_log('api', 'info', 'Denied: ' . $GLOBALS['tf']->accounts->data['account_lid'], __LINE__, __FILE__); |
| 65 | + return $result; |
| 66 | + } |
| 67 | + ``` |
| 68 | + |
| 69 | + Wrap the ownership check itself in `try/catch` — `kyTicket::get()` can throw. |
| 70 | + |
| 71 | +5. **Initialize Kayako SOAP** — call `function_requirements` outside the try, init inside: |
| 72 | + |
| 73 | + ```php |
| 74 | + function_requirements('class.kyConfig'); |
| 75 | + try { |
| 76 | + kyConfig::set(new kyConfig(KAYAKO_API_URL, KAYAKO_API_KEY, KAYAKO_API_SECRET)); |
| 77 | + kyConfig::get()->setDebugEnabled(false)->setTimeout(120); |
| 78 | + } catch (Exception $e) { |
| 79 | + $result['status'] = 'failed'; |
| 80 | + $result['status_text'] = 'Kayako exception occurred setting config options. Please try again!'; |
| 81 | + myadmin_log('api', 'info', $e->getMessage(), __LINE__, __FILE__); |
| 82 | + return $result; |
| 83 | + } |
| 84 | + ``` |
| 85 | + |
| 86 | +6. **Wrap each subsequent Kayako operation** in its own `try/catch`. Set `'status_text'` to a human message naming the failing operation: |
| 87 | + |
| 88 | + ```php |
| 89 | + try { |
| 90 | + $ticket = kyTicket::get($ticketID); |
| 91 | + $user = $ticket->getUser(); |
| 92 | + } catch (Exception $e) { |
| 93 | + $result['status'] = 'Failed'; |
| 94 | + $result['status_text'] = 'Kayako exception occurred getting ticket detail. Please try again!'; |
| 95 | + myadmin_log('api', 'info', $e->getMessage(), __LINE__, __FILE__); |
| 96 | + return $result; |
| 97 | + } |
| 98 | + try { |
| 99 | + $post = $ticket->newPost($user, $content)->create(); |
| 100 | + if ($post) { |
| 101 | + $result['status'] = 'Success'; |
| 102 | + $result['status_text'] = 'Post added successfully'; |
| 103 | + } else { |
| 104 | + $result['status'] = 'Failed'; |
| 105 | + $result['status_text'] = 'Exception occurred adding post.'; |
| 106 | + } |
| 107 | + return $result; |
| 108 | + } catch (Exception $e) { |
| 109 | + $result['status'] = 'Failed'; |
| 110 | + $result['status_text'] = 'Kayako exception occurred adding post. Please try again!'; |
| 111 | + myadmin_log('api', 'info', $e->getMessage(), __LINE__, __FILE__); |
| 112 | + return $result; |
| 113 | + } |
| 114 | + } |
| 115 | + ``` |
| 116 | + |
| 117 | +7. **For DB-only functions** (no Kayako, e.g. listing from `swtickets`), use `clone $GLOBALS['helpdesk_dbh']` and always escape: |
| 118 | + |
| 119 | + ```php |
| 120 | + $db = clone $GLOBALS['helpdesk_dbh']; |
| 121 | + $db->query("SELECT * FROM swtickets WHERE ticketmaskid = '" . $db->real_escape($ticketID) . "'", __LINE__, __FILE__); |
| 122 | + ``` |
| 123 | + |
| 124 | +8. **Run tests** to verify the function exists and its signature is correct: |
| 125 | + |
| 126 | + ```bash |
| 127 | + vendor/bin/phpunit tests/ApiFunctionsTest.php |
| 128 | + ``` |
| 129 | + |
| 130 | + Add tests to `tests/ApiFunctionsTest.php` following the `ReflectionFunction` pattern: verify function exists, parameter count, parameter names, and validation-failure return shape. |
| 131 | + |
| 132 | +## Examples |
| 133 | + |
| 134 | +**User says:** "Add a function to close a ticket by ID." |
| 135 | + |
| 136 | +**Actions taken:** |
| 137 | +1. Add `closeTicket($ticketID)` to `src/api.php` |
| 138 | +2. `$result = ['status' => 'incomplete', 'status_text' => '']` |
| 139 | +3. Guard: `if (!$ticketID)` → return `'Failed'` |
| 140 | +4. Ownership check in try/catch |
| 141 | +5. SOAP init block |
| 142 | +6. `try { kyTicket::get($ticketID)->close()->save(); $result['status'] = 'Success'; ... return $result; } catch ...` |
| 143 | +7. Add `testCloseTicketFunctionExists`, `testCloseTicketSignature`, `testCloseTicketFailsWithEmptyId` to `tests/ApiFunctionsTest.php` |
| 144 | +8. Run `vendor/bin/phpunit tests/ApiFunctionsTest.php` |
| 145 | + |
| 146 | +**Result:** New function in `src/api.php`, matching shape of `ticketPost`; tests green. |
| 147 | + |
| 148 | +## Common Issues |
| 149 | + |
| 150 | +- **`Call to undefined function kyConfig::set()`**: `function_requirements('class.kyConfig')` was not called before the SOAP init block. Add it immediately before the try/catch. |
| 151 | +- **`Undefined variable $result` in catch block**: `$result` array was not initialized before the first if-guard. Move the `$result = [...]` declaration to the very first line of the function body. |
| 152 | +- **Tests fail with `openTicket not found`**: `setUpBeforeClass` only calls `require_once` when `openTicket` doesn't exist. Add a stub for any new global functions your function calls (e.g. `ticket_status_all`) inside the `if (!function_exists('openTicket'))` block in `ApiFunctionsTest::setUpBeforeClass()`. |
| 153 | +- **`status` key returns lowercase `'failed'` instead of `'Failed'`**: Validation-path failures use capital-F `'Failed'`; only SOAP init failures in `openTicket`/`getTicketList` use lowercase. Match the convention of the nearest sibling function. |
| 154 | +- **`Undefined index: account_lid`**: `$GLOBALS['tf']->accounts->data` is not available in unit tests — wrap ownership checks in try/catch and confirm the test only exercises the pre-ownership validation paths. |
0 commit comments