承接 dontpanic/zammad-external-data 相关项目开发

从需求分析到上线部署,全程专人跟进,保证项目质量与交付效率

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

dontpanic/zammad-external-data

最新稳定版本:v1.2.1

Composer 安装命令:

composer create-project dontpanic/zammad-external-data

包简介

External data API for Zammad integration

README 文档

README

A tiny PHP service that exposes simple HTTP endpoints to fetch external data (via JSON‑RPC) and return it as JSON. Requests are routed to a controller that relays query parameters to a remote API using cURL.

This project is structured for small integrations (e.g., enriching Zammad or other systems) where you want a thin proxy layer between your application and a remote API.

What changed recently

  • PHP namespace was renamed from App to DontPanic\ZammadExternalData (PSR‑4 updated accordingly).
  • Router now supports explicit paths:
    • GET /objects → forwards query params as a simple filter
    • GET /conditions → builds an i‑doit property condition from query params
    • GET /report → fetches objects delivered by a report
    • GET /zammad/tickets → fetches tickets from Zammad
  • Automatic base‑path detection in Router for subdirectory deployments.

Features

  • Minimal routing with two endpoints (/objects, /conditions)
  • Simple pass‑through of query parameters as filter conditions
  • JSON responses, suitable for easy consumption

Contents

  • public/index.php – front controller, initializes autoloading and routing
  • src/Router/Router.php – simple router that dispatches to QueryController
  • src/Controller/QueryController.php – loads configuration and calls ApiClient
  • src/Service/ApiClient.php – cURL JSON‑RPC client with helper methods
  • src/Config/Config.php – reads configuration from config.json
  • config.json – API credentials and endpoint configuration (do not commit real secrets)

Requirements

  • PHP 8.1+
  • PHP cURL extension (ext-curl)
  • Composer (for autoloading)

Note: ext-curl is required at runtime but is not explicitly listed in composer.json.

Installation

  1. Clone the repository
  2. Install dependencies (autoload only):
    • If composer.lock is present, regenerate autoload files:
      composer dump-autoload
      
    • Otherwise (or to refresh), run:
      composer install
      
  3. Copy and edit config.json with your actual credentials and endpoint.

After namespace changes, ensure autoload is rebuilt:

composer dump-autoload

Configuration

The service reads its configuration from config/config.json (relative to the project root). Example:

{
  "api": {
    "url": "https://example.tld/i-doit/src/jsonrpc.php",
    "token": "YOUR_API_TOKEN",
    "user": "apiuser",
    "password": "apipassword"
  },
  "zammad": {
    "url": "https://zammad.exaple.tld",
    "token": "token_from_a_user_with_access_to_tickets"
  }
}

Fields used by the app:

  • api.url – JSON‑RPC endpoint URL
  • api.token – API token used on requests
  • api.user – Auth username passed via header
  • api.password – Auth password passed via header

The Config class loads this file and makes properties available to ApiClient.

Running locally

Use PHP’s built‑in web server and point the document root to public/:

php -S localhost:8000 -t public

Base path handling: When deployed in a subdirectory, the Router auto‑detects the base path (removes /public/index.php) so routes still work under that prefix.

Usage and request format

  • Objects by simple filter (forwards all query params to ApiClient->getObjects()):

    curl "http://localhost:8000/objects?title=MyObject&status=C__RECORD_STATUS__NORMAL"
    

    More examples:

    • Find by title: GET /objects?title=MyObject
    • Filter by email: GET /objects?email=user@example.com
    • Explicit status: GET /objects?status=C__RECORD_STATUS__NORMAL
  • Objects by property condition (uses ApiClient->getObjectsByConditions()):

    The current implementation expects scalar parameters (not arrays):

    curl "http://localhost:8000/conditions?category=C__CATG__GLOBAL&attribute=title&value=itsm*&comparison=like"
    

    This becomes a single condition with property = "{category}-{attribute}".

Response is JSON with the raw payload returned by the remote JSON‑RPC endpoint.

