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

Introducing the low-level API #324

Merged
merged 11 commits into from
Oct 4, 2023
7 changes: 5 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- New class `Redmine\Serializer\PathSerializer` to build an URL path with query parameters.
- New class `Redmine\Serializer\JsonSerializer` to encode or normalize JSON data.
- New class `Redmine\Serializer\XmlSerializer` to encode or normalize XML data.
- Allow `Psr\Http\Message\RequestFactoryInterface` as Argument #2 ($requestFactory) in `Redmine\Client\Psr18Client::__construct()`
- Added support for PHP 8.2

### Deprecated

- Providing Argument #2 ($requestFactory) in `Redmine\Client\Psr18Client::__construct()` as type `Psr\Http\Message\ServerRequestFactoryInterface` is deprecated, provide as type `Psr\Http\Message\RequestFactoryInterface` instead
- `Redmine\Api\AbstractApi::attachCustomFieldXML()` is deprecated
- `Redmine\Api\Project::prepareParamsXml()` is deprecated
- `Redmine\Api\AbstractApi::attachCustomFieldXML()` is deprecated, use `Redmine\Serializer\XmlSerializer::createFromArray()` instead
- `Redmine\Api\Project::prepareParamsXml()` is deprecated, use `Redmine\Serializer\XmlSerializer::createFromArray()` instead

## [v2.2.0](https://github.com/kbsali/php-redmine-api/compare/v2.1.1...v2.2.0) - 2022-03-01

