Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CBP tickets #511

Merged
merged 8 commits into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
## Note on Patches/Pull Requests

1. Fork the project.
2. Make your feature addition or bug fix.
3. Add tests for it. This is important so that we don't break your improvement in a future version unintentionally.
Expand Down Expand Up @@ -45,7 +46,8 @@ Class docblocks should contain:
* A short description of the class
* Any methods available that are called via magic method with what that method returns.

A good example is
A good example is:

``` php
/**
* Client class, base level access
Expand All @@ -56,17 +58,17 @@ A good example is
*/
```


#### Methods

Method docblocks should contain:

* A short description of what the method does.
* The parameters passed with what type to expect.
* Description of the parameters passed with examples(optional).
* The type of the return.
* All the possible exceptions the method may throw.

A good example of this is
A good example of this is:

``` php
/**
Expand All @@ -88,7 +90,7 @@ Class properties docblocs should contain:
* A short description of the property (optional)
* The var type

A good example of this
A good example is:

``` php
/**
Expand All @@ -99,6 +101,7 @@ A good example of this
```

### Arrays

The short notations for declaring arrays (`[]`) is preferred over the longer `array()`.

Align `=>`s following the longest key to make the arrays easier to read.
Expand Down Expand Up @@ -127,7 +130,6 @@ $lastRequestBody = 'example';
$lastResponseCode = 'something';
$lastResponseHeaders = 'test';
$lastResponseError = 'test2';

```

### Traits
Expand All @@ -146,6 +148,7 @@ $lastResponseError = 'test2';
When adding a resource, use traits to define available API calls. Resource traits are namespaced under `Zendesk\API\Traits\Resource`.

**Single Resource**

* Create
* Delete
* Find
Expand All @@ -154,6 +157,7 @@ When adding a resource, use traits to define available API calls. Resource trait
* Defaults - this adds **Find**, **FindAll**, **Create**, **Update**, and **Delete**

**Bulk traits**

* CreateMany
* DeleteMany
* FindMany
Expand Down
59 changes: 51 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

# Zendesk PHP API Client Library #

[![Build Status](https://travis-ci.org/zendesk/zendesk_api_client_php.svg?branch=master)](https://travis-ci.org/zendesk/zendesk_api_client_php)
![CI](https://github.com/zendesk/zendesk_api_client_php/actions/workflows/ci.yaml/badge.svg)
[![Latest Stable Version](https://poser.pugx.org/zendesk/zendesk_api_client_php/v/stable)](https://packagist.org/packages/zendesk/zendesk_api_client_php)
[![Total Downloads](https://poser.pugx.org/zendesk/zendesk_api_client_php/downloads)](https://packagist.org/packages/zendesk/zendesk_api_client_php)
[![Code Climate](https://codeclimate.com/github/zendesk/zendesk_api_client_php/badges/gpa.svg)](https://codeclimate.com/github/zendesk/zendesk_api_client_php)
Expand All @@ -29,6 +29,7 @@ The Zendesk PHP API client can be installed using [Composer](https://packagist.o
To install run `composer require zendesk/zendesk_api_client_php`

### Upgrading from V1 to V2

If you are upgrading from [v1](https://github.com/zendesk/zendesk_api_client_php/tree/v1) of the client, we've written an [upgrade guide](https://github.com/zendesk/zendesk_api_client_php/wiki/Upgrading-from-v1-to-v2) to highlight some of the key differences.

## Configuration
Expand Down Expand Up @@ -121,28 +122,70 @@ $tickets = $client->tickets()->sideload(['users', 'groups'])->findAll();
```

### Pagination
The Zendesk API offers a way to get the next pages for the requests and is documented in [the Zendesk Developer Documentation](https://developer.zendesk.com/rest_api/docs/core/introduction#pagination).

The way to do this is to pass it as an option to your request.
See the [API reference for pagination](https://developer.zendesk.com/api-reference/introduction/pagination).

There are two ways to do pagination in the Zendesk API, **CBP (Cursor Based Pagination)** and **OBP (Offset Based Pagination)**. The recommended and less limited way is to use CBP.

#### Iterator (recommended)

The use of the correct pagination is encapsulated using the iterator pattern, which allows you to retrieve all resources in all pages, without having to deal with pagination at all:

```php
$ticketsIterator = $client->tickets()->iterator();

foreach ($ticketsIterator as $ticket) {
process($ticket) // Your implementation
}
```

#### Find All using CBP (fine)

If you still want use `findAll()`, until CBP becomes the default API response, you must explicitly request CBP responses by using the param `page[size]`.

``` php
$tickets = $this->client->tickets()->findAll(['per_page' => 10, 'page' => 2]);
// CBP: /path?page[size]=100
$response = $client->tickets()->findAll(['page[size]' => 100]);
process($response->tickets); // Your implementation
do {
if ($response->meta->has_more) {
// CBP: /path?page[after]=cursor
$response = $client->tickets()->findAll(['page[after]' => $response->meta->after_cursor]);
process($response->tickets);
}
} while ($response->meta->has_more);
```

**Process data _immediately_ upon fetching**. This optimizes memory usage, enables real-time processing, and helps adhere to API rate limits, enhancing efficiency and user experience.

#### Find All using OBP (only recommended if the endpoint doesn't support CBP)

If CBP is not available, this is how you can fetch one page at a time:

```php
$pageSize = 100;
$pageNumber = 1;
do {
// OBP: /path?per_page=100&page=2
$response = $client->tickets()->findAll(['per_page' => $pageSize, 'page' => $pageNumber]);
process($response->tickets); // Your implementation
$pageNumber++;
} while (count($response->tickets) == $pageSize);
```

The allowed options are
* per_page
* page
* sort_order
**Process data _immediately_ upon fetching**. This optimizes memory usage, enables real-time processing, and helps adhere to API rate limits, enhancing efficiency and user experience.

### Retrying Requests

Add the `RetryHandler` middleware on the `HandlerStack` of your `GuzzleHttp\Client` instance. By default `Zendesk\Api\HttpClient`
retries:

* timeout requests
* those that throw `Psr\Http\Message\RequestInterface\ConnectException:class`
* and those that throw `Psr\Http\Message\RequestInterface\RequestException:class` that are identified as ssl issue.

#### Available options

Options are passed on `RetryHandler` as an array of values.

* max = 2 _limit of retries_
Expand Down
16 changes: 15 additions & 1 deletion src/Zendesk/API/Resources/Core/Tickets.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Zendesk\API\Traits\Resource\FindMany;
use Zendesk\API\Traits\Resource\UpdateMany;
use Zendesk\API\Traits\Utility\InstantiatorTrait;
use Zendesk\API\Traits\Utility\TicketsIterator;

/**
* The Tickets class exposes key methods for reading and updating ticket data
Expand Down Expand Up @@ -44,6 +45,19 @@ class Tickets extends ResourceAbstract
*/
protected $lastAttachments = [];