Note: ApiClient also contains helper methods (e.g., getPersonByEmail, getPersonByLogin, read/write category helpers, batch operations). You can extend QueryController and routing logic to expose additional operations as needed.

Query Zammad for tickets with linked external data

Fetching Tickets

curl "http://localhost:8000/zammad/tickets/?query=object_name.value:object_id"

Adding a custom category to i-doit

This example uses the Zammad External Data Proxy to fetch tickets from Zammad and display them in an i-doit custom category.

In i-doit, go to Administration > Data structure > Custom categories and add a new category. For the new category add a new field with the type "Javascript" and add the following code:

// Fetch tickets from zammad-external-data

// Store a unique marker in the script itself, then find it (couldn’t find a better way to place the table in the DOM)
var currentScript = document.currentScript || (function() {
    var scripts = document.getElementsByTagName('script');
    for (var i = 0; i < scripts.length; i++) {
        if (scripts[i].textContent.indexOf('UNIQUE_MARKER_12345') > -1) {
            return scripts[i];
        }
    }
    return scripts[scripts.length - 1];
})();
var container = currentScript.parentElement;

// Configuration
var proxyUrl = 'https://itsmcloud.panic.at/i-doit/zammad-external-data'; // Replace with your actual proxy URL
var zammadBaseURL = 'https://zammad.panic.at'; // Replace with your actual zammad URL

// Get object ID from URL parameter
var objectID = new URLSearchParams(window.location.search).get('objID')

// Show loading message
container.innerHTML = '<p style="padding: 10px;">Loading tickets...</p>';

// Fetch tickets through proxy
var xhr = new XMLHttpRequest();
xhr.open('GET', proxyUrl + '/zammad/tickets?query=idoit_organisations.value:' + objectID, true);