Expand Down
51 changes: 16 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,41 +15,8 @@ Uses [Redmine API](http://www.redmine.org/projects/redmine/wiki/Rest_api/).
* Choose between using native `cURL` function or any
[PSR-18](https://www.php-fig.org/psr/psr-18/) http client like
[Guzzle](https://github.com/guzzle/guzzle) for handling http connections
* API entry points implementation state:
* :heavy_check_mark: Attachments
* :heavy_check_mark: Groups
* :heavy_check_mark: Custom Fields
* :heavy_check_mark: Issues
* :heavy_check_mark: Issue Categories
* :heavy_check_mark: Issue Priorities
* :x: *Issue Relations - only partially implemented*
* :heavy_check_mark: Issue Statuses
* :heavy_check_mark: News
* :heavy_check_mark: Projects
* :heavy_check_mark: Project Memberships
* :heavy_check_mark: Queries
* :heavy_check_mark: Roles
* :heavy_check_mark: Time Entries
* :heavy_check_mark: Time Entry Activities
* :heavy_check_mark: Trackers
* :heavy_check_mark: Users
* :heavy_check_mark: Versions
* :heavy_check_mark: Wiki

## Todo

* Check header's response code (especially for POST/PUT/DELETE requests)
* See http://stackoverflow.com/questions/9183178/php-curl-retrieving-response-headers-and-body-in-a-single-request/9183272#9183272

## Limitations / Missing Redmine-API

Redmine is missing some APIs for a full remote management of the data:
* List of activities & roles: http://www.redmine.org/issues/11464
* Closing a project: https://www.redmine.org/issues/13725

A possible solution to this would be to create an extra APIs implementing the
missing entry points. See existing effort in doing so:
https://github.com/rschobbert/redmine-miss-api
* [mid-level API](docs/usage.md#mid-level-api) e.g. `$client->getApi('issue')->create($data)`
* [low-level API](docs/usage.md#low-level-api) e.g. `$client->requestPost('/issues.json', $data)`

## Requirements

Expand All @@ -65,6 +32,20 @@ https://github.com/rschobbert/redmine-miss-api
* The PHP [cURL](http://php.net/manual/en/book.curl.php) extension if you want to use the native `cURL` functions.
* [PHPUnit](https://phpunit.de/) >= 9.0 (optional) to run the test suite

## Todo

* Check header's response code (especially for POST/PUT/DELETE requests)
* See http://stackoverflow.com/questions/9183178/php-curl-retrieving-response-headers-and-body-in-a-single-request/9183272#9183272

## Limitations / Missing Redmine-API

Redmine is missing some APIs for a full remote management of the data:
* List of activities & roles: http://www.redmine.org/issues/11464

A possible solution to this would be to create an extra APIs implementing the
missing entry points. See existing effort in doing so:
https://github.com/rschobbert/redmine-miss-api

## Install

### Composer
Expand Down
102 changes: 101 additions & 1 deletion docs/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,9 @@ try {

## API

You can now use the `getApi()` method to create and get a specific Redmine API.
### Mid-level API

You can now use the `getApi()` method to create and get a specific Redmine API. This simplifies common use-cases and gives you some features like caching and assigning a user to an issue by username instead of the user ID.

To check for failed requests you can afterwards check the status code via `$client->getLastResponseStatusCode()`.

Expand Down Expand Up @@ -553,3 +555,101 @@ Array
// Search
$client->getApi('search')->search('Myproject', ['limit' => 100]);
```

#### API entry points implementation state:

* :heavy_check_mark: Attachments
* :heavy_check_mark: Groups
* :heavy_check_mark: Custom Fields
* :heavy_check_mark: Issues
* :heavy_check_mark: Issue Categories
* :heavy_check_mark: Issue Priorities
* :x: *Issue Relations - only partially implemented*
* :heavy_check_mark: Issue Statuses
* :heavy_check_mark: News
* :heavy_check_mark: Projects
* :heavy_check_mark: Project Memberships
* :heavy_check_mark: Queries
* :heavy_check_mark: Roles
* :heavy_check_mark: Time Entries
* :heavy_check_mark: Time Entry Activities
* :heavy_check_mark: Trackers
* :heavy_check_mark: Users
* :heavy_check_mark: Versions
* :heavy_check_mark: Wiki

If some features are missing in `getApi()` you are welcome to create a PR. Besides, it is always possible to use the low-level API.

### Low-level API

The low-level API allows you to send highly customized requests to the Redmine server.

> :bulb: See the [Redmine REST-API docs](https://www.redmine.org/projects/redmine/wiki/Rest_api) for available endpoints and required parameters.

The client has 4 methods for requests:

- `requestGet()`
- `requestPost()`
- `requestPut()`
- `requestDelete()`

Using this methods you can use every Redmine API endpoint. The following example shows you how to rename a project and add a custom field. To build the XML body you can use the `XmlSerializer`.

```php
$client->requestPut(
'/projects/1.xml',
(string) \Redmine\Serializer\XmlSerializer::createFromArray([
'project' => [
'name' => 'renamed project',
'custom_fields' => [
[
'id' => 123,
'name' => 'cf_name',
'field_format' => 'string',
'value' => [1, 2, 3],
],
],
]
])
);
```

> :bulb: Use `\Redmine\Serializer\JsonSerializer` if you want to use the JSON endpoint.

Or to fetch data with complex query parameters you can use `requestGet()` with the `PathSerializer`:

```php
$client->requestGet(
(string) \Redmine\Serializer\PathSerializer::create(
'/time_entries.json',
[
'f' => ['spent_on'],
'op' => ['spent_on' => '><'],
'v' => [
'spent_on' => [
'2016-01-18',
'2016-01-22',
],
],
],
)
);
```

After the request you can use these 3 methods to work with the response:

- `getLastResponseStatusCode()`
- `getLastResponseContentType()`
- `getLastResponseBody()`

To parse the response body from the last example to an array you can use `XmlSerializer`:

```php
if ($client->getLastResponseStatusCode() === 200) {
$responseAsArray = \Redmine\Serializer\XmlSerializer::createFromString(
$client->getLastResponseBody()
)->getNormalized();
}
```

> :bulb: Use `\Redmine\Serializer\JsonSerializer` if you have send the request as JSON.
12 changes: 6 additions & 6 deletions src/Redmine/Api/AbstractApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,12 @@ public function __construct(Client $client)
*
* @return bool
*
* @deprecated This method does not correctly handle 2xx codes that are not 200 or 201, use \Redmine\Client\Client::getLastResponseStatusCode() instead
* @deprecated since v2.1.0, because it does not correctly handle 2xx codes that are not 200 or 201, use \Redmine\Client\Client::getLastResponseStatusCode() instead
* @see Client::getLastResponseStatusCode() for checking the status code directly
*/
public function lastCallFailed()
{
@trigger_error('The '.__METHOD__.' method is deprecated, use \Redmine\Client\Client::getLastResponseStatusCode() instead.', E_USER_DEPRECATED);
@trigger_error('`'.__METHOD__.'()` is deprecated since v2.1.0, use \Redmine\Client\Client::getLastResponseStatusCode() instead.', E_USER_DEPRECATED);

$code = $this->client->getLastResponseStatusCode();

Expand Down Expand Up @@ -164,7 +164,7 @@ protected function sanitizeParams(array $defaults, array $params)
* Retrieves all the elements of a given endpoint (even if the
* total number of elements is greater than 100).
*
* @deprecated the `retrieveAll()` method is deprecated, use `retrieveData()` instead
* @deprecated since v2.2.0, use `retrieveData()` instead
*
* @param string $endpoint API end point
* @param array $params optional parameters to be passed to the api (offset, limit, ...)
Expand All @@ -173,7 +173,7 @@ protected function sanitizeParams(array $defaults, array $params)
*/
protected function retrieveAll($endpoint, array $params = [])
{
@trigger_error('The '.__METHOD__.' method is deprecated, use `retrieveData()` instead.', E_USER_DEPRECATED);
@trigger_error('`'.__METHOD__.'()` is deprecated since v2.2.0, use `retrieveData()` instead.', E_USER_DEPRECATED);

try {
$data = $this->retrieveData(strval($endpoint), $params);
Expand Down Expand Up @@ -253,7 +253,7 @@ protected function retrieveData(string $endpoint, array $params = []): array
/**
* Attaches Custom Fields to a create/update query.
*
* @deprecated the `attachCustomFieldXML()` method is deprecated.
* @deprecated since v2.3.0, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead
*
* @param SimpleXMLElement $xml XML Element the custom fields are attached to
* @param array $fields array of fields to attach, each field needs name, id and value set
Expand All @@ -264,7 +264,7 @@ protected function retrieveData(string $endpoint, array $params = []): array
*/
protected function attachCustomFieldXML(SimpleXMLElement $xml, array $fields)
{
@trigger_error('The '.__METHOD__.' method is deprecated.', E_USER_DEPRECATED);
@trigger_error('`'.__METHOD__.'()` is deprecated since v2.3.0, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead.', E_USER_DEPRECATED);

$_fields = $xml->addChild('custom_fields');
$_fields->addAttribute('type', 'array');
Expand Down
4 changes: 2 additions & 2 deletions src/Redmine/Api/Project.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,15 +156,15 @@ public function update($id, array $params)
}

/**
* @deprecated the `prepareParamsXml()` method is deprecated, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead.
* @deprecated since v2.3.0, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead.
*
* @param array $params
*
* @return \SimpleXMLElement
*/
protected function prepareParamsXml($params)
{
@trigger_error('The '.__METHOD__.' method is deprecated, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead.', E_USER_DEPRECATED);
@trigger_error('`'.__METHOD__.'()` is deprecated since v2.3.0, use `\Redmine\Serializer\XmlSerializer::createFromArray()` instead.', E_USER_DEPRECATED);

return new \SimpleXMLElement(
XmlSerializer::createFromArray(['project' => $params])->getEncoded()
Expand Down
10 changes: 7 additions & 3 deletions src/Redmine/Serializer/JsonSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,12 @@

use JsonException;
use Redmine\Exception\SerializerException;
use Stringable;

/**
* JsonSerializer.
*
* @internal
*/
final class JsonSerializer
final class JsonSerializer implements Stringable
{
/**
* @throws SerializerException if $data is not valid JSON
Expand Down Expand Up @@ -57,6 +56,11 @@ public function getEncoded(): string
return $this->encoded;
}

public function __toString(): string
{
return $this->getEncoded();
}

private function decode(string $encoded): void
{
$this->encoded = $encoded;
Expand Down
13 changes: 9 additions & 4 deletions src/Redmine/Serializer/PathSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

namespace Redmine\Serializer;

use Stringable;

/**
* PathSerializer.
*
* @internal
* PathSerializer to handle query parameters.
*/
final class PathSerializer
final class PathSerializer implements Stringable
{
public static function create(string $path, array $queryParams = []): self
{
Expand Down Expand Up @@ -40,4 +40,9 @@ public function getPath(): string

return $this->path.$queryString;
}

public function __toString(): string
{
return $this->getPath();
}
}
10 changes: 7 additions & 3 deletions src/Redmine/Serializer/XmlSerializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@
use JsonException;
use Redmine\Exception\SerializerException;
use SimpleXMLElement;
use Stringable;
use Throwable;

/**
* XmlSerializer.
*
* @internal
*/
final class XmlSerializer
final class XmlSerializer implements Stringable
{
/**
* @throws SerializerException if $data is not valid XML
Expand Down Expand Up @@ -61,6 +60,11 @@ public function getEncoded(): string
return $this->encoded;
}

public function __toString(): string
{
return $this->getEncoded();
}

private function deserialize(string $encoded): void
{
$this->encoded = $encoded;
Expand Down
Loading