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

DRUP-574 Simulate pagination if CPS is not enabled. #43

Merged
merged 7 commits into from
Feb 4, 2019
Merged
Show file tree
Hide file tree
Changes from 6 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ We are planning to add Monetization API support to this library in the near futu
The [Apigee Monetization APIs](https://apidocs.apigee.com/api-reference/content/monetization-apis) have been added to this library but are
considered to be an alpha. If you run into any problems, add an issue to our [GitHub issue queue](https://github.com/apigee/apigee-client-php/issues).

## Edge for Private Cloud
[Core Persistent Services (CPS)](https://docs.apigee.com/api-platform/reference/cps) is not yet available on Private Cloud installations.
mxr576 marked this conversation as resolved.
Show resolved Hide resolved
The PHP API client supports to leverage pagination on listing API endpoints (ex.: [List Developers](https://apidocs.apigee.com/management/apis/get/organizations/%7Borg_name%7D/developers)). If CPS is not available the PHP API client simulates the pagination feature and it triggers an E_USER_NOTICE level error to let developers know that the paginated result is generated by PHP and not the Management API server.
mxr576 marked this conversation as resolved.
Show resolved Hide resolved
This notice can be suppressed in multiple ways. You can suppress it by changing PHP's `error_reporting` configuration to
suppress _all_ E_NOTICE level errors with changing its value to `E_ALL | ~E_NOTICE` for example. You can also suppress only the notice generated by the PHP API client by setting the `APIGEE_EDGE_PHP_CLIENT_SUPPRESS_CPS_SIMULATION_NOTICE` environment variable value to a falsy value, for example: `APIGEE_EDGE_PHP_CLIENT_SUPPRESS_CPS_SIMULATION_NOTICE=1`.

## Installing the client library

You must install an HTTP client or adapter before you install the Apigee API Client Library for PHP. For a complete list
Expand Down Expand Up @@ -145,6 +151,8 @@ APIGEE_EDGE_PHP_CLIENT_BASIC_AUTH_USER=[YOUR-EMAIL-ADDRESS@HOST.COM]
APIGEE_EDGE_PHP_CLIENT_BASIC_AUTH_PASSWORD=[PASSWORD]
APIGEE_EDGE_PHP_CLIENT_ORGANIZATION=[ORGANIZATION]
APIGEE_EDGE_PHP_CLIENT_ENVIRONMENT=[ENVIRONMENT]
# If test organization does not support CPS.
APIGEE_EDGE_PHP_CLIENT_SUPPRESS_CPS_SIMULATION_NOTICE=1
```

There are multiple ways to set these environment variables, but probably the easiest is creating a copy from the
Expand Down
239 changes: 205 additions & 34 deletions src/Controller/PaginationHelperTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@

namespace Apigee\Edge\Controller;

use Apigee\Edge\Exception\CpsNotEnabledException;
use Apigee\Edge\Exception\ClientErrorException;
use Apigee\Edge\Exception\RuntimeException;
use Apigee\Edge\Structure\PagerInterface;
use Psr\Http\Message\ResponseInterface;

Expand All @@ -39,12 +40,6 @@ trait PaginationHelperTrait
*/
public function createPager(int $limit = 0, ?string $startKey = null): PagerInterface
{
/** @var \Apigee\Edge\Api\Management\Entity\OrganizationInterface $organization */
$organization = $this->getOrganizationController()->load($this->getOrganisationName());
if (!$organization->getPropertyValue('features.isCpsEnabled')) {
throw new CpsNotEnabledException($this->getOrganisationName());
}

// Create an anonymous class here because this class should not exist and be in use
// in those controllers that do not work with entities that belongs to an organization.
$pager = new class() implements PagerInterface {
Expand Down Expand Up @@ -107,14 +102,78 @@ public function setLimit(int $limit): int
*
* @return \Apigee\Edge\Entity\EntityInterface[]
* Array of entity objects.
*/
protected function listEntities(PagerInterface $pager = null, array $query_params = [], string $key_provider = 'id'): array
{
/** @var \Apigee\Edge\Api\Management\Entity\OrganizationInterface $organization */
$organization = $this->getOrganizationController()->load($this->getOrganisationName());
$isCpsEnabled = $organization->getPropertyValue('features.isCpsEnabled');

if ($isCpsEnabled) {
return $this->listEntitiesWithCps($pager, $query_params, $key_provider);
} else {
$this->triggerCpsSimulationNotice($pager);

return $this->listEntitiesWithoutCps($pager, $query_params, $key_provider);
}
}

/**
* Loads entity ids from Apigee Edge.
*
* @param \Apigee\Edge\Structure\PagerInterface|null $pager
* Pager.
* @param array $query_params
* Additional query parameters.
*
* @return string[]
* Array of entity ids.
*/
protected function listEntityIds(PagerInterface $pager = null, array $query_params = []): array
{
/** @var \Apigee\Edge\Api\Management\Entity\OrganizationInterface $organization */
$organization = $this->getOrganizationController()->load($this->getOrganisationName());
$isCpsEnabled = $organization->getPropertyValue('features.isCpsEnabled');

if ($isCpsEnabled) {
return $this->listEntityIdsWithCps($pager, $query_params);
} else {
$this->triggerCpsSimulationNotice($pager);

return $this->listEntityIdsWithoutCps($pager, $query_params);
}
}

/**
* @inheritdoc
*/
abstract protected function responseToArray(ResponseInterface $response): array;

/**
* @inheritdoc
*/
abstract protected function responseArrayToArrayOfEntities(array $responseArray, string $keyGetter = 'id'): array;

/**
* Real paginated entity listing on organization with CPS support.
*
* @param \Apigee\Edge\Structure\PagerInterface|null $pager
* Pager.
* @param array $query_params
* Additional query parameters.
* @param string $key_provider
* Getter method on the entity that should provide a unique array key.
*
* @return \Apigee\Edge\Entity\EntityInterface[]
* Array of entity objects.
*
* @psalm-suppress PossiblyNullArrayOffset $tmp->id() is always not null here.
*/
protected function listEntities(PagerInterface $pager = null, array $query_params = [], string $key_provider = 'id'): array
private function listEntitiesWithCps(PagerInterface $pager = null, array $query_params = [], string $key_provider = 'id'): array
{
$query_params = [
'expand' => 'true',
] + $query_params;
'expand' => 'true',
] + $query_params;

if ($pager) {
$responseArray = $this->getResultsInRange($pager, $query_params);
Expand Down Expand Up @@ -163,7 +222,84 @@ protected function listEntities(PagerInterface $pager = null, array $query_param
}

/**
* Loads entity ids from Apigee Edge.
* Simulates paginated entity listing on organization without CPS support.
*
* For example, on on-prem installations.
*
* @param \Apigee\Edge\Structure\PagerInterface|null $pager
* Pager.
* @param array $query_params
* Additional query parameters.
* @param string $key_provider
* Getter method on the entity that should provide a unique array key.
*
* @return \Apigee\Edge\Entity\EntityInterface[]
* Array of entity objects.
*/
private function listEntitiesWithoutCps(PagerInterface $pager = null, array $query_params = [], string $key_provider = 'id'): array
{
$query_params = [
'expand' => 'true',
] + $query_params;

$uri = $this->getBaseEndpointUri()->withQuery(http_build_query($query_params));
$response = $this->getClient()->get($uri);
$responseArray = $this->responseToArray($response);
// Ignore entity type key from response, ex.: apiProduct.
$responseArray = reset($responseArray);

$entities = $this->responseArrayToArrayOfEntities($responseArray, $key_provider);

return $pager ? $this->simulateCpsPagination($pager, $entities, array_keys($entities)) : $entities;
}

/**
* Gets entities and entity ids in a provided range from Apigee Edge.
*
* This method for organizations with CPS enabled.
*
* @param \Apigee\Edge\Structure\PagerInterface $pager
* CPS limit object with configured startKey and limit.
* @param array $query_params
* Query parameters for the API call.
*
* @return array
* API response parsed as an array.
*/
private function getResultsInRange(PagerInterface $pager, array $query_params): array
{
$query_params['startKey'] = $pager->getStartKey();
// Do not add 0 unnecessarily to the query parameters.
if ($pager->getLimit() > 0) {
$query_params['count'] = $pager->getLimit();
}
$uri = $this->getBaseEndpointUri()->withQuery(http_build_query($query_params));
$response = $this->getClient()->get($uri);

return $this->responseToArray($response);
}

/**
* Triggers an E_USER_NOTICE if pagination is used in a non-CPS org.
*
* @param \Apigee\Edge\Structure\PagerInterface|null $pager
* Pager.
*/
private function triggerCpsSimulationNotice(PagerInterface $pager = null): void
{
// Trigger an E_USER_NOTICE error if pagination feature needs to
// be simulated on an organization without CPS to let developers
// know that the Apigee PHP API client executed a workaround.
// If suppressing all E_NOTICE level errors in an environment is
// undesired then setting the following environment variable to
// falsy value can also suppress this notice.
if ($pager && !getenv('APIGEE_EDGE_PHP_CLIENT_SUPPRESS_CPS_SIMULATION_NOTICE')) {
trigger_error('Apigee Edge PHP Client: Simulating CPS pagination on an organization that does not have CPS support. https://docs.apigee.com/api-platform/reference/cps', E_USER_NOTICE);
}
}

/**
* Real paginated entity id listing on organization with CPS support.
*
* @param \Apigee\Edge\Structure\PagerInterface|null $pager
* Pager.
Expand All @@ -173,11 +309,11 @@ protected function listEntities(PagerInterface $pager = null, array $query_param
* @return string[]
* Array of entity ids.
*/
protected function listEntityIds(PagerInterface $pager = null, array $query_params = []): array
private function listEntityIdsWithCps(PagerInterface $pager = null, array $query_params = []): array
{
$query_params = [
'expand' => 'false',
] + $query_params;
'expand' => 'false',
] + $query_params;
if ($pager) {
return $this->getResultsInRange($pager, $query_params);
} else {
Expand Down Expand Up @@ -207,36 +343,71 @@ protected function listEntityIds(PagerInterface $pager = null, array $query_para
}

/**
* @inheritdoc
* Simulates paginated entity id listing on organization without CPS.
*
* @param \Apigee\Edge\Structure\PagerInterface|null $pager
* Pager.
* @param array $query_params
* Additional query parameters.
*
* @return string[]
* Array of entity ids.
*/
abstract protected function responseToArray(ResponseInterface $response): array;
private function listEntityIdsWithoutCps(PagerInterface $pager = null, array $query_params = []): array
{
$query_params = [
'expand' => 'false',
] + $query_params;

/**
* @inheritdoc
*/
abstract protected function responseArrayToArrayOfEntities(array $responseArray, string $keyGetter = 'id'): array;
$uri = $this->getBaseEndpointUri()->withQuery(http_build_query($query_params));
$response = $this->getClient()->get($uri);

$ids = $this->responseToArray($response);

// Re-key the array from 0 if CPS had to be simulated.
return $pager ? array_values($this->simulateCpsPagination($pager, $ids)) : $ids;
}

/**
* Gets entities and entity ids in a provided range from Apigee Edge.
* Simulates paginated response on an organization without CPS.
*
* @param \Apigee\Edge\Structure\PagerInterface $pager
* CPS limit object with configured startKey and limit.
* @param array $query_params
* Query parameters for the API call.
* Pager.
* @param array $result
* The non-paginated result returned by the API.
* @param array|null $array_search_haystack
* Haystack for array_search, the needle is the start key from the pager.
* If it is null, then the haystack is the $result.
*
* @return array
* API response parsed as an array.
* The paginated result.
*/
private function getResultsInRange(PagerInterface $pager, array $query_params): array
private function simulateCpsPagination(PagerInterface $pager, array $result, array $array_search_haystack = null): array
{
$query_params['startKey'] = $pager->getStartKey();
// Do not add 0 unnecessarily to the query parameters.
if ($pager->getLimit() > 0) {
$query_params['count'] = $pager->getLimit();
$array_search_haystack = $array_search_haystack ?? $result;
// If start key is null let's set it to the first key in the
// result just like the API would do.
$start_key = $pager->getStartKey() ?? reset($array_search_haystack);
$offset = array_search($start_key, $array_search_haystack);
// Start key has not been found in the response. Apigee Edge with
// CPS enabled would return an HTTP 404, with error code
// "keymanagement.service.[ENTITY_TYPE]_doesnot_exist" which would
// trigger a ClientErrorException. We throw a RuntimeException
// instead of that because it does not require to construct an
// API response object.
if (false === $offset) {
throw new RuntimeException(sprintf('CPS simulation error: "%s" does not exist.', $start_key));
}
$uri = $this->getBaseEndpointUri()->withQuery(http_build_query($query_params));
$response = $this->getClient()->get($uri);

return $this->responseToArray($response);
// The default pagination limit (aka. "count") on CPS supported
// listing endpoints varies. When this script was written it was
// 1000 on two endpoints and 100 on two app related endpoints,
// namely List Developer Apps and List Company Apps. A
// developer/company should not have 100 apps, this is
// the reason why this limit is smaller. Therefore we choose to
// use 1000 as pagination limit if it has not been set.
// https://apidocs.apigee.com/management/apis/get/organizations/%7Borg_name%7D/apiproducts-0
// https://apidocs.apigee.com/management/apis/get/organizations/%7Borg_name%7D/developers
// https://apidocs.apigee.com/management/apis/get/organizations/%7Borg_name%7D/developers/%7Bdeveloper_email_or_id%7D/apps
return array_slice($result, $offset, $pager->getLimit() ?: 1000, true);
}
}
1 change: 1 addition & 0 deletions src/Exception/CpsNotEnabledException.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
* feature is not enabled on the organization on Apigee Edge.
*
* @see https://docs.apigee.com/api-services/content/api-reference-getting-started#cps
* @deprecated Since 2.0.1, https://github.com/apigee/apigee-client-php/pull/43/files
*/
class CpsNotEnabledException extends \RuntimeException
{
Expand Down
Loading