From b962ac60c2afdb0a9a8270853cd89289d2173095 Mon Sep 17 00:00:00 2001 From: Frank Schulze Date: Fri, 26 Jan 2024 12:21:59 +0100 Subject: [PATCH 1/9] Start implementing Support for Ticket Linking REST API --- README.md | 31 ++++++++------ src/Resource/Links.php | 92 ++++++++++++++++++++++++++++++++++++++++++ src/ResourceType.php | 1 + 3 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 src/Resource/Links.php diff --git a/README.md b/README.md index d7d6f51..2371ab4 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,6 @@ Add the following to the "require" section of your project's composer.json file: "zammad/zammad-api-client-php": "^2.0" ``` -### Installing the API client's dependencies -Fetch the API client's code and its dependencies by updating your project's dependencies with composer: -``` -$ composer update -``` - -Once installed, you have to include the generated autoload.php into your project's code: -```php -require_once dirname(__DIR__).'/vendor/autoload.php'; -``` - ## How to use the API client ### Example code @@ -47,7 +36,7 @@ $client = new Client([ ]); ``` Besides using a combination of `username` and `password`, you can alternatively give an `http_token` or an `oauth2_token`. -**Important:** You have to activate API access in Zammad. +**Important:** You have to activate API access in Zammad. Should be active by default. ### Fetching a single Resource object To fetch a `Resource` object by ID, e. g. a ticket with ID 34, use the `Client` object: @@ -83,6 +72,7 @@ Additionally you can have a look at the REST interface documentation of Zammad: * [Ticket priorities](https://docs.zammad.org/en/latest/api/ticket-priority.html) * [Ticket states](https://docs.zammad.org/en/latest/api/ticket-state.html) * [Tags](https://docs.zammad.org/en/latest/api/tags.html) +* [Linking Tickets](https://docs.zammad.org/en/latest/api/ticket/links.html) #### Fetching a ticket's articles If you already have a ticket object, you can easily fetch its articles: @@ -244,6 +234,23 @@ use ZammadAPIClient\ResourceType; $tags = $client->resource( ResourceType::TAG )->search('my tag'); ``` +### Linking Tickets + +#### Linking two Tickets + +Zammad can link two or more Ticket objects. Allowed Link Types are `normal`, `parent` or `child`. + +```php +use ZammadAPIClient\ResourceType; + +// First parameter $sourceTicket is the Ticket that should be linked +// Second parameter $targetTicket is the Ticket that $sourceTicket should be linked to +// Third parameter is the LinkType the $sourceTicket will be linked to $targetTicket with. +$client->resource( ResourceType::LINKS )->add( $sourceTicket, $targetTicket, 'normal' ); +``` + + + ### Object import Besides the usual methods available for objects, there is also a method available to import these via CSV. Example for text module CSV import: diff --git a/src/Resource/Links.php b/src/Resource/Links.php new file mode 100644 index 0000000..9ca02ce --- /dev/null +++ b/src/Resource/Links.php @@ -0,0 +1,92 @@ + 'links', + 'add' => 'links/add', + // TODO: 'delete' => 'links/remove' + ]; + + const LINKTYPES = [ + 'normal', + 'parent', + 'child' + ]; + public function add(Ticket $source, Ticket $target, $type = 'normal') + { + $this->clearError(); + if(empty($source->getID()) || empty($target->getID())){ + $this->setError('Tickets not valid.'); + return []; + } + if(!in_array($type,self::LINKTYPES, true)){ + $this->setError('Linktype is not supported.'); + return []; + } + $data = [ + "link_type" => $type, + "link_object_target" => "Ticket", + "link_object_target_value" => $target->getValue('id'), + "link_object_source" => "Ticket", + "link_object_source_number" => $source->getValue('number') + ]; + $url = $this->getURL('add'); + $response = $this->getClient()->post( + $url, + $data, + [ + 'expand' => true + ] + ); + if($response->hasError()){ + $this->setError($response->getError()); + return $this; + } + $this->clearError(); + $this->setRemoteData($response->getData()); + $this->clearUnsavedValues(); + return $this; + } + + public function get($object_id) + { + $this->clearError(); + if(empty($object_id)){ + $this->setError('LinkID Object not given'); + return []; + } + + $url = $this->getURL('get'); + $response = $this->getClient()->post( + $url, + [ + "link_object" => "Ticket", + "link_object_value" => $object_id + ], + [ + 'expand' => true + ] + ); + if($response->hasError()){ + $this->setError($response->getError()); + return $this; + } + $this->clearError(); + $this->setRemoteData($response->getData()); + $this->clearUnsavedValues(); + return $this; + } + + public function delete() + { + $this->clearError(); + $this->setError('not yet supported.'); //TODO: implement delete(); + return []; + } + +} diff --git a/src/ResourceType.php b/src/ResourceType.php index aa5fb3c..2d9efcb 100644 --- a/src/ResourceType.php +++ b/src/ResourceType.php @@ -18,4 +18,5 @@ class ResourceType const TICKET = '\\ZammadAPIClient\\Resource\\Ticket'; const TICKET_ARTICLE = '\\ZammadAPIClient\\Resource\\TicketArticle'; const TAG = '\\ZammadAPIClient\\Resource\\Tag'; + const LINKS = '\\ZammadAPIClient\\Resource\\LINKS'; } From a12c88e98bab3fb299423babca99384a3f790dd4 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Tue, 2 Jun 2026 14:22:41 +0200 Subject: [PATCH 2/9] Update README.md with changes from commit 7f943516 --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2371ab4..3be9b18 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ This client supports Zammad 3.4.1 and newer. The API client needs [composer](https://getcomposer.org/). For installation have a look at its [documentation](https://getcomposer.org/download/). Additionally, the API client needs PHP 7.2 or newer. -### Integration into your project -Add the following to the "require" section of your project's composer.json file: -```json -"zammad/zammad-api-client-php": "^2.0" +### Installing the API client's dependencies +: +``` +$ composer require zammad/zammad-api-client-php ``` ## How to use the API client @@ -68,7 +68,7 @@ Additionally you can have a look at the REST interface documentation of Zammad: * [Groups](https://docs.zammad.org/en/latest/api/group.html) * [Organizations](https://docs.zammad.org/en/latest/api/organization.html) * [Tickets](https://docs.zammad.org/en/latest/api/ticket.html) - * [Ticket articles](https://docs.zammad.org/en/latest/api/ticket/articles.html) + * [Ticket articles](https://docs.zammad.org/en/latest/api/ticket-article.html) * [Ticket priorities](https://docs.zammad.org/en/latest/api/ticket-priority.html) * [Ticket states](https://docs.zammad.org/en/latest/api/ticket-state.html) * [Tags](https://docs.zammad.org/en/latest/api/tags.html) From 9f64a658dcb4c25c408533461534212a361943cd Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Tue, 2 Jun 2026 14:24:53 +0200 Subject: [PATCH 3/9] merge readme --- README.md | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 3be9b18..cecaa70 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,21 @@ This client supports Zammad 3.4.1 and newer. The API client needs [composer](https://getcomposer.org/). For installation have a look at its [documentation](https://getcomposer.org/download/). Additionally, the API client needs PHP 7.2 or newer. +### Integration into your project +Add the following to the "require" section of your project's composer.json file: +```json +"zammad/zammad-api-client-php": "^2.0" +``` + ### Installing the API client's dependencies -: +Fetch the API client's code and its dependencies by updating your project's dependencies with composer: +``` +$ composer update ``` -$ composer require zammad/zammad-api-client-php + +Once installed, you have to include the generated autoload.php into your project's code: +```php +require_once dirname(__DIR__).'/vendor/autoload.php'; ``` ## How to use the API client @@ -68,7 +79,7 @@ Additionally you can have a look at the REST interface documentation of Zammad: * [Groups](https://docs.zammad.org/en/latest/api/group.html) * [Organizations](https://docs.zammad.org/en/latest/api/organization.html) * [Tickets](https://docs.zammad.org/en/latest/api/ticket.html) - * [Ticket articles](https://docs.zammad.org/en/latest/api/ticket-article.html) + * [Ticket articles](https://docs.zammad.org/en/latest/api/ticket/articles.html) * [Ticket priorities](https://docs.zammad.org/en/latest/api/ticket-priority.html) * [Ticket states](https://docs.zammad.org/en/latest/api/ticket-state.html) * [Tags](https://docs.zammad.org/en/latest/api/tags.html) From 02e7a915b2b1b013984b2c48960920cf22bc319b Mon Sep 17 00:00:00 2001 From: Fran Rey Date: Thu, 16 Apr 2026 01:44:42 +0200 Subject: [PATCH 4/9] add: support for links with tests --- src/Resource/Link.php | 144 ++++++++++ test/ZammadAPIClient/Resource/LinkTest.php | 310 +++++++++++++++++++++ 2 files changed, 454 insertions(+) create mode 100644 src/Resource/Link.php create mode 100644 test/ZammadAPIClient/Resource/LinkTest.php diff --git a/src/Resource/Link.php b/src/Resource/Link.php new file mode 100644 index 0000000..cd2de39 --- /dev/null +++ b/src/Resource/Link.php @@ -0,0 +1,144 @@ + + */ + +namespace ZammadAPIClient\Resource; + +class Link extends AbstractResource +{ + const URLS = [ + 'get' => 'links', + 'add' => 'links/add', + 'remove' => 'links/remove' + ]; + + const LINKTYPES = [ + 'normal', + 'parent', + 'child' + ]; + + /** + * Fetches links of an object. + * + * @param int $object_id ID of the object to fetch links for. + * @param string $object_type Type of object to fetch links for (e. g. 'Ticket'). + * + * @return object This object. + */ + public function get($object_id, $object_type = 'Ticket') + { + $this->clearError(); + + $object_id = intval($object_id); + if ( empty($object_id) ) { + throw new \RuntimeException('Missing object ID'); + } + + $response = $this->getClient()->get( + $this->getURL('get'), + [ + 'link_object' => $object_type, + 'link_object_value' => $object_id, + ] + ); + + if ( $response->hasError() ) { + $this->setError( $response->getError() ); + } + else { + $this->clearError(); + $this->setRemoteData( $response->getData() ); + } + + return $this; + } + + /** + * Adds a link between two tickets. + * + * @param Ticket $source Source ticket object. + * @param Ticket $target Target ticket object. + * @param string $type Link type (default: 'normal'). + * + * @return object This object. + */ + public function add(Ticket $source, Ticket $target, $type = 'normal') + { + $this->clearError(); + + if ( empty($source->getID()) || empty($target->getID()) ) { + $this->setError('Tickets not valid.'); + return $this; + } + if ( !in_array($type, self::LINKTYPES, true) ) { + $this->setError('Linktype is not supported.'); + return $this; + } + + $response = $this->getClient()->post( + $this->getURL('add'), + [ + 'link_type' => $type, + 'link_object_target' => 'Ticket', + 'link_object_target_value' => $target->getID(), + 'link_object_source' => 'Ticket', + 'link_object_source_number' => $source->getValue('number') + ] + ); + + if ( $response->hasError() ) { + $this->setError( $response->getError() ); + } + + return $this; + } + + /** + * Removes a link between two tickets. + * + * @param Ticket $source Source ticket object. + * @param Ticket $target Target ticket object. + * @param string $type Link type (default: 'normal'). + * + * @return object This object. + */ + public function remove(Ticket $source, Ticket $target, $type = 'normal') + { + $this->clearError(); + + if ( empty($source->getID()) || empty($target->getID()) ) { + $this->setError('Tickets not valid.'); + return $this; + } + if ( !in_array($type, self::LINKTYPES, true) ) { + $this->setError('Linktype is not supported.'); + return $this; + } + + $response = $this->getClient()->delete( + $this->getURL('remove'), + [ + 'link_type' => $type, + 'link_object_source' => 'Ticket', + 'link_object_source_value' => $source->getID(), + 'link_object_target' => 'Ticket', + 'link_object_target_value' => $target->getID() + ] + ); + + if ( $response->hasError() ) { + $this->setError( $response->getError() ); + return $this; + } + + $this->clearError(); + $this->clearRemoteData(); + $this->clearUnsavedValues(); + + return $this; + } +} diff --git a/test/ZammadAPIClient/Resource/LinkTest.php b/test/ZammadAPIClient/Resource/LinkTest.php new file mode 100644 index 0000000..3317969 --- /dev/null +++ b/test/ZammadAPIClient/Resource/LinkTest.php @@ -0,0 +1,310 @@ + 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_URL', + 'username' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_USERNAME', + 'password' => 'ZAMMAD_PHP_API_CLIENT_UNIT_TESTS_PASSWORD', + ]; + foreach ( $env_keys as $config_key => $env_key ) { + $value = getenv( $env_key ); + if ( empty($value) ) { + throw new \RuntimeException("Missing environment variable $env_key"); + } + + $client_config[$config_key] = $value; + } + + self::$client = new Client($client_config); + } + + public function setUp(): void + { + parent::setUp(); + self::createTickets(); + } + + public function tearDown(): void + { + parent::tearDown(); + self::deleteTickets(); + } + + public static function getClient() + { + return self::$client; + } + + protected static function getUniqueID() + { + return uniqid('', true); + } + + private static function createTickets() + { + self::$source_ticket = self::getClient()->resource(ResourceType::TICKET); + self::$source_ticket->setValues([ + 'group_id' => 1, + 'priority_id' => 1, + 'state_id' => 1, + 'title' => 'Unit test link source ticket ' . self::getUniqueID(), + 'customer_id' => 1, + 'article' => [ + 'subject' => 'Unit test article 1 ' . self::getUniqueID(), + 'body' => 'Unit test article 1... ' . self::getUniqueID(), + ], + ]); + self::$source_ticket->save(); + + self::$target_ticket = self::getClient()->resource(ResourceType::TICKET); + self::$target_ticket->setValues([ + 'group_id' => 1, + 'priority_id' => 1, + 'state_id' => 1, + 'title' => 'Unit test link target ticket ' . self::getUniqueID(), + 'customer_id' => 1, + 'article' => [ + 'subject' => 'Unit test article 2 ' . self::getUniqueID(), + 'body' => 'Unit test article 2... ' . self::getUniqueID(), + ], + ]); + self::$target_ticket->save(); + } + + private static function deleteTickets() + { + if (!empty(self::$source_ticket)) { + self::$source_ticket->delete(); + } + if (!empty(self::$target_ticket)) { + self::$target_ticket->delete(); + } + } + + public function testGetWithoutObjectId() + { + $link = self::getClient()->resource(ResourceType::LINK); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Missing object ID'); + + $link->get('', 'Ticket'); + } + + public function testGetWithInvalidObjectId() + { + $link = self::getClient()->resource(ResourceType::LINK); + + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Missing object ID'); + + $link->get(0, 'Ticket'); + } + + public function testGet() + { + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->get(self::$source_ticket->getID(), 'Ticket'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError()); + } + + public function testGetByTicketId() + { + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->get(self::$source_ticket->getID()); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError()); + } + + public function testAddWithoutSourceTicket() + { + $link = self::getClient()->resource(ResourceType::LINK); + + $this->expectException(\TypeError::class); + + $link->add(null, null); + } + + public function testAddWithoutTargetTicket() + { + $link = self::getClient()->resource(ResourceType::LINK); + + $this->expectException(\TypeError::class); + + $link->add(self::$source_ticket, null); + } + + public function testAdd() + { + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->add(self::$source_ticket, self::$target_ticket, 'normal'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError()); + } + + public function testAddWithParentLinkType() + { + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->add(self::$source_ticket, self::$target_ticket, 'parent'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError()); + } + + public function testAddWithChildLinkType() + { + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->add(self::$source_ticket, self::$target_ticket, 'child'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError()); + } + + public function testAddWithInvalidLinkType() + { + $link = self::getClient()->resource(ResourceType::LINK); + $link->add(self::$source_ticket, self::$target_ticket, 'invalid_type'); + + $this->assertNotEmpty($link->getError()); + $this->assertSame('Linktype is not supported.', $link->getError()); + } + + public function testAddWithInvalidTickets() + { + $source_ticket = self::getClient()->resource(ResourceType::TICKET); + $target_ticket = self::getClient()->resource(ResourceType::TICKET); + + $link = self::getClient()->resource(ResourceType::LINK); + $link->add($source_ticket, $target_ticket); + + $this->assertNotEmpty($link->getError()); + $this->assertSame('Tickets not valid.', $link->getError()); + } + + public function testRemoveWithoutSourceTicket() + { + $link = self::getClient()->resource(ResourceType::LINK); + + $this->expectException(\TypeError::class); + + $link->remove(null, null); + } + + public function testRemoveWithoutTargetTicket() + { + $link = self::getClient()->resource(ResourceType::LINK); + + $this->expectException(\TypeError::class); + + $link->remove(self::$source_ticket, null); + } + + public function testRemoveWithInvalidLinkType() + { + $link = self::getClient()->resource(ResourceType::LINK); + $link->remove(self::$source_ticket, self::$target_ticket, 'invalid_type'); + + $this->assertNotEmpty($link->getError()); + $this->assertSame('Linktype is not supported.', $link->getError()); + } + + public function testRemoveWithInvalidTickets() + { + $source_ticket = self::getClient()->resource(ResourceType::TICKET); + $target_ticket = self::getClient()->resource(ResourceType::TICKET); + + $link = self::getClient()->resource(ResourceType::LINK); + $link->remove($source_ticket, $target_ticket); + + $this->assertNotEmpty($link->getError()); + $this->assertSame('Tickets not valid.', $link->getError()); + } + + public function testRemove() + { + $link = self::getClient()->resource(ResourceType::LINK); + $link->add(self::$source_ticket, self::$target_ticket, 'normal'); + + $this->assertFalse($link->hasError()); + + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->remove(self::$source_ticket, self::$target_ticket, 'normal'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError(), 'Remove error: ' . $link->getError()); + } + + public function testAddAndVerify() + { + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->add(self::$source_ticket, self::$target_ticket, 'normal'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError()); + + $link = self::getClient()->resource(ResourceType::LINK); + $link->get(self::$source_ticket->getID(), 'Ticket'); + + $links = $link->getValue('links'); + $this->assertIsArray($links); + + $found_link = false; + foreach ($links as $linked_ticket) { + if ($linked_ticket['link_object_value'] == self::$target_ticket->getID() + && $linked_ticket['link_type'] === 'normal') { + $found_link = true; + break; + } + } + $this->assertTrue($found_link, 'Link was not found after add operation'); + } + + public function testRemoveAndVerify() + { + $link = self::getClient()->resource(ResourceType::LINK); + $link->add(self::$source_ticket, self::$target_ticket, 'normal'); + + $this->assertFalse($link->hasError()); + + $link = self::getClient()->resource(ResourceType::LINK); + $result = $link->remove(self::$source_ticket, self::$target_ticket, 'normal'); + + $this->assertSame($link, $result); + $this->assertFalse($link->hasError(), 'Remove error: ' . $link->getError()); + + $link = self::getClient()->resource(ResourceType::LINK); + $link->get(self::$source_ticket->getID(), 'Ticket'); + + $links = $link->getValue('links'); + $this->assertIsArray($links); + + $found_link = false; + foreach ($links as $linked_ticket) { + if ($linked_ticket['link_object_value'] == self::$target_ticket->getID() + && $linked_ticket['link_type'] === 'normal') { + $found_link = true; + break; + } + } + $this->assertFalse($found_link, 'Link was still found after remove operation'); + } +} From 6f6b6a3f8516d82a48d149f37ae74a7c99e685d3 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Wed, 3 Jun 2026 08:02:52 +0200 Subject: [PATCH 5/9] Add Link resource for ticket linking support --- .gitignore | 1 + README.md | 5 +-- src/Resource/Links.php | 92 ------------------------------------------ src/ResourceType.php | 2 +- 4 files changed, 4 insertions(+), 96 deletions(-) delete mode 100644 src/Resource/Links.php diff --git a/.gitignore b/.gitignore index d4363f5..84192e7 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ /examples/test.php /test*.php *~ +.phpunit.result.cache \ No newline at end of file diff --git a/README.md b/README.md index cecaa70..fd132e7 100644 --- a/README.md +++ b/README.md @@ -257,11 +257,9 @@ use ZammadAPIClient\ResourceType; // First parameter $sourceTicket is the Ticket that should be linked // Second parameter $targetTicket is the Ticket that $sourceTicket should be linked to // Third parameter is the LinkType the $sourceTicket will be linked to $targetTicket with. -$client->resource( ResourceType::LINKS )->add( $sourceTicket, $targetTicket, 'normal' ); +$client->resource( ResourceType::LINK )->add( $sourceTicket, $targetTicket, 'normal' ); ``` - - ### Object import Besides the usual methods available for objects, there is also a method available to import these via CSV. Example for text module CSV import: @@ -326,6 +324,7 @@ $client->resource( ResourceType::TICKET ); | GROUP|✔|✔|–|✔|✔|–|–|–| | USER|✔|✔|✔|✔|✔|–|–|✔| | TAG|✔|–|✔|–|–|✔|✔|–| +| LINK|✔|–|–|–|–|✔|✔|–| ## Publishing diff --git a/src/Resource/Links.php b/src/Resource/Links.php deleted file mode 100644 index 9ca02ce..0000000 --- a/src/Resource/Links.php +++ /dev/null @@ -1,92 +0,0 @@ - 'links', - 'add' => 'links/add', - // TODO: 'delete' => 'links/remove' - ]; - - const LINKTYPES = [ - 'normal', - 'parent', - 'child' - ]; - public function add(Ticket $source, Ticket $target, $type = 'normal') - { - $this->clearError(); - if(empty($source->getID()) || empty($target->getID())){ - $this->setError('Tickets not valid.'); - return []; - } - if(!in_array($type,self::LINKTYPES, true)){ - $this->setError('Linktype is not supported.'); - return []; - } - $data = [ - "link_type" => $type, - "link_object_target" => "Ticket", - "link_object_target_value" => $target->getValue('id'), - "link_object_source" => "Ticket", - "link_object_source_number" => $source->getValue('number') - ]; - $url = $this->getURL('add'); - $response = $this->getClient()->post( - $url, - $data, - [ - 'expand' => true - ] - ); - if($response->hasError()){ - $this->setError($response->getError()); - return $this; - } - $this->clearError(); - $this->setRemoteData($response->getData()); - $this->clearUnsavedValues(); - return $this; - } - - public function get($object_id) - { - $this->clearError(); - if(empty($object_id)){ - $this->setError('LinkID Object not given'); - return []; - } - - $url = $this->getURL('get'); - $response = $this->getClient()->post( - $url, - [ - "link_object" => "Ticket", - "link_object_value" => $object_id - ], - [ - 'expand' => true - ] - ); - if($response->hasError()){ - $this->setError($response->getError()); - return $this; - } - $this->clearError(); - $this->setRemoteData($response->getData()); - $this->clearUnsavedValues(); - return $this; - } - - public function delete() - { - $this->clearError(); - $this->setError('not yet supported.'); //TODO: implement delete(); - return []; - } - -} diff --git a/src/ResourceType.php b/src/ResourceType.php index 2d9efcb..4fe0f90 100644 --- a/src/ResourceType.php +++ b/src/ResourceType.php @@ -18,5 +18,5 @@ class ResourceType const TICKET = '\\ZammadAPIClient\\Resource\\Ticket'; const TICKET_ARTICLE = '\\ZammadAPIClient\\Resource\\TicketArticle'; const TAG = '\\ZammadAPIClient\\Resource\\Tag'; - const LINKS = '\\ZammadAPIClient\\Resource\\LINKS'; + const LINK = '\\ZammadAPIClient\\Resource\\Link'; } From 6b544105ce4b0c07c71a903d40ea6e14448ef1cc Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Wed, 3 Jun 2026 08:20:57 +0200 Subject: [PATCH 6/9] Fix link_object_source_value in remove() to use ticket number --- src/Resource/Link.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Resource/Link.php b/src/Resource/Link.php index cd2de39..40c4b17 100644 --- a/src/Resource/Link.php +++ b/src/Resource/Link.php @@ -101,8 +101,8 @@ public function add(Ticket $source, Ticket $target, $type = 'normal') * Removes a link between two tickets. * * @param Ticket $source Source ticket object. - * @param Ticket $target Target ticket object. - * @param string $type Link type (default: 'normal'). + * @param Ticket $target Target ticket object. + * @param string $type Link type (default: 'normal'). * * @return object This object. */ @@ -124,7 +124,7 @@ public function remove(Ticket $source, Ticket $target, $type = 'normal') [ 'link_type' => $type, 'link_object_source' => 'Ticket', - 'link_object_source_value' => $source->getID(), + 'link_object_source_value' => $source->getValue('number'), 'link_object_target' => 'Ticket', 'link_object_target_value' => $target->getID() ] From 8fda0900a7fa67ecbf2591ef55b797b5c6087902 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Wed, 3 Jun 2026 08:32:58 +0200 Subject: [PATCH 7/9] Fix link_object_source_value in remove() to use post instead of delete --- src/Resource/Link.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Resource/Link.php b/src/Resource/Link.php index 40c4b17..88148e9 100644 --- a/src/Resource/Link.php +++ b/src/Resource/Link.php @@ -119,7 +119,7 @@ public function remove(Ticket $source, Ticket $target, $type = 'normal') return $this; } - $response = $this->getClient()->delete( + $response = $this->getClient()->post( $this->getURL('remove'), [ 'link_type' => $type, From 38811822a740bf7c6fbaf4a38b63a1bcf943760e Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Wed, 3 Jun 2026 08:48:01 +0200 Subject: [PATCH 8/9] revert delete ticket linking --- src/Resource/Link.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Resource/Link.php b/src/Resource/Link.php index 88148e9..a6db72b 100644 --- a/src/Resource/Link.php +++ b/src/Resource/Link.php @@ -119,12 +119,13 @@ public function remove(Ticket $source, Ticket $target, $type = 'normal') return $this; } - $response = $this->getClient()->post( + $response = $this->getClient()->delete( $this->getURL('remove'), + [], [ 'link_type' => $type, 'link_object_source' => 'Ticket', - 'link_object_source_value' => $source->getValue('number'), + 'link_object_source_value' => $source->getID(), 'link_object_target' => 'Ticket', 'link_object_target_value' => $target->getID() ] From 761a18b33d2a43c3feb6fdbba5b8fc7a00ed1688 Mon Sep 17 00:00:00 2001 From: Rene Reimann Date: Wed, 3 Jun 2026 09:03:19 +0200 Subject: [PATCH 9/9] fix: send link remove payload as JSON body via delete() --- src/Client.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index a8789fc..9361112 100644 --- a/src/Client.php +++ b/src/Client.php @@ -165,12 +165,18 @@ public function put( $url, array $data = [], array $url_parameters = [] ) * * @return Response object */ - public function delete( $url, array $url_parameters = [] ) + public function delete( $url, array $url_parameters = [], array $data = [] ) { + $options = []; + if ( !empty($data) ) { + $options['json'] = $data; + } + $response = $this->request( 'DELETE', $url, - $url_parameters + $url_parameters, + $options ); return $response;