Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ jobs:
fail-fast: false
matrix:
php-version:
- "7.4"
- "8.0"
- "8.1"
- "8.2"
steps:
- uses: actions/checkout@master

Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ and adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).

## Unreleased

- [#262](https://github.com/Shopify/shopify-api-php/pull/262) ⚠️ [Breaking] Added support for PHP 8.2, and removed support for PHP 7.4

## v4.3.0 - 2023-04-12

- [#259](https://github.com/Shopify/shopify-api-php/pull/259) Added support for 2023-04 API version, updated auto-generated REST resources
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
}
},
"require": {
"php": "^7.4 || ^8.0 || ^8.1",
"php": "^8.0 || ^8.1 || ^8.2",
"ext-json": "*",
"ramsey/uuid": "^4.1",
"psr/log": "^1.1 || ^2.0 || ^3.0",
Expand Down
16 changes: 8 additions & 8 deletions docs/getting_started.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,14 @@ You should call this method as early as possible in your application, as none of

```php
Context::initialize(
$_ENV['SHOPIFY_API_KEY'],
$_ENV['SHOPIFY_API_SECRET'],
$_ENV['SHOPIFY_APP_SCOPES'],
$_ENV['SHOPIFY_APP_HOST_NAME'],
new FileSessionStorage('/tmp/php_sessions'),
'2021-04',
true,
false,
apiKey: $_ENV['SHOPIFY_API_KEY'],
apiSecretKey: $_ENV['SHOPIFY_API_SECRET'],
scopes: $_ENV['SHOPIFY_APP_SCOPES'],
hostName: $_ENV['SHOPIFY_APP_HOST_NAME'],
sessionStorage: new FileSessionStorage('/tmp/php_sessions'),
apiVersion: '2023-04',
isEmbeddedApp: true,
isPrivateApp: false,
Copy link
Contributor

Choose a reason for hiding this comment

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

Did this get renamed to isCustomStoreApp?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No, I think we still use isPrivateApp. We should probably rename it, but that should be its own PR.

);
```

Expand Down
9 changes: 6 additions & 3 deletions docs/usage/graphql.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Make a GraphQL API call

Once OAuth is complete, we can use the library's GraphQL client to make requests to the GraphQL Admin API. To do that, create an instance of `Graphql` using the current shop URL and, for non-private apps, the session `accessToken` in your app's endpoint.

The GraphQL client's main method is `query`, which takes the following arguments and returns an `HttpResponse` object:
Expand All @@ -10,6 +11,7 @@ The GraphQL client's main method is `query`, which takes the following arguments
| `tries` | `int \| null` | No | `1` | How many times to attempt the request |

Example use of `query`

```php
// Load current session to get `accessToken`
$session = Shopify\Utils::loadCurrentSession($headers, $cookies, $isOnline);
Expand All @@ -29,12 +31,13 @@ $queryString = <<<QUERY
}
}
QUERY;
$response = $client->query($queryString);
$response = $client->query(data: $queryString);

// do something with the returned data
```

Example with variables

```php
// load current session and create GraphQL client like above example

Expand All @@ -53,9 +56,9 @@ $variables = [
["title" => "TARDIS"],
["descriptionHtml" => "Time and Relative Dimensions in Space"],
["productType" => "Time Lord technology"]
]
]
];
$response = $client->query(['query' => $queryUsingVariables, 'variables' => $variables]);
$response = $client->query(data: ['query' => $queryUsingVariables, 'variables' => $variables]);

// do something with the returned data
```
Expand Down
27 changes: 14 additions & 13 deletions docs/usage/rest.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ You can read our [REST Admin API](https://shopify.dev/docs/api/admin/getting-sta
## Making your first REST request

REST Admin API endpoints are organized by [resource](https://shopify.dev/docs/api/admin/rest/reference#selecting-apis-for-your-app) . You'll need to use different API endpoints depending on the service that your app provides. There are two different ways of doing that with this library:
* [REST resources](#rest-resources)
* [REST client](#rest-client)

- [REST resources](#rest-resources)
- [REST client](#rest-client)

### REST resources

Expand All @@ -20,22 +21,22 @@ The resource classes will provide methods for all endpoints described in [the RE

The REST client uses `get`, `post`, `put`, and `delete` requests to retrieve, create, update, and delete resources respectively.

| Parameter | Type | Required? | Default Value | Notes |
|:----------|:----------------|:---------:|:----------------:|:-------------------------------------------------|
| path | string | Yes | | The requested API endpoint path. This can be one of two formats:<ul><li>The path starting after the `/admin/api/{version}/` prefix, such as `'products'`, which executes `/admin/api/{version}/products.json`</li><li>The full path, such as `/admin/oauth/access_scopes.json`</li></ul> |
| body | string or array | No | null | Only `post`, and `put` methods can have body |
| headers | array | No | [] | Any extra headers to send along with the request |
| query | array | No | [] | Query parameters as an associative array |
| tries | int | No | null | How many times to attempt the request |
| dataType | No | No | `DATA_TYPE_JSON` | Only `post`, and `put` methods can have body |
| Parameter | Type | Required? | Default Value | Notes |
| :-------- | :-------------- | :-------: | :--------------: | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| path | string | Yes | | The requested API endpoint path. This can be one of two formats:<ul><li>The path starting after the `/admin/api/{version}/` prefix, such as `'products'`, which executes `/admin/api/{version}/products.json`</li><li>The full path, such as `/admin/oauth/access_scopes.json`</li></ul> |
| body | string or array | No | null | Only `post`, and `put` methods can have body |
| headers | array | No | [] | Any extra headers to send along with the request |
| query | array | No | [] | Query parameters as an associative array |
| tries | int | No | null | How many times to attempt the request |
| dataType | No | No | `DATA_TYPE_JSON` | Only `post`, and `put` methods can have body |

In the following example we will retrieve a list of products from a shop using `Shopify\Clients\Rest` class.

```php
use Shopify\Clients\Rest;

$client = new Rest($session->getShop(), $session->getAccessToken());
$response = $client->get('products');
$response = $client->get(path: 'products');
```

This request returns an instance of `Shopify\Clients\RestResponse`. The response object includes response `statusCode`, `body`, `headers`, and pagination information. To access the response attributes you can use `getStatusCode`, `getBody` and `getHeaders` respectively. There is also a convenience method `getDecodedBody` that will give you the JSON decoded associative array of the response body.
Expand All @@ -56,13 +57,13 @@ To get the next page.

```php
$pageInfo = unserialize($serializedPageInfo);
$result = $client->get('products', [], $pageInfo->getNextPageQuery());
$result = $client->get(path: 'products', query: $pageInfo->getNextPageQuery());
```

PageInfo gives you some convenience methods to determine whether there is there are more pages.

| method | Return type | Notes |
|:----------------------:|:-----------:|:-------------------------------|
| :--------------------: | :---------: | :----------------------------- |
| hasNextPage() | bool | false if there is no more page |
| hasPreviousPage() | bool | false if there is no more page |
| getNextPageQuery() | array | Query to get the next page |
Expand Down
2 changes: 1 addition & 1 deletion docs/usage/storefront.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ $storefrontClient = new \Shopify\Clients\Storefront($shop, $storefrontAccessToke

// Call query and pass your query as `data`
$products = $storefrontClient->query(
<<<QUERY
data: <<<QUERY
{
products (first: 10) {
edges {
Expand Down
11 changes: 5 additions & 6 deletions src/Auth/Session.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,10 @@ class Session
private $onlineAccessInfo = null;

public function __construct(
string $id,
string $shop,
bool $isOnline,
string $state
private string $id,
private string $shop,
private bool $isOnline,
private string $state
) {
$this->id = $id;
$this->shop = Utils::sanitizeShopDomain($shop);
Expand Down Expand Up @@ -146,8 +146,7 @@ public function clone(string $newSessionId): Session
*/
public function isValid(): bool
{
return (
Context::$SCOPES->equals($this->scope) &&
return (Context::$SCOPES->equals($this->scope) &&
$this->accessToken &&
(!$this->expires || ($this->expires > new DateTime()))
);
Expand Down
39 changes: 30 additions & 9 deletions src/Clients/Http.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,13 @@ public function __construct(string $domain)
*/
public function get(string $path, array $headers = [], array $query = [], ?int $tries = null): HttpResponse
{
return $this->request($path, self::METHOD_GET, null, $headers, $query, $tries);
return $this->request(
path: $path,
method: self::METHOD_GET,
headers: $headers,
query: $query,
tries: $tries,
);
}

/**
Expand All @@ -71,7 +77,15 @@ public function post(
?int $tries = null,
string $dataType = self::DATA_TYPE_JSON
): HttpResponse {
return $this->request($path, self::METHOD_POST, $body, $headers, $query, $tries, $dataType);
return $this->request(
path: $path,
method: self::METHOD_POST,
body: $body,
headers: $headers,
query: $query,
tries: $tries,
dataType: $dataType,
);
}

/**
Expand All @@ -96,7 +110,15 @@ public function put(
?int $tries = null,
string $dataType = self::DATA_TYPE_JSON
): HttpResponse {
return $this->request($path, self::METHOD_PUT, $body, $headers, $query, $tries, $dataType);
return $this->request(
path: $path,
method: self::METHOD_PUT,
body: $body,
headers: $headers,
query: $query,
tries: $tries,
dataType: $dataType,
);
}

/**
Expand All @@ -114,12 +136,11 @@ public function put(
public function delete(string $path, array $headers = [], array $query = [], ?int $tries = null): HttpResponse
{
return $this->request(
$path,
self::METHOD_DELETE,
null,
$headers,
$query,
$tries,
path: $path,
method: self::METHOD_DELETE,
headers: $headers,
query: $query,
tries: $tries,
);
}

Expand Down
26 changes: 21 additions & 5 deletions src/Rest/Base.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
use Shopify\Exception\RestResourceException;
use Shopify\Exception\RestResourceRequestException;

abstract class Base
// When upgrading to PHP 8.2, consider using the AllowDynamicProperties attribute
// https://stitcher.io/blog/deprecated-dynamic-properties-in-php-82#a-better-alternative
abstract class Base extends \stdClass
{
public static string $API_VERSION;
public static ?array $NEXT_PAGE_QUERY = null;
Expand Down Expand Up @@ -178,16 +180,30 @@ protected static function request(
$params = array_filter($params);
switch ($httpMethod) {
case "get":
$response = $client->get($path, [], $params);
$response = $client->get(
path: $path,
query: $params,
);
break;
case "post":
$response = $client->post($path, $body, [], $params);
$response = $client->post(
path: $path,
body: $body,
query: $params,
);
break;
case "put":
$response = $client->put($path, $body, [], $params);
$response = $client->put(
path: $path,
body: $body,
query: $params,
);
break;
case "delete":
$response = $client->delete($path, [], $params);
$response = $client->delete(
path: $path,
query: $params,
);
break;
}

Expand Down
6 changes: 4 additions & 2 deletions src/Webhooks/Registry.php
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ private static function isWebhookRegistrationNeeded(
string $callbackAddress,
DeliveryMethod $method
): array {
$checkResponse = $client->query($method->buildCheckQuery($topic));
$checkResponse = $client->query(data: $method->buildCheckQuery($topic));

$checkStatusCode = $checkResponse->getStatusCode();
$checkBody = $checkResponse->getDecodedBody();
Expand Down Expand Up @@ -225,7 +225,9 @@ private static function sendRegisterRequest(
DeliveryMethod $deliveryMethod,
?string $webhookId
): array {
$registerResponse = $client->query($deliveryMethod->buildRegisterQuery($topic, $callbackAddress, $webhookId));
$registerResponse = $client->query(
data: $deliveryMethod->buildRegisterQuery($topic, $callbackAddress, $webhookId),
);

$statusCode = $registerResponse->getStatusCode();
$body = $registerResponse->getDecodedBody();
Expand Down
19 changes: 12 additions & 7 deletions tests/BaseTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use GuzzleHttp\Psr7\Response;
use PHPUnit\Framework\TestCase;
use PHPUnit\Framework\MockObject\MockObject;
use Psr\Http\Client\ClientInterface;
use Shopify\Clients\HttpClientFactory;
use Shopify\Context;
Expand All @@ -26,11 +27,11 @@ public function setUp(): void
{
// Initialize Context before each test
Context::initialize(
'ash',
'steffi',
['sleepy', 'kitty'],
'www.my-friends-cats.com',
new MockSessionStorage(),
apiKey: 'ash',
apiSecretKey: 'steffi',
scopes: ['sleepy', 'kitty'],
hostName: 'www.my-friends-cats.com',
sessionStorage: new MockSessionStorage(),
);
Context::$RETRY_TIME_IN_SECONDS = 0;
$this->version = require dirname(__FILE__) . '/../src/version.php';
Expand Down Expand Up @@ -87,7 +88,7 @@ public function mockTransportRequests(array $requests): void
$request->identicalBody
);

$requestMatchers[] = [$matcher];
$requestMatchers[] = $matcher;

$newResponses[] = $request->error ? 'TEST EXCEPTION' : new Response(
$request->response['statusCode'],
Expand All @@ -96,12 +97,15 @@ public function mockTransportRequests(array $requests): void
);
}

/** @var MockObject */
$client = $this->createMock(ClientInterface::class);

$i = 0;
$client->expects($this->exactly(count($requestMatchers)))
->method('sendRequest')
->withConsecutive(...$requestMatchers)
->with(self::callback(function ($request) use (&$i, $requestMatchers) {
return $requestMatchers[$i]->matches($request);
}))
->willReturnCallback(
function () use (&$i, $newResponses) {
$response = $newResponses[$i++];
Expand All @@ -113,6 +117,7 @@ function () use (&$i, $newResponses) {
}
);

/** @var MockObject */
$factory = $this->createMock(HttpClientFactory::class);

$factory->expects($this->any())
Expand Down
Loading