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
30 changes: 30 additions & 0 deletions src/Model/AdBusinessCenter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** A Business Center, or the network's equivalent grouping of ad accounts. */
final class AdBusinessCenter extends Model
{
private function __construct(
array $raw,
public readonly string $id,
public readonly string $name,
public readonly ?string $role,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'id'),
self::requiredStr($data, 'name'),
self::str($data, 'role'),
);
}
}
45 changes: 45 additions & 0 deletions src/Model/AdComment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** A comment on an ad, read live from the network and never stored. */
final class AdComment extends Model
{
private function __construct(
array $raw,
public readonly string $id,
public readonly ?string $adId,
public readonly string $text,
public readonly ?string $authorName,
public readonly ?string $authorAvatarUrl,
public readonly ?string $createdAt,
public readonly int $likes,
public readonly int $replyCount,
public readonly bool $hidden,
/** The comment this one answers, when it is not on the ad itself. */
public readonly ?string $parentId,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'id'),
self::str($data, 'ad_id'),
self::str($data, 'text') ?? '',
self::str($data, 'author_name'),
self::str($data, 'author_avatar_url'),
self::str($data, 'created_at'),
self::int($data, 'likes') ?? 0,
self::int($data, 'reply_count') ?? 0,
self::bool($data, 'hidden') ?? false,
self::str($data, 'parent_id'),
);
}
}
31 changes: 31 additions & 0 deletions src/Model/AdCommentsPage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** One page of an ad's comments; pass `nextCursor` back as `after`. */
final class AdCommentsPage extends Model
{
/**
* @param array<int, AdComment> $comments
*/
private function __construct(
array $raw,
public readonly array $comments,
public readonly ?string $nextCursor,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
AdComment::listFrom(self::seq($data, 'comments')),
self::str($data, 'next_cursor'),
);
}
}
33 changes: 33 additions & 0 deletions src/Model/AdIdentity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** The account an ad runs as. Meta calls it a Page, TikTok an identity. */
final class AdIdentity extends Model
{
private function __construct(
array $raw,
public readonly string $id,
/** The network's own identity kind, e.g. CUSTOMIZED_USER. */
public readonly string $type,
public readonly string $name,
public readonly ?string $avatarUrl,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'id'),
self::requiredStr($data, 'type'),
self::requiredStr($data, 'name'),
self::str($data, 'avatar_url'),
);
}
}
36 changes: 36 additions & 0 deletions src/Model/SparkPost.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Fopost\Sdk\Model;

/** A post already live on the network, offered as the source of a Spark ad. */
final class SparkPost extends Model
{
private function __construct(
array $raw,
public readonly string $id,
public readonly string $identityId,
public readonly ?string $caption,
public readonly ?string $thumbnailUrl,
public readonly ?string $createdAt,
public readonly ?int $views,
) {
parent::__construct($raw);
}

public static function fromArray(mixed $data): static
{
$data = is_array($data) ? $data : [];

return new self(
$data,
self::requiredStr($data, 'id'),
self::requiredStr($data, 'identity_id'),
self::str($data, 'caption'),
self::str($data, 'thumbnail_url'),
self::str($data, 'created_at'),
self::int($data, 'views'),
);
}
}
149 changes: 149 additions & 0 deletions src/Resource/AdsResource.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@
use Fopost\Sdk\Model\Ad;
use Fopost\Sdk\Model\AdAccountTree;
use Fopost\Sdk\Model\AdActivityResult;
use Fopost\Sdk\Model\AdBusinessCenter;
use Fopost\Sdk\Model\AdCampaign;
use Fopost\Sdk\Model\AdCommentsPage;
use Fopost\Sdk\Model\AdConnection;
use Fopost\Sdk\Model\AdCreative;
use Fopost\Sdk\Model\AdIdentity;
use Fopost\Sdk\Model\AdInsightsReport;
use Fopost\Sdk\Model\AdLabel;
use Fopost\Sdk\Model\AdLibraryPage;
Expand Down Expand Up @@ -40,6 +43,7 @@
use Fopost\Sdk\Model\ProductFeedUpload;
use Fopost\Sdk\Model\ProductSet;
use Fopost\Sdk\Model\ReachEstimate;
use Fopost\Sdk\Model\SparkPost;
use Fopost\Sdk\Model\ReachFrequencyPrediction;
use Fopost\Sdk\Model\ReachFrequencyResult;
use Fopost\Sdk\Model\TargetingOption;
Expand Down Expand Up @@ -203,6 +207,7 @@ public function create(
?string $mediaUrl = null,
?bool $paused = null,
?string $urlTags = null,
?string $sparkPostId = null,
): Ad {
$body = self::compact([
'workspaceId' => $workspaceId,
Expand All @@ -219,6 +224,7 @@ public function create(
'mediaUrl' => $mediaUrl,
'paused' => $paused,
'urlTags' => $urlTags,
'sparkPostId' => $sparkPostId,
]);

return Ad::fromArray(self::unwrap($this->http->post('/ads', $body)));
Expand Down Expand Up @@ -351,6 +357,147 @@ public function leads(
])));
}

/**
* TikTok's Business Centers, the one network-named read in this resource.
*
* @return array<int, AdBusinessCenter>
*/
public function tiktokBusinessCenters(string $connectionId, ?string $workspaceId = null): array
{
return AdBusinessCenter::listFrom(self::unwrap($this->http->get(
'/ads/tiktok/business-centers',
self::scope($workspaceId, $connectionId),
)));
}

/**
* The accounts an ad can run as; an identity id is a $pageId.
*
* @return array<int, AdIdentity>
*/
public function tiktokIdentities(
string $connectionId,
string $adAccountId,
?string $workspaceId = null,
): array {
return AdIdentity::listFrom(self::unwrap($this->http->get('/ads/tiktok/identities', [
'workspace_id' => $workspaceId,
'connection_id' => $connectionId,
'ad_account_id' => $adAccountId,
])));
}

/**
* Posts already live under an identity, each a candidate Spark ad.
*
* @return array<int, SparkPost>
*/
public function sparkPosts(
string $connectionId,
string $adAccountId,
string $identityId,
?string $workspaceId = null,
): array {
return SparkPost::listFrom(self::unwrap($this->http->get('/ads/spark-posts', [
'workspace_id' => $workspaceId,
'connection_id' => $connectionId,
'ad_account_id' => $adAccountId,
'identity_id' => $identityId,
])));
}

/**
* Offline conversions. Identifiers are hashed before they leave FoPost.
*
* @param array<int, array<string, mixed>> $events up to 1000 per call
*
* @return int the number the network accepted
*/
public function uploadConversions(
string $workspaceId,
string $connectionId,
string $adAccountId,
string $pixelId,
array $events,
): int {
$result = self::unwrap($this->http->post('/ads/conversions', [
'workspaceId' => $workspaceId,
'connectionId' => $connectionId,
'adAccountId' => $adAccountId,
'pixelId' => $pixelId,
'events' => $events,
]));

return is_array($result) && isset($result['accepted']) ? (int) $result['accepted'] : 0;
}

/** One page of an ad's comments; pass `nextCursor` back as $after. */
public function comments(
string $connectionId,
string $adId,
?string $after = null,
?string $workspaceId = null,
): AdCommentsPage {
return AdCommentsPage::fromArray(self::unwrap($this->http->get('/ads/comments', [
'workspace_id' => $workspaceId,
'connection_id' => $connectionId,
'ad_id' => $adId,
'after' => $after,
])));
}

/**
* Needs the `publish` scope as well as `ads`.
*
* @return string the reply's id on the network
*/
public function replyToComment(
string $commentId,
string $workspaceId,
string $connectionId,
string $adId,
string $text,
): string {
$result = self::unwrap($this->http->post("/ads/comments/{$commentId}/reply", [
'workspaceId' => $workspaceId,
'connectionId' => $connectionId,
'adId' => $adId,
'text' => $text,
]));

return is_array($result) && isset($result['replyId']) ? (string) $result['replyId'] : '';
}

/** Needs the `publish` scope as well as `ads`. */
public function setCommentHidden(
string $commentId,
string $workspaceId,
string $connectionId,
string $adId,
bool $hidden,
): void {
$this->http->post("/ads/comments/{$commentId}/hide", [
'workspaceId' => $workspaceId,
'connectionId' => $connectionId,
'adId' => $adId,
'hidden' => $hidden,
]);
}

/** One already gone on the network succeeds. Needs `publish` as well as `ads`. */
public function deleteComment(
string $commentId,
string $workspaceId,
string $connectionId,
string $adId,
): void {
$this->http->request('DELETE', "/ads/comments/{$commentId}", [
'workspaceId' => $workspaceId,
'connectionId' => $connectionId,
'adId' => $adId,
]);
}

/** Every campaign on the ad account with its ad sets and ads, read live. */
public function accountTree(string $adAccountId, string $connectionId, ?string $workspaceId = null): AdAccountTree
{
Expand All @@ -371,6 +518,7 @@ public function createCampaign(
string $name,
string $goal,
?bool $paused = null,
?bool $smartPlus = null,
): AdCampaign {
$body = self::compact([
'workspaceId' => $workspaceId,
Expand All @@ -379,6 +527,7 @@ public function createCampaign(
'name' => $name,
'goal' => $goal,
'paused' => $paused,
'smartPlus' => $smartPlus,
]);

return AdCampaign::fromArray(self::unwrap($this->http->post('/ads/campaigns', $body)));
Expand Down
Loading
Loading