Skip to content
118 changes: 87 additions & 31 deletions src/ConvertKit_API_Traits.php
Original file line number Diff line number Diff line change
Expand Up @@ -158,30 +158,39 @@ public function get_growth_stats(\DateTime|null $starting = null, \DateTime|null
/**
* List forms.
*
* @param string $status Form status (active|archived|trashed|all).
* @param boolean $include_total_count To include the total count of records in the response, use true.
* @param string $after_cursor Return results after the given pagination cursor.
* @param string $before_cursor Return results before the given pagination cursor.
* @param integer $per_page Number of results to return.
* @param string $status Form status (active|archived|trashed|all).
* @param array<string> $include Additional fields to include: subscriber_count.
* @param boolean $include_total_count To include the total count of records in the response, use true.
* @param string $after_cursor Return results after the given pagination cursor.
* @param string $before_cursor Return results before the given pagination cursor.
* @param integer $per_page Number of results to return.
*
* @see https://developers.kit.com/api-reference/forms/list-forms
*
* @return mixed|array<int,\stdClass>
*/
public function get_forms(
string $status = 'active',
array $include = [],
bool $include_total_count = false,
string $after_cursor = '',
string $before_cursor = '',
int $per_page = 100
) {
// Build parameters.
$options = [
'type' => 'embed',
'status' => $status,
];

if (!empty($include)) {
$options['include'] = implode(',', $include);
}

return $this->get(
'forms',
$this->build_total_count_and_pagination_params(
[
'type' => 'embed',
'status' => $status,
],
$options,
$include_total_count,
$after_cursor,
$before_cursor,
Expand Down Expand Up @@ -1004,10 +1013,11 @@ public function update_snippet(
/**
* List tags.
*
* @param boolean $include_total_count To include the total count of records in the response, use true.
* @param string $after_cursor Return results after the given pagination cursor.
* @param string $before_cursor Return results before the given pagination cursor.
* @param integer $per_page Number of results to return.
* @param array<string> $include Additional fields to include: subscriber_count.
* @param boolean $include_total_count To include the total count of records in the response, use true.
* @param string $after_cursor Return results after the given pagination cursor.
* @param string $before_cursor Return results before the given pagination cursor.
* @param integer $per_page Number of results to return.
*
* @see https://developers.kit.com/api-reference/tags/list-tags
*
Expand All @@ -1016,15 +1026,23 @@ public function update_snippet(
* @return mixed|array<int,\stdClass>
*/
public function get_tags(
array $include = [],
bool $include_total_count = false,
string $after_cursor = '',
string $before_cursor = '',
int $per_page = 100
) {
// Build parameters.
$options = [];

if (!empty($include)) {
$options['include'] = implode(',', $include);
}

return $this->get(
'tags',
$this->build_total_count_and_pagination_params(
[],
$options,
$include_total_count,
$after_cursor,
$before_cursor,
Expand Down Expand Up @@ -1085,6 +1103,41 @@ public function create_tags(array $tags, string $callback_url = '')
);
}

/**
* Bulk delete tags.
*
* @param array<int> $tag_ids Tag IDs.
* @param string $callback_url URL to notify for large batch size when async processing complete.
*
* @since 2.6.0
*
* @see https://developers.kit.com/api-reference/tags/bulk-delete-tags
*
* @return false|mixed
*/
public function delete_tags(array $tag_ids, string $callback_url = '')
{
// Build parameters.
$options = [
'tags' => [],
];
foreach ($tag_ids as $i => $tag_id) {
$options['tags'][] = [
'id' => (int) $tag_id,
];
}

if (!empty($callback_url)) {
$options['callback_url'] = $callback_url;
}

// Send request.
return $this->delete(
'bulk/tags',
$options
);
}

/**
* Updates the name of a tag.
*
Expand Down Expand Up @@ -1512,21 +1565,22 @@ public function create_subscribers(array $subscribers, string $callback_url = ''
/**
* Filter subscribers based on engagement.
*
* @param array<int, array<string, mixed>> $all Array of filter conditions where ALL must be met (AND logic). Each condition can have.
* - 'type' (string).
* - 'count_greater_than' (int|null).
* - 'count_less_than' (int|null).
* - 'after' (\DateTime|null).
* - 'before' (\DateTime|null).
* - 'states' (array<string>).
* - 'any' (array<int|string, mixed>|null).
* @param string $counting_mode Controls how engagement-filter count thresholds are tallied.
* - 'raw' (default) counts every event — five opens of the same email = five.
* - 'unique_email' counts distinct emails on which the action occurred.
* @param boolean $include_total_count To include the total count of records in the response, use true.
* @param string $after_cursor Return results after the given pagination cursor.
* @param string $before_cursor Return results before the given pagination cursor.
* @param integer $per_page Number of results to return.
* @param list<array<string, mixed>> $all Array of filter conditions where ALL must be met (AND logic). Each condition can have.
* - 'type' (string).
* - 'count_greater_than' (int|null).
* - 'count_less_than' (int|null).
* - 'after' (\DateTime|null).
* - 'before' (\DateTime|null).
* - 'states' (array<string>).
* - 'any' (array<int|string, mixed>|null).
* @param string $counting_mode Controls how engagement-filter count thresholds are tallied.
* - 'raw' (default) counts every event — five opens of the same email = five.
* - 'unique_email' counts distinct emails on which the action occurred.
* @param list<array<string, mixed>> $include Array of additional fields to embed on each subscriber row.
* @param boolean $include_total_count To include the total count of records in the response, use true.
* @param string $after_cursor Return results after the given pagination cursor.
* @param string $before_cursor Return results before the given pagination cursor.
* @param integer $per_page Number of results to return.
*
* @since 2.4.0
*
Expand All @@ -1537,6 +1591,7 @@ public function create_subscribers(array $subscribers, string $callback_url = ''
public function filter_subscribers(
array $all = [],
string $counting_mode = 'raw',
array $include = [],
bool $include_total_count = false,
string $after_cursor = '',
string $before_cursor = '',
Expand Down Expand Up @@ -1584,6 +1639,7 @@ public function filter_subscribers(
[
'all' => $options,
'counting_mode' => $counting_mode,
'include' => $include,
],
$include_total_count,
$after_cursor,
Expand Down Expand Up @@ -2670,8 +2726,8 @@ public function put(string $endpoint, array $args = [])
/**
* Performs a DELETE request to the API.
*
* @param string $endpoint API Endpoint.
* @param array<string, int|string|array<string, int|string>|string> $args Request arguments.
* @param string $endpoint API Endpoint.
* @param array<string, bool|integer|float|string|null|array<int|string, array<string, int|string>|boolean|integer|float|string>> $args Request arguments.
*
* @return false|mixed
*/
Expand Down
8 changes: 7 additions & 1 deletion tests/ConvertKitAPIKeyTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -237,13 +237,19 @@ public function testRequestHeadersMethodWithTypeAndAuthDisabled()
*
* @return void
*/
public function testCreateTags()
public function testCreateAndDeleteTags()
{
$this->expectException(ClientException::class);
$result = $this->api->create_tags([
'Tag Test ' . mt_rand(),
'Tag Test ' . mt_rand(),
]);

$this->expectException(ClientException::class);
$result = $this->api->delete_tags([
12345,
23456,
]);
}

/**
Expand Down
120 changes: 97 additions & 23 deletions tests/ConvertKitAPITest.php
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,28 @@ public function testGetFormsWithArchivedStatus()
}
}

/**
* Test that get_forms() returns the subscriber count
* when included in the `include` argument.
*
* @since 2.6.0
*
* @return void
*/
public function testGetFormsWithSubscriberCount()
{
$result = $this->api->get_forms(
include: ['subscriber_count']
);

// Assert forms and pagination exist.
$this->assertDataExists($result, 'forms');
$this->assertPaginationExists($result);

// Assert subscriber count is included.
$this->assertArrayHasKey('subscriber_count', get_object_vars($result->forms[0]));
}

/**
* Test that get_forms() returns the expected data
* when the total count is included.
Expand Down Expand Up @@ -2220,6 +2242,28 @@ public function testGetTags()
$this->assertArrayHasKey('created_at', $tag);
}

/**
* Test that get_tags() returns the subscriber count
* when included in the `include` argument.
*
* @since 2.6.0
*
* @return void
*/
public function testGetTagsWithSubscriberCount()
{
$result = $this->api->get_tags(
include: ['subscriber_count']
);

// Assert forms and pagination exist.
$this->assertDataExists($result, 'tags');
$this->assertPaginationExists($result);

// Assert subscriber count is included.
$this->assertArrayHasKey('subscriber_count', get_object_vars($result->tags[0]));
}

/**
* Test that get_tags() returns the expected data
* when the total count is included.
Expand Down Expand Up @@ -2366,44 +2410,36 @@ public function testCreateTagThatExists()
}

/**
* Test that create_tags() returns the expected data.
* Test that create_tags() and delete_tags() returns the expected data.
*
* @since 1.1.0
*
* @return void
*/
public function testCreateTags()
public function testCreateAndDeleteTags()
{
$tagNames = [
'Tag Test ' . mt_rand(),
'Tag Test ' . mt_rand(),
];

// Add mock handler for this API request, as the API doesn't provide
// a method to delete tags to cleanup the test.
$this->api = $this->mockResponse(
api: $this->api,
responseBody: [
'tags' => [
[
'id' => 12345,
'name' => $tagNames[0],
'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z',
],
[
'id' => 23456,
'name' => $tagNames[1],
'created_at' => date('Y-m-d') . 'T' . date('H:i:s') . 'Z',
],
],
'failures' => [],
]
);

// Create tags.
$result = $this->api->create_tags($tagNames);

// Assert no failures.
$this->assertCount(0, $result->failures);

// Build tag IDs array.
$ids = [];
foreach ($result->tags as $tag) {
$ids[] = $tag->id;
}

// Delete tags.
$result = $this->api->delete_tags($ids);

// Assert no failures.
$this->assertCount(0, $result->failures);
}

/**
Expand Down Expand Up @@ -4565,6 +4601,44 @@ public function testFilterSubscribersWithCountingMode()
$this->assertPaginationExists($result);
}

/**
* Test that filter_subscribers() returns the expected data
* when the `include` parameter is specified.
*
* @since 2.6.0
*
* @return void
*/
public function testFilterSubscribersWithInclude()
{
$result = $this->api->filter_subscribers(
all: [
[
'type' => 'opens',
'count_greater_than' => 0,
'count_less_than' => 100,
'after' => new \DateTime('2024-01-01'),
'before' => new \DateTime('2029-01-01'),
'states' => [
'active',
],
]
],
include: [
[
'type' => 'tags',
],
]
);

// Assert subscribers and pagination exist.
$this->assertDataExists($result, 'subscribers');
$this->assertPaginationExists($result);

// Assert tags are included.
$this->assertArrayHasKey('tags', get_object_vars($result->subscribers[0]));
}

/**
* Test that filter_subscribers() returns the expected data
* when no parameters are specified.
Expand Down