xhr.onload = function() {
    if (xhr.status >= 200 && xhr.status < 300) {
        var response = JSON.parse(xhr.responseText);

        // Clear loading message
        container.innerHTML = '';

        // Check for errors
        if (response.error) {
            container.innerHTML = '<p style="color: red; padding: 10px;">Error: ' + response.error + '</p>';
            return;
        }

        // Response is directly an array of tickets
        var tickets = Array.isArray(response) ? response : [];

        // Check if we have tickets
        if (tickets.length === 0) {
            container.innerHTML = '<p style="padding: 10px;">No tickets found for this organisation.</p>';
            return;
        }

        // Create wrapper div for margin
        var wrapper = document.createElement('div');
        wrapper.style.margin = '10px 20px';

        // Create table
        var table = document.createElement('table');
        table.style.width = '100%';
        table.style.borderCollapse = 'collapse';

        // Create header
        var thead = document.createElement('thead');
        var headerRow = document.createElement('tr');

        var headers = ['Subject', 'State', 'Owner', 'Created at', 'Updated at', 'Priority', "Group", ""];
        headers.forEach(function(headerText) {
            var th = document.createElement('th');
            th.textContent = headerText;
            th.style.border = '1px solid #ddd';
            th.style.padding = '8px';
            th.style.backgroundColor = '#f2f2f2';
            th.style.textAlign = 'left';
            th.style.fontWeight = 'bold';
            headerRow.appendChild(th);
        });

        thead.appendChild(headerRow);
        table.appendChild(thead);

        // Create body with ticket data
        var tbody = document.createElement('tbody');

        tickets.forEach(function(ticket, index) {
            var tr = document.createElement('tr');
            if (index % 2 === 0) {
                tr.style.backgroundColor = '#f9f9f9';
            }

            // Title
            var tdTitle = document.createElement('td');
            tdTitle.textContent = ticket.title || '-';
            tdTitle.style.border = '1px solid #ddd';
            tdTitle.style.padding = '8px';
            tr.appendChild(tdTitle);

            // State
            var tdState = document.createElement('td');
            tdState.textContent = ticket.state || '-';
            tdState.style.border = '1px solid #ddd';
            tdState.style.padding = '8px';
            tr.appendChild(tdState);

            // Owner
            var tdOwner = document.createElement('td');
            tdOwner.textContent = ticket.owner || '-';
            tdOwner.style.border = '1px solid #ddd';
            tdOwner.style.padding = '8px';
            tr.appendChild(tdOwner);

            // created at 
            var tdCreatedAt = document.createElement('td');
            tdCreatedAt.textContent = ticket.created_at || '-';
            tdCreatedAt.style.border = '1px solid #ddd';
            tdCreatedAt.style.padding = '8px';
            tr.appendChild(tdCreatedAt);

            // created at 
            var tdUpdatedAt = document.createElement('td');
            tdUpdatedAt.textContent = ticket.updated_at || '-';
            tdUpdatedAt.style.border = '1px solid #ddd';
            tdUpdatedAt.style.padding = '8px';
            tr.appendChild(tdUpdatedAt);



            // Priority
            var tdPriority = document.createElement('td');
            tdPriority.textContent = ticket.priority || '-';
            tdPriority.style.border = '1px solid #ddd';
            tdPriority.style.padding = '8px';
            tr.appendChild(tdPriority);
            
            // Group
            var tdGroup = document.createElement('td');
            tdGroup.textContent = ticket.group || '-';
            tdGroup.style.border = '1px solid #ddd';
            tdGroup.style.padding = '8px';
            tr.appendChild(tdGroup);

            // Link to ticket (https://zammad.panic.at/#ticket/zoom/id)
            var tdLink = document.createElement('td');
            tdLink.style.border = '1px solid #ddd';
            tdLink.style.padding = '8px';

            var link = document.createElement('a');
            link.href = zammadBaseURL + '/#ticket/zoom/' + ticket.id;
            link.className = 'btn';
            link.target = '_blank';

            var img = document.createElement('img');
            img.src = 'images/axialis/basic/link.svg';
            img.alt = '';

            var span = document.createElement('span');
            span.textContent = 'Open in Zammad';

            link.appendChild(img);
            link.appendChild(span);
            tdLink.appendChild(link);
            tr.appendChild(tdLink);

            tbody.appendChild(tr);
        });

        table.appendChild(tbody);
        wrapper.appendChild(table);
        container.appendChild(wrapper);

    } else {
        container.innerHTML = '<p style="color: red; padding: 10px;">Error loading tickets: ' + xhr.status + '</p>';
    }
};

xhr.onerror = function() {
    container.innerHTML = '<p style="color: red; padding: 10px;">Network error occurred while loading tickets</p>';
};

xhr.send();

Project structure (overview)

  • public/index.php – bootstraps Composer and dispatches request
  • src/Router/Router.php – Router::dispatch($uri, $query) and base‑path handling
  • src/Controller/QueryController.php – loads ../config.json, exposes /objects and /conditions
  • src/Service/ApiClient.php – cURL JSON‑RPC wrapper (post, getObjects, getObjectsByConditions, and more)
  • src/Config/Config.php – parses config.json

Troubleshooting

  • 400/500 errors from upstream: ApiClient throws when HTTP code >= 400; ensure credentials and endpoint are correct.
  • SSL verification is disabled in the client (curl options CURLOPT_SSL_VERIFYPEER=false and CURLOPT_SSL_VERIFYHOST=0). For production, enable verification or terminate TLS at a trusted proxy.
  • If autoloading fails, run composer dump-autoload and verify PSR‑4 mapping in composer.json:
    • "DontPanic\\ZammadExternalData\\": "src/"

Security and deployment notes

  • Do NOT commit real secrets in config.json; use environment‑specific copies.
  • Restrict network access to this service if it proxies sensitive data.
  • Consider moving secrets to environment variables or a secret manager for production.
  • Enable TLS termination in front of this service when deploying.

License

This project is licensed under the MIT License – see the LICENSE file for details.

统计信息

  • 总下载量: 8
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 0
  • 点击次数: 0
  • 依赖项目数: 0
  • 推荐数: 0

GitHub 信息

  • Stars: 0
  • Watchers: 0
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: MIT
  • 更新时间: 2025-10-23