/**
* Usage:
* foreach ($ticketsIterator as $ticket) {
* process($ticket)
* }
*
* @return TicketsIterator Returns a new TicketsIterator object.
*/
public function iterator()
{
return new TicketsIterator($this);
}

/**
* {@inheritdoc}
*/
Expand Down Expand Up @@ -152,7 +166,7 @@ public function create(array $params)
$params['comment']['uploads'] = $this->lastAttachments;
$this->lastAttachments = [];
}

$extraOptions = [];
if (isset($params['async']) && ($params['async'] == true)) {
$extraOptions = [
Expand Down
119 changes: 119 additions & 0 deletions src/Zendesk/API/Traits/Utility/TicketsIterator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

namespace Zendesk\API\Traits\Utility;

use Iterator;

/**
* An iterator for fetching tickets from the Zendesk API using cursor-based pagination.
*/
class TicketsIterator implements Iterator
{
/**
* The default number of items per page for pagination.
*/
public const DEFAULT_PAGE_SIZE = 100;

/**
* @var Zendesk\API\HttpClient The Zendesk API client.
*/
private $resources;

/**
* @var int The current position in the tickets array.
*/
private $position = 0;

/**
* @var array The fetched tickets.
*/
private $tickets = [];

/**
* @var string|null The cursor for the next page of tickets.
*/
private $afterCursor = null;

/**
* @var int The number of tickets to fetch per page.
*/
private $pageSize;

/**
* @var bool A flag indicating whether the iterator has started fetching tickets.
*/
private $started = false;

/**
* TicketsIterator constructor.
*
* @param \stdClass $resources implementing the iterator ($this), with findAll()
* @param int $pageSize The number of tickets to fetch per page.
*/
public function __construct($resources, $pageSize = self::DEFAULT_PAGE_SIZE)
{
$this->resources = $resources;
$this->pageSize = $pageSize;
}

/**
* @return Ticket The current ticket, possibly fetching a new page.
*/
public function current()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this, valid, key and other methods are part of the PHP Iterator pattern.

{
if (!isset($this->tickets[$this->position]) && (!$this->started || $this->afterCursor)) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just noted this can be abstracted. I'll do that in my next PR.

$this->getPage();
}
return $this->tickets[$this->position];
}

/**
* @return int The current position.
*/
public function key()
{
return $this->position;
}

/**
* Moves to the next ticket.
*/
public function next()
{
++$this->position;
}

/**
* Rewinds to the first ticket.
*/
public function rewind()
{
$this->position = 0;
}

/**
* @return bool True there is a current element after calls to `rewind` or `next`, possibly fetching a new page.
*/
public function valid()
{
if (!isset($this->tickets[$this->position]) && (!$this->started || $this->afterCursor)) {
$this->getPage();
Copy link
Contributor Author

@ecoologic ecoologic Oct 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note we only fetch the next page if we're done with the current one. we don't want to fetch all pages and then do all the processing. This is also in the readme.

}
return isset($this->tickets[$this->position]);
}

/**
* Fetches the next page of tickets from the API.
*/
private function getPage()
{
$this->started = true;
$params = ['page[size]' => $this->pageSize];
if ($this->afterCursor) {
$params['page[after]'] = $this->afterCursor;
}
$response = $this->resources->findAll($params);
$this->tickets = array_merge($this->tickets, $response->tickets);
$this->afterCursor = $response->meta->has_more ? $response->meta->after_cursor : null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

namespace Zendesk\API\UnitTests\Core;

use Zendesk\API\UnitTests\BasicTest;
use Zendesk\API\Traits\Utility\TicketsIterator;

class MockTickets {
public function findAll($params)
{
static $callCount = 0;

// Simulate two pages of tickets
$tickets = $callCount === 0
? [['id' => 1], ['id' => 2]]
: [['id' => 3], ['id' => 4]];

// Simulate a cursor for the next page on the first call
$afterCursor = $callCount === 0 ? 'cursor_for_next_page' : null;

$callCount++;

return (object) [
'tickets' => $tickets,
'meta' => (object) [
'has_more' => $afterCursor !== null,
'after_cursor' => $afterCursor,
],
];
}
}

class TicketsIteratorTest extends BasicTest
{
public function testFetchesTickets()
{
$mockTickets = new MockTickets;
$iterator = new TicketsIterator($mockTickets, 2);

$tickets = iterator_to_array($iterator);

$this->assertEquals([['id' => 1], ['id' => 2], ['id' => 3], ['id' => 4]], $tickets);
}
}
Loading