diff --git a/src/Dto/AbstractDto.php b/src/Dto/AbstractDto.php new file mode 100644 index 0000000..36838d9 --- /dev/null +++ b/src/Dto/AbstractDto.php @@ -0,0 +1,10 @@ +'; + + public const LT = '<'; + + public const EQ = '='; + + public const NEQ = '!='; +} diff --git a/src/Dto/Filter/AmountSpentFilter.php b/src/Dto/Filter/AmountSpentFilter.php new file mode 100644 index 0000000..000a521 --- /dev/null +++ b/src/Dto/Filter/AmountSpentFilter.php @@ -0,0 +1,43 @@ +'; + + public const LT = '<'; + + public const EQ = '='; + + /** + * @var self::GT|self::LT|self::EQ + */ + protected string $relation; + + /** + * @var int|float + */ + protected $value; + + /** + * @param self::GT|self::LT|self::EQ $relation + * @param int|float $value + */ + public function __construct(string $relation, $value) + { + $this->relation = $relation; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'amount_spent', + 'relation' => $this->relation, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/AppVersionFilter.php b/src/Dto/Filter/AppVersionFilter.php new file mode 100644 index 0000000..a343f4e --- /dev/null +++ b/src/Dto/Filter/AppVersionFilter.php @@ -0,0 +1,33 @@ +relation = $relation; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'app_version', + 'relation' => $this->relation, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/BoughtSkuFilter.php b/src/Dto/Filter/BoughtSkuFilter.php new file mode 100644 index 0000000..1b49c84 --- /dev/null +++ b/src/Dto/Filter/BoughtSkuFilter.php @@ -0,0 +1,41 @@ +relation = $relation; + $this->key = $key; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'bought_sku', + 'relation' => $this->relation, + 'key' => $this->key, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/ConditionalFilter.php b/src/Dto/Filter/ConditionalFilter.php new file mode 100644 index 0000000..e9745a3 --- /dev/null +++ b/src/Dto/Filter/ConditionalFilter.php @@ -0,0 +1,42 @@ +operator = $operator; + } + + /** + * @param self::AND|self::OR $operator + */ + public function setOperator(string $operator): self + { + $this->operator = $operator; + + return $this; + } + + public function toArray(): array + { + return [ + 'operator' => $this->operator, + ]; + } +} diff --git a/src/Dto/Filter/CountryFilter.php b/src/Dto/Filter/CountryFilter.php new file mode 100644 index 0000000..0641f80 --- /dev/null +++ b/src/Dto/Filter/CountryFilter.php @@ -0,0 +1,24 @@ +value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'country', + 'relation' => self::EQ, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/FirstSessionFilter.php b/src/Dto/Filter/FirstSessionFilter.php new file mode 100644 index 0000000..83ca7b8 --- /dev/null +++ b/src/Dto/Filter/FirstSessionFilter.php @@ -0,0 +1,37 @@ +relation = $relation; + $this->hoursAgo = $hoursAgo; + } + + public function toArray(): array + { + return [ + 'field' => 'first_session', + 'relation' => $this->relation, + 'hours_ago' => $this->hoursAgo, + ]; + } +} diff --git a/src/Dto/Filter/LanguageFilter.php b/src/Dto/Filter/LanguageFilter.php new file mode 100644 index 0000000..5dd94cd --- /dev/null +++ b/src/Dto/Filter/LanguageFilter.php @@ -0,0 +1,33 @@ +relation = $relation; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'language', + 'relation' => $this->relation, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/LastSessionFilter.php b/src/Dto/Filter/LastSessionFilter.php new file mode 100644 index 0000000..70ecbdf --- /dev/null +++ b/src/Dto/Filter/LastSessionFilter.php @@ -0,0 +1,37 @@ +relation = $relation; + $this->hoursAgo = $hoursAgo; + } + + public function toArray(): array + { + return [ + 'field' => 'last_session', + 'relation' => $this->relation, + 'hours_ago' => $this->hoursAgo, + ]; + } +} diff --git a/src/Dto/Filter/LocationFilter.php b/src/Dto/Filter/LocationFilter.php new file mode 100644 index 0000000..926f37a --- /dev/null +++ b/src/Dto/Filter/LocationFilter.php @@ -0,0 +1,31 @@ +radius = $radius; + $this->lat = $lat; + $this->long = $long; + } + + public function toArray(): array + { + return [ + 'field' => 'location', + 'radius' => $this->radius, + 'lat' => $this->lat, + 'long' => $this->long, + ]; + } +} diff --git a/src/Dto/Filter/SessionCountFilter.php b/src/Dto/Filter/SessionCountFilter.php new file mode 100644 index 0000000..0bee56f --- /dev/null +++ b/src/Dto/Filter/SessionCountFilter.php @@ -0,0 +1,33 @@ +relation = $relation; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'session_count', + 'relation' => $this->relation, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/SessionTimeFilter.php b/src/Dto/Filter/SessionTimeFilter.php new file mode 100644 index 0000000..abf7217 --- /dev/null +++ b/src/Dto/Filter/SessionTimeFilter.php @@ -0,0 +1,33 @@ +relation = $relation; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'session_time', + 'relation' => $this->relation, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Filter/TagFilter.php b/src/Dto/Filter/TagFilter.php new file mode 100644 index 0000000..f0b6eae --- /dev/null +++ b/src/Dto/Filter/TagFilter.php @@ -0,0 +1,49 @@ +relation = $relation; + $this->key = $key; + $this->value = $value; + } + + public function toArray(): array + { + return [ + 'field' => 'tag', + 'relation' => $this->relation, + 'key' => $this->key, + 'value' => $this->value, + ]; + } +} diff --git a/src/Dto/Segment/CreateSegment.php b/src/Dto/Segment/CreateSegment.php new file mode 100644 index 0000000..74e1c72 --- /dev/null +++ b/src/Dto/Segment/CreateSegment.php @@ -0,0 +1,66 @@ + + */ + protected ?array $filters = null; + + /** + * @param non-empty-string|null $name + * @param array|null $filters + */ + public function __construct(string $name = null, array $filters = null) + { + $this->name = $name; + $this->filters = $filters; + } + + /** + * @param non-empty-string $name + */ + public function name(string $name): self + { + $this->name = $name; + + return $this; + } + + /** + * @param list $filters + */ + public function filters(array $filters): self + { + $this->filters = $filters; + + return $this; + } + + public function toArray(): array + { + return array_filter([ + 'name' => $this->name, + 'filters' => $this->filters !== null + ? array_map( + static function (AbstractFilter $filter): array { + return $filter->toArray(); + }, + $this->filters + ) + : null, + ]); + } +} diff --git a/src/Dto/Segment/ListSegments.php b/src/Dto/Segment/ListSegments.php new file mode 100644 index 0000000..7244e92 --- /dev/null +++ b/src/Dto/Segment/ListSegments.php @@ -0,0 +1,58 @@ +|null + */ + protected ?int $limit = null; + + /** + * @var int<0, 2147483648>|null + */ + protected ?int $offset = null; + + /** + * @param int<0, 2147483648>|null $limit + * @param int<0, 2147483648>|null $offset + */ + public function __construct(int $limit = null, int $offset = null) + { + $this->limit = $limit; + $this->offset = $offset; + } + + /** + * @param int<0, 2147483648> $limit + */ + public function limit(int $limit): self + { + $this->limit = $limit; + + return $this; + } + + /** + * @param int<0, 2147483648> $offset + */ + public function offset(int $offset): self + { + $this->offset = $offset; + + return $this; + } + + public function toArray(): array + { + return array_filter([ + 'limit' => $this->limit, + 'offset' => $this->offset, + ]); + } +} diff --git a/src/Exception/UnsuccessfulResponse.php b/src/Exception/UnsuccessfulResponse.php new file mode 100644 index 0000000..a6aff80 --- /dev/null +++ b/src/Exception/UnsuccessfulResponse.php @@ -0,0 +1,34 @@ +request = $request; + $this->response = $response; + + parent::__construct(); + } + + public function getRequest(): RequestInterface + { + return $this->request; + } + + public function getResponse(): ResponseInterface + { + return $this->response; + } +} diff --git a/src/OneSignal.php b/src/OneSignal.php index 422923d..1f7e10b 100644 --- a/src/OneSignal.php +++ b/src/OneSignal.php @@ -7,6 +7,7 @@ use OneSignal\Exception\BadMethodCallException; use OneSignal\Exception\InvalidArgumentException; use OneSignal\Exception\JsonException; +use OneSignal\Exception\UnsuccessfulResponse; use OneSignal\Resolver\ResolverFactory; use Psr\Http\Client\ClientInterface; use Psr\Http\Message\RequestFactoryInterface; @@ -86,6 +87,29 @@ public function sendRequest(RequestInterface $request): array return $content; } + public function makeRequest(RequestInterface $request): array + { + $response = $this->httpClient->sendRequest($request); + + if ($response->getStatusCode() < 200 || $response->getStatusCode() >= 400) { + throw new UnsuccessfulResponse($request, $response); + } + + $content = $response->getBody()->__toString(); + + try { + $content = json_decode($content, true, 512, JSON_BIGINT_AS_STRING | JSON_THROW_ON_ERROR); + } catch (\JsonException $e) { + throw new JsonException($e->getMessage(), $e->getCode(), $e); + } + + if (!is_array($content)) { + throw new JsonException(sprintf('JSON content was expected to decode to an array, %s returned.', gettype($content))); + } + + return $content; + } + /** * @return object * @@ -105,6 +129,10 @@ public function api(string $name) case 'notifications': $api = new Notifications($this, $this->resolverFactory); + break; + case 'segments': + $api = new Segments($this); + break; default: throw new InvalidArgumentException("Undefined api instance called: '$name'."); diff --git a/src/Response/AbstractResponse.php b/src/Response/AbstractResponse.php new file mode 100644 index 0000000..3c90d58 --- /dev/null +++ b/src/Response/AbstractResponse.php @@ -0,0 +1,10 @@ +success = $success; + $this->id = $id; + } + + public static function makeFromResponse(array $response): self + { + return new static( + $response['success'], + $response['id'] + ); + } + + public function getSuccess(): bool + { + return $this->success; + } + + public function getId(): string + { + return $this->id; + } +} diff --git a/src/Response/Segment/DeleteSegmentResponse.php b/src/Response/Segment/DeleteSegmentResponse.php new file mode 100644 index 0000000..d762a4b --- /dev/null +++ b/src/Response/Segment/DeleteSegmentResponse.php @@ -0,0 +1,29 @@ +success = $success; + } + + public static function makeFromResponse(array $response): self + { + return new static( + $response['success'] + ); + } + + public function getSuccess(): bool + { + return $this->success; + } +} diff --git a/src/Response/Segment/ListSegmentsResponse.php b/src/Response/Segment/ListSegmentsResponse.php new file mode 100644 index 0000000..c9df7ce --- /dev/null +++ b/src/Response/Segment/ListSegmentsResponse.php @@ -0,0 +1,93 @@ + + */ + protected int $totalCount; + + /** + * @var int<0, 2147483648> + */ + protected int $offset; + + /** + * @var int<0, 2147483648> + */ + protected int $limit; + + /** + * @var list + */ + protected array $segments; + + /** + * @param int<0, 2147483648> $totalCount + * @param int<0, 2147483648> $limit + * @param int<0, 2147483648> $offset + * @param list $segments + */ + public function __construct(int $totalCount, int $offset, int $limit, array $segments) + { + $this->totalCount = $totalCount; + $this->offset = $offset; + $this->limit = $limit; + $this->segments = $segments; + } + + public static function makeFromResponse(array $response): self + { + $segments = array_map( + static function (array $segment): Segment { + return new Segment( + $segment['id'], + $segment['name'], + new DateTimeImmutable($segment['created_at']), + new DateTimeImmutable($segment['updated_at']), + $segment['app_id'], + $segment['read_only'], + $segment['is_active'], + ); + }, + $response['segments'] + ); + + return new static( + $response['total_count'], + $response['offset'], + $response['limit'], + $segments + ); + } + + public function getTotalCount(): int + { + return $this->totalCount; + } + + public function getOffset(): int + { + return $this->offset; + } + + public function getLimit(): int + { + return $this->limit; + } + + /** + * @return list + */ + public function getSegments(): array + { + return $this->segments; + } +} diff --git a/src/Response/Segment/Segment.php b/src/Response/Segment/Segment.php new file mode 100644 index 0000000..8be3492 --- /dev/null +++ b/src/Response/Segment/Segment.php @@ -0,0 +1,91 @@ +id = $id; + $this->name = $name; + $this->createdAt = $createdAt; + $this->updatedAt = $updatedAt; + $this->appId = $appId; + $this->readOnly = $readOnly; + $this->isActive = $isActive; + } + + public function getId(): string + { + return $this->id; + } + + public function getName(): string + { + return $this->name; + } + + public function getCreatedAt(): DateTimeImmutable + { + return $this->createdAt; + } + + public function getUpdatedAt(): DateTimeImmutable + { + return $this->updatedAt; + } + + public function getAppId(): string + { + return $this->appId; + } + + public function getReadOnly(): bool + { + return $this->readOnly; + } + + public function getIsActive(): bool + { + return $this->isActive; + } +} diff --git a/src/Segments.php b/src/Segments.php new file mode 100644 index 0000000..69e1fb6 --- /dev/null +++ b/src/Segments.php @@ -0,0 +1,68 @@ +client->getConfig()->getApplicationId(); + + $request = $this->createRequest('GET', '/apps/'.$appId.'/segments?'.http_build_query($listSegmentsDto->toArray())); + $request = $request->withHeader('Authorization', "Basic {$this->client->getConfig()->getApplicationAuthKey()}"); + + return ListSegmentsResponse::makeFromResponse($this->client->makeRequest($request)); + } + + /** + * Create new segment with provided data. + * + * Application authentication key and ID must be set. + */ + public function create(CreateSegment $createSegmentDto): CreateSegmentResponse + { + $appId = $this->client->getConfig()->getApplicationId(); + + $request = $this->createRequest('POST', '/apps/'.$appId.'/segments'); + $request = $request->withHeader('Authorization', "Basic {$this->client->getConfig()->getApplicationAuthKey()}"); + $request = $request->withHeader('Content-Type', 'application/json'); + $request = $request->withBody($this->createStream($createSegmentDto->toArray())); + + return CreateSegmentResponse::makeFromResponse($this->client->makeRequest($request)); + } + + /** + * Delete segment. + * + * Application authentication key and ID must be set. + * + * @param non-empty-string $id Segment ID + */ + public function delete(string $id): DeleteSegmentResponse + { + $appId = $this->client->getConfig()->getApplicationId(); + + $request = $this->createRequest('DELETE', '/apps/'.$appId.'/segments/'.$id); + $request = $request->withHeader('Authorization', "Basic {$this->client->getConfig()->getApplicationAuthKey()}"); + + return DeleteSegmentResponse::makeFromResponse($this->client->makeRequest($request)); + } +} diff --git a/tests/Fixtures/segments_create.json b/tests/Fixtures/segments_create.json new file mode 100644 index 0000000..cb0cb13 --- /dev/null +++ b/tests/Fixtures/segments_create.json @@ -0,0 +1 @@ +{"success": true, "id": "7ed2887d-bd24-4a81-8220-4b256a08ab19"} \ No newline at end of file diff --git a/tests/Fixtures/segments_delete.json b/tests/Fixtures/segments_delete.json new file mode 100644 index 0000000..c8af694 --- /dev/null +++ b/tests/Fixtures/segments_delete.json @@ -0,0 +1 @@ +{"success": true} diff --git a/tests/Fixtures/segments_get_all.json b/tests/Fixtures/segments_get_all.json new file mode 100644 index 0000000..2ad6d4d --- /dev/null +++ b/tests/Fixtures/segments_get_all.json @@ -0,0 +1,16 @@ +{ + "total_count": 1, + "offset": 0, + "limit": 300, + "segments": [ + { + "id": "4414c404-56a3-11ed-9b6a-0242ac120002", + "name": "Subscribed Users", + "created_at": "2022-07-23T13:44:10.324Z", + "updated_at": "2022-09-18T11:33:02.451Z", + "app_id": "65c10914-56a3-11ed-9b6a-0242ac120002", + "read_only": false, + "is_active": true + } + ] +} \ No newline at end of file diff --git a/tests/SegmentsTest.php b/tests/SegmentsTest.php new file mode 100644 index 0000000..ca4f223 --- /dev/null +++ b/tests/SegmentsTest.php @@ -0,0 +1,94 @@ +createClientMock(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('GET', $method); + $this->assertSame(OneSignal::API_URL.'/apps/fakeApplicationId/segments?limit=300', $url); + $this->assertArrayHasKey('accept', $options['normalized_headers']); + $this->assertSame('Accept: application/json', $options['normalized_headers']['accept'][0]); + + return new MockResponse($this->loadFixture('segments_get_all.json'), ['http_code' => 200]); + }); + + $segments = new Segments($client); + + $responseData = $segments->list(new ListSegments(300)); + + self::assertEquals(ListSegmentsResponse::makeFromResponse([ + 'total_count' => 1, + 'offset' => 0, + 'limit' => 300, + 'segments' => [ + [ + 'id' => '4414c404-56a3-11ed-9b6a-0242ac120002', + 'name' => 'Subscribed Users', + 'created_at' => '2022-07-23T13:44:10.324Z', + 'updated_at' => '2022-09-18T11:33:02.451Z', + 'app_id' => '65c10914-56a3-11ed-9b6a-0242ac120002', + 'read_only' => false, + 'is_active' => true, + ], + ], + ]), $responseData); + } + + public function testCreate(): void + { + $client = $this->createClientMock(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('POST', $method); + $this->assertSame(OneSignal::API_URL.'/apps/fakeApplicationId/segments', $url); + $this->assertArrayHasKey('accept', $options['normalized_headers']); + $this->assertArrayHasKey('content-type', $options['normalized_headers']); + $this->assertSame('Accept: application/json', $options['normalized_headers']['accept'][0]); + $this->assertSame('Content-Type: application/json', $options['normalized_headers']['content-type'][0]); + + return new MockResponse($this->loadFixture('segments_create.json'), ['http_code' => 201]); + }); + + $segments = new Segments($client); + + $responseData = $segments->create(new CreateSegment('Segment 2')); + + self::assertEquals(CreateSegmentResponse::makeFromResponse([ + 'success' => true, + 'id' => '7ed2887d-bd24-4a81-8220-4b256a08ab19', + ]), $responseData); + } + + public function testDelete(): void + { + $client = $this->createClientMock(function (string $method, string $url, array $options): ResponseInterface { + $this->assertSame('DELETE', $method); + $this->assertSame(OneSignal::API_URL.'/apps/fakeApplicationId/segments/e4e87830-b954-11e3-811d-f3b376925f15', $url); + $this->assertArrayHasKey('accept', $options['normalized_headers']); + $this->assertArrayHasKey('authorization', $options['normalized_headers']); + $this->assertSame('Accept: application/json', $options['normalized_headers']['accept'][0]); + $this->assertSame('Authorization: Basic fakeApplicationAuthKey', $options['normalized_headers']['authorization'][0]); + + return new MockResponse($this->loadFixture('segments_delete.json'), ['http_code' => 200]); + }); + + $segments = new Segments($client); + + $responseData = $segments->delete('e4e87830-b954-11e3-811d-f3b376925f15'); + + self::assertEquals(new DeleteSegmentResponse(true), $responseData); + } +}