feat: extract business providers from app/Plug/Business
GoogleMyBusiness provider with Core\Plug\Business namespace alignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
599039f283
commit
f6491f075e
6 changed files with 653 additions and 0 deletions
17
composer.json
Normal file
17
composer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "core/php-plug-business",
|
||||
"description": "Business platform integrations for the Plug framework",
|
||||
"type": "library",
|
||||
"license": "EUPL-1.2",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"core/php": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\Plug\\Business\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
156
src/GoogleMyBusiness/Auth.php
Normal file
156
src/GoogleMyBusiness/Auth.php
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Business\GoogleMyBusiness;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Authenticable;
|
||||
use Core\Plug\Contract\Refreshable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Google My Business OAuth 2.0 authentication.
|
||||
*/
|
||||
class Auth implements Authenticable, Refreshable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const AUTH_URL = 'https://accounts.google.com/o/oauth2/v2/auth';
|
||||
|
||||
private const TOKEN_URL = 'https://oauth2.googleapis.com/token';
|
||||
|
||||
private string $clientId;
|
||||
|
||||
private string $clientSecret;
|
||||
|
||||
private string $redirectUrl;
|
||||
|
||||
private array $scope = [
|
||||
'https://www.googleapis.com/auth/business.manage',
|
||||
];
|
||||
|
||||
public function __construct(string $clientId = '', string $clientSecret = '', string $redirectUrl = '')
|
||||
{
|
||||
$this->clientId = $clientId;
|
||||
$this->clientSecret = $clientSecret;
|
||||
$this->redirectUrl = $redirectUrl;
|
||||
}
|
||||
|
||||
public static function identifier(): string
|
||||
{
|
||||
return 'googlemybusiness';
|
||||
}
|
||||
|
||||
public static function name(): string
|
||||
{
|
||||
return 'Google My Business';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get OAuth URL.
|
||||
*/
|
||||
public function getAuthUrl(): string
|
||||
{
|
||||
$params = [
|
||||
'client_id' => $this->clientId,
|
||||
'redirect_uri' => $this->redirectUrl,
|
||||
'scope' => implode(' ', $this->scope),
|
||||
'response_type' => 'code',
|
||||
'access_type' => 'offline',
|
||||
'prompt' => 'consent',
|
||||
'include_granted_scopes' => 'true',
|
||||
];
|
||||
|
||||
return self::AUTH_URL.'?'.http_build_query($params);
|
||||
}
|
||||
|
||||
/**
|
||||
* Exchange code for access token.
|
||||
*
|
||||
* @param array $params ['code' => string]
|
||||
*/
|
||||
public function requestAccessToken(array $params): array
|
||||
{
|
||||
if (! isset($params['code'])) {
|
||||
return ['error' => 'Authorization code is required'];
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->asForm()
|
||||
->post(self::TOKEN_URL, [
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'redirect_uri' => $this->redirectUrl,
|
||||
'grant_type' => 'authorization_code',
|
||||
'code' => $params['code'],
|
||||
]);
|
||||
|
||||
if (! $response->successful()) {
|
||||
$error = $response->json();
|
||||
|
||||
return ['error' => $error['error_description'] ?? $error['error'] ?? 'Token exchange failed'];
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
return [
|
||||
'access_token' => $data['access_token'],
|
||||
'refresh_token' => $data['refresh_token'] ?? '',
|
||||
'expires_in' => now('UTC')->addSeconds($data['expires_in'] ?? 3600)->timestamp,
|
||||
'token_type' => $data['token_type'] ?? 'Bearer',
|
||||
'scope' => $data['scope'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the access token.
|
||||
*/
|
||||
public function refresh(): Response
|
||||
{
|
||||
$refreshToken = $this->token['refresh_token'] ?? null;
|
||||
|
||||
if (! $refreshToken) {
|
||||
return $this->error('No refresh token available');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->asForm()
|
||||
->post(self::TOKEN_URL, [
|
||||
'client_id' => $this->clientId,
|
||||
'client_secret' => $this->clientSecret,
|
||||
'grant_type' => 'refresh_token',
|
||||
'refresh_token' => $refreshToken,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) use ($refreshToken) {
|
||||
return [
|
||||
'access_token' => $data['access_token'],
|
||||
'refresh_token' => $refreshToken, // Google doesn't return new refresh token
|
||||
'expires_in' => now('UTC')->addSeconds($data['expires_in'] ?? 3600)->timestamp,
|
||||
'token_type' => $data['token_type'] ?? 'Bearer',
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
public function getAccount(): Response
|
||||
{
|
||||
return (new Read)->withToken($this->token)->me();
|
||||
}
|
||||
|
||||
/**
|
||||
* Google Business Profile URL.
|
||||
*/
|
||||
public static function externalAccountUrl(string $placeId): string
|
||||
{
|
||||
if ($placeId) {
|
||||
return "https://www.google.com/maps/place/?q=place_id:{$placeId}";
|
||||
}
|
||||
|
||||
return 'https://business.google.com';
|
||||
}
|
||||
}
|
||||
53
src/GoogleMyBusiness/Delete.php
Normal file
53
src/GoogleMyBusiness/Delete.php
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Business\GoogleMyBusiness;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Deletable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Google My Business local post deletion.
|
||||
*/
|
||||
class Delete implements Deletable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://mybusiness.googleapis.com/v4';
|
||||
|
||||
/**
|
||||
* Delete a local post.
|
||||
*
|
||||
* @param string $id Full post resource name (e.g., accounts/xxx/locations/xxx/localPosts/xxx)
|
||||
*/
|
||||
public function delete(string $id): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->delete(self::API_URL."/{$id}");
|
||||
|
||||
// GMB returns 200 with empty body on success
|
||||
if ($response->successful()) {
|
||||
return $this->ok([
|
||||
'deleted' => true,
|
||||
'id' => $id,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'deleted' => false,
|
||||
'error' => $data['error']['message'] ?? 'Failed to delete post',
|
||||
]);
|
||||
}
|
||||
}
|
||||
117
src/GoogleMyBusiness/Locations.php
Normal file
117
src/GoogleMyBusiness/Locations.php
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Business\GoogleMyBusiness;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Listable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Google My Business locations listing.
|
||||
*/
|
||||
class Locations implements Listable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const BUSINESS_API_URL = 'https://mybusinessbusinessinformation.googleapis.com/v1';
|
||||
|
||||
private ?string $accountName = null;
|
||||
|
||||
/**
|
||||
* Set the account to list locations for.
|
||||
*/
|
||||
public function forAccount(string $accountName): self
|
||||
{
|
||||
$this->accountName = $accountName;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* List business locations.
|
||||
*/
|
||||
public function listEntities(): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
if (! $this->accountName) {
|
||||
return $this->error('Account name is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->get(self::BUSINESS_API_URL."/{$this->accountName}/locations", [
|
||||
'readMask' => 'name,title,storefrontAddress,metadata,phoneNumbers,websiteUri',
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
return [
|
||||
'locations' => array_map(fn ($location) => [
|
||||
'id' => $location['name'] ?? '',
|
||||
'title' => $location['title'] ?? '',
|
||||
'address' => $this->formatAddress($location['storefrontAddress'] ?? []),
|
||||
'place_id' => $location['metadata']['placeId'] ?? '',
|
||||
'maps_url' => $location['metadata']['mapsUri'] ?? '',
|
||||
'phone' => $location['phoneNumbers']['primaryPhone'] ?? null,
|
||||
'website' => $location['websiteUri'] ?? null,
|
||||
], $data['locations'] ?? []),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific location.
|
||||
*/
|
||||
public function get(string $locationName): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->get(self::BUSINESS_API_URL."/{$locationName}", [
|
||||
'readMask' => 'name,title,storefrontAddress,metadata,phoneNumbers,websiteUri,regularHours,specialHours',
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
return [
|
||||
'id' => $data['name'] ?? '',
|
||||
'title' => $data['title'] ?? '',
|
||||
'address' => $this->formatAddress($data['storefrontAddress'] ?? []),
|
||||
'place_id' => $data['metadata']['placeId'] ?? '',
|
||||
'maps_url' => $data['metadata']['mapsUri'] ?? '',
|
||||
'phone' => $data['phoneNumbers']['primaryPhone'] ?? null,
|
||||
'website' => $data['websiteUri'] ?? null,
|
||||
'regular_hours' => $data['regularHours'] ?? null,
|
||||
'special_hours' => $data['specialHours'] ?? null,
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Format address array to string.
|
||||
*/
|
||||
private function formatAddress(array $address): string
|
||||
{
|
||||
$parts = array_filter([
|
||||
$address['addressLines'][0] ?? '',
|
||||
$address['locality'] ?? '',
|
||||
$address['administrativeArea'] ?? '',
|
||||
$address['postalCode'] ?? '',
|
||||
$address['regionCode'] ?? '',
|
||||
]);
|
||||
|
||||
return implode(', ', $parts);
|
||||
}
|
||||
}
|
||||
164
src/GoogleMyBusiness/Post.php
Normal file
164
src/GoogleMyBusiness/Post.php
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Business\GoogleMyBusiness;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Postable;
|
||||
use Core\Plug\Response;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
/**
|
||||
* Google My Business local post publishing.
|
||||
*/
|
||||
class Post implements Postable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://mybusiness.googleapis.com/v4';
|
||||
|
||||
/**
|
||||
* Publish a local post to GMB.
|
||||
*
|
||||
* @param string $text Post summary text
|
||||
* @param Collection $media Media items (photos only, no video support)
|
||||
* @param array $params location (required), post_type, cta_type, cta_url, event_*, coupon_code, etc.
|
||||
*/
|
||||
public function publish(string $text, Collection $media, array $params = []): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$location = $params['location'] ?? '';
|
||||
if (! $location) {
|
||||
return $this->error('Location is required for Google My Business posts');
|
||||
}
|
||||
|
||||
$postType = $params['post_type'] ?? 'STANDARD';
|
||||
$payload = $this->buildPayload($text, $media, $postType, $params);
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->post(self::API_URL."/{$location}/localPosts", $payload);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['name'] ?? '',
|
||||
'state' => $data['state'] ?? 'LIVE',
|
||||
'url' => $data['searchUrl'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build post payload based on type.
|
||||
*/
|
||||
private function buildPayload(string $text, Collection $media, string $postType, array $params): array
|
||||
{
|
||||
$payload = [
|
||||
'languageCode' => $params['language'] ?? 'en',
|
||||
'summary' => $text,
|
||||
'topicType' => $postType,
|
||||
];
|
||||
|
||||
// Add media (GMB only supports photos)
|
||||
if ($media->isNotEmpty()) {
|
||||
$mediaItem = $media->first();
|
||||
$url = is_array($mediaItem) ? ($mediaItem['url'] ?? '') : $mediaItem;
|
||||
|
||||
if ($url) {
|
||||
$payload['media'] = [
|
||||
'mediaFormat' => 'PHOTO',
|
||||
'sourceUrl' => $url,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Call to action
|
||||
if (! empty($params['cta_type']) && ! empty($params['cta_url'])) {
|
||||
$payload['callToAction'] = [
|
||||
'actionType' => $params['cta_type'], // BOOK, ORDER, SHOP, LEARN_MORE, SIGN_UP, CALL
|
||||
'url' => $params['cta_url'],
|
||||
];
|
||||
}
|
||||
|
||||
// Event details
|
||||
if ($postType === 'EVENT' && ! empty($params['event_title'])) {
|
||||
$payload['event'] = [
|
||||
'title' => $params['event_title'],
|
||||
'schedule' => [],
|
||||
];
|
||||
|
||||
if (! empty($params['event_start'])) {
|
||||
$payload['event']['schedule']['startDate'] = $this->formatDate($params['event_start']);
|
||||
$payload['event']['schedule']['startTime'] = $this->formatTime($params['event_start']);
|
||||
}
|
||||
|
||||
if (! empty($params['event_end'])) {
|
||||
$payload['event']['schedule']['endDate'] = $this->formatDate($params['event_end']);
|
||||
$payload['event']['schedule']['endTime'] = $this->formatTime($params['event_end']);
|
||||
}
|
||||
}
|
||||
|
||||
// Offer details
|
||||
if ($postType === 'OFFER') {
|
||||
$payload['offer'] = [];
|
||||
|
||||
if (! empty($params['coupon_code'])) {
|
||||
$payload['offer']['couponCode'] = $params['coupon_code'];
|
||||
}
|
||||
|
||||
if (! empty($params['redeem_url'])) {
|
||||
$payload['offer']['redeemOnlineUrl'] = $params['redeem_url'];
|
||||
}
|
||||
|
||||
if (! empty($params['terms'])) {
|
||||
$payload['offer']['termsConditions'] = $params['terms'];
|
||||
}
|
||||
}
|
||||
|
||||
return $payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format datetime to GMB date array.
|
||||
*/
|
||||
private function formatDate(string $datetime): array
|
||||
{
|
||||
$date = new \DateTime($datetime);
|
||||
|
||||
return [
|
||||
'year' => (int) $date->format('Y'),
|
||||
'month' => (int) $date->format('m'),
|
||||
'day' => (int) $date->format('d'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Format datetime to GMB time array.
|
||||
*/
|
||||
private function formatTime(string $datetime): array
|
||||
{
|
||||
$date = new \DateTime($datetime);
|
||||
|
||||
return [
|
||||
'hours' => (int) $date->format('H'),
|
||||
'minutes' => (int) $date->format('i'),
|
||||
'seconds' => 0,
|
||||
'nanos' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* GMB posts don't have public URLs.
|
||||
*/
|
||||
public static function externalPostUrl(string $locationName, string $postId): string
|
||||
{
|
||||
return '';
|
||||
}
|
||||
}
|
||||
146
src/GoogleMyBusiness/Read.php
Normal file
146
src/GoogleMyBusiness/Read.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Business\GoogleMyBusiness;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Concern\ManagesTokens;
|
||||
use Core\Plug\Concern\UsesHttp;
|
||||
use Core\Plug\Contract\Readable;
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* Google My Business account and location reading.
|
||||
*/
|
||||
class Read implements Readable
|
||||
{
|
||||
use BuildsResponse;
|
||||
use ManagesTokens;
|
||||
use UsesHttp;
|
||||
|
||||
private const API_URL = 'https://mybusiness.googleapis.com/v4';
|
||||
|
||||
private const BUSINESS_API_URL = 'https://mybusinessbusinessinformation.googleapis.com/v1';
|
||||
|
||||
/**
|
||||
* Get a specific local post.
|
||||
*
|
||||
* @param string $id Full post resource name
|
||||
*/
|
||||
public function get(string $id): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->get(self::API_URL."/{$id}");
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'id' => $data['name'] ?? '',
|
||||
'summary' => $data['summary'] ?? '',
|
||||
'state' => $data['state'] ?? '',
|
||||
'topic_type' => $data['topicType'] ?? '',
|
||||
'create_time' => $data['createTime'] ?? null,
|
||||
'update_time' => $data['updateTime'] ?? null,
|
||||
'media' => $data['media'] ?? null,
|
||||
'call_to_action' => $data['callToAction'] ?? null,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current account info.
|
||||
*/
|
||||
public function me(): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->get(self::API_URL.'/accounts');
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
$accounts = $data['accounts'] ?? [];
|
||||
|
||||
if (empty($accounts)) {
|
||||
return [
|
||||
'id' => null,
|
||||
'name' => null,
|
||||
'error' => 'No business accounts found',
|
||||
];
|
||||
}
|
||||
|
||||
// Return first account as primary
|
||||
$account = $accounts[0];
|
||||
|
||||
return [
|
||||
'id' => $account['name'] ?? '',
|
||||
'name' => $account['accountName'] ?? 'Google Business Profile',
|
||||
'type' => $account['type'] ?? '',
|
||||
'role' => $account['role'] ?? '',
|
||||
'state' => $account['state']['status'] ?? '',
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List accounts.
|
||||
*/
|
||||
public function list(array $params = []): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->get(self::API_URL.'/accounts');
|
||||
|
||||
return $this->fromHttp($response, function ($data) {
|
||||
return [
|
||||
'accounts' => array_map(fn ($account) => [
|
||||
'id' => $account['name'] ?? '',
|
||||
'name' => $account['accountName'] ?? '',
|
||||
'type' => $account['type'] ?? '',
|
||||
'role' => $account['role'] ?? '',
|
||||
], $data['accounts'] ?? []),
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* List local posts for a location.
|
||||
*/
|
||||
public function posts(string $locationName, array $params = []): Response
|
||||
{
|
||||
$accessToken = $this->accessToken();
|
||||
if (! $accessToken) {
|
||||
return $this->error('Access token is required');
|
||||
}
|
||||
|
||||
$response = $this->http()
|
||||
->withToken($accessToken)
|
||||
->get(self::API_URL."/{$locationName}/localPosts", [
|
||||
'pageSize' => $params['per_page'] ?? 10,
|
||||
'pageToken' => $params['page_token'] ?? null,
|
||||
]);
|
||||
|
||||
return $this->fromHttp($response, fn ($data) => [
|
||||
'posts' => array_map(fn ($post) => [
|
||||
'id' => $post['name'] ?? '',
|
||||
'summary' => $post['summary'] ?? '',
|
||||
'state' => $post['state'] ?? '',
|
||||
'topic_type' => $post['topicType'] ?? '',
|
||||
'create_time' => $post['createTime'] ?? null,
|
||||
], $data['localPosts'] ?? []),
|
||||
'next_page_token' => $data['nextPageToken'] ?? null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue