Skip to content

Instantly share code, notes, and snippets.

@croxton
Last active April 23, 2025 16:15
Show Gist options
  • Save croxton/3ddac43c7770e899e70dc72b79a086ad to your computer and use it in GitHub Desktop.
Save croxton/3ddac43c7770e899e70dc72b79a086ad to your computer and use it in GitHub Desktop.
Passle API -> Craft CMS -> Feed Me
<?php
/**
* Passle Import Controller
*
* Transform Passle API queries to a format that can be imported with Feed Me.
*
* @link https://hallmark-design.co.uk
* @copyright Copyright (c) 2025 Mark Croxton
*/
namespace modules\custommodule\controllers;
use Craft;
use craft\web\Controller;
use GuzzleHttp\Exception\GuzzleException;
use yii\base\InvalidConfigException;
use yii\web\Response;
use GuzzleHttp\Client;
use Craft\helpers\StringHelper;
/**
* Import Controller
*
* @author Mark Croxton
* @package customModule
* @since 1.0.0
*/
class ImportController extends Controller
{
// Protected Properties
// =========================================================================
/**
* @var bool|array Allows anonymous access to this controller's actions.
* The actions must be in 'kebab-case'
* @access protected
*/
protected int|bool|array $allowAnonymous = [
'posts',
'tags',
'people',
'sync'
];
// Public Methods
// =========================================================================
/**
* Import Passle posts
*
* @param int $page
* @return Response
* @throws GuzzleException
*/
public function actionPosts(int $page=1) : Response
{
$url = getenv('PASSLE_ENDPOINT_POSTS');
$shortCode = getenv('PASSLE_SHORT_CODE');
$limit = 20;
$jsonArray = $this->passleApi($url, [
'PassleShortcode' => $shortCode,
'PageNumber' => $page,
'ItemsPerPage' => $limit,
'SortOrder' => 1,
'FeaturedImage' => [
'Width' => 1600,
'Height' => 900
]
]
);
// Add status of posts
foreach ($jsonArray['Posts'] as &$post) {
$post['Status'] = 'enabled';
}
unset($post);
if ( array_key_exists('TotalCount', $jsonArray) && ($page * $limit) < $jsonArray['TotalCount'] ) {
$jsonArray['next'] = Craft::getAlias('@web/api/custom/import/posts?page=') . ($page + 1);
}
// output feed
$output = $this->asJson($jsonArray);
$output->setStatusCode(200);
return $output;
}
/**
* Import Passle tag groups
*
* @return Response
* @throws GuzzleException
*/
public function actionTags() : Response
{
$url = getenv('PASSLE_ENDPOINT_TAGGROUPS');
$shortCode = getenv('PASSLE_SHORT_CODE');
$jsonArray = $this->passleApi($url, [
'PassleShortcode' => $shortCode
]
);
$formatted = [];
if (array_key_exists('TagGroups', $jsonArray)) {
foreach ($jsonArray['TagGroups'] as $group) {
$formatted[] = array(
'title' => $group['Name'],
'slug' => StringHelper::slugify($group['Name']),
'parent' => ""
);
foreach ($group['Tags'] as $tag) {
$formatted[] = array(
'title' => $tag,
'slug' => StringHelper::slugify($tag),
'parent' => $group['Name']
);
}
}
}
// Get a one-dimensional array of all the tags that were grouped
$groupedTags = array_column($formatted, 'title');
// Now get all other tags
$url = getenv('PASSLE_ENDPOINT_TAGS');
// Create a new Guzzle Client instance
$jsonArray = $this->passleApi($url, [
'PassleShortcode' => $shortCode
]
);
if (array_key_exists('Tags', $jsonArray)) {
foreach ($jsonArray['Tags'] as $tag) {
if (!in_array($tag, $groupedTags, true)) {
$formatted[] = [
'title' => $tag,
'slug' => StringHelper::slugify($tag),
'parent' => 'Uncategorized'
];
}
}
}
// Output feed
$output = $this->asJson($formatted);
$output->setStatusCode(200);
return $output;
}
/**
* Import Passle people
*
* @param int $page
* @return Response
* @throws GuzzleException
*/
public function actionPeople(int $page=1) : Response
{
$url = getenv('PASSLE_ENDPOINT_PEOPLE');
$shortCode = getenv('PASSLE_SHORT_CODE');
$limit = 200;
// Passle API query
$jsonArray = $this->passleApi($url, [
'PassleShortcode' => $shortCode,
'PageNumber' => $page,
'ItemsPerPage' => $limit,
'SortOrder' => 1,
'Image' => [
'Width' => 800,
'Height' => 800
]
]
);
if ( array_key_exists('TotalCount', $jsonArray) && ($page * $limit) < $jsonArray['TotalCount'] ) {
$jsonArray['next'] = Craft::getAlias('@web/api/custom/import/people?page=') . ($page + 1);
}
// output feed
$output = $this->asJson($jsonArray);
$output->setStatusCode(200);
return $output;
}
/**
* Sync Passle posts and tags
*
* @return Response
* @throws GuzzleException
* @throws \Exception
* @throws \Throwable
*/
public function actionSync() : Response
{
$url = getenv('PASSLE_ENDPOINT_SYNC');
$shortCode = getenv('PASSLE_SHORT_CODE');
// Get last sync date
$passleSet = Craft::$app->globals->getSetByHandle('passle');
$dateFrom = \DateTimeImmutable::createFromMutable($passleSet->eventDate)
->setTimezone(new \DateTimeZone("UTC"))
->format("Y-m-d\\TH:i:sp");
// Passle API query
$jsonArray = $this->passleApi($url, [
'PassleShortcode' => $shortCode,
'DateFrom' => $dateFrom
]
);
/*
SyncPost = 1
DeletePost = 2
SyncAuthor = 3
DeleteAuthor = 4
UpdateFeaturedPost = 5
Ping = 6
*/
// Build an array of shortcodes to update or delete (disable)
$postsToUpdate = [];
$postsToDelete = [];
if (array_key_exists('Updates', $jsonArray)) {
foreach($jsonArray['Updates'] as $update) {
// We only interested in updated and deleted posts
if ($update['Action'] === 2) {
$postsToDelete[] = $update['Data']['Shortcode'];
} elseif ($update['Action'] === 1) {
$postsToUpdate[] = $update['Data']['Shortcode'];
}
}
}
// Get updated posts to sync
if (count($postsToUpdate) > 0) {
$url = getenv('PASSLE_ENDPOINT_SYNC_POSTS');
$jsonArray = $this->passleApi($url, [
'PostShortcode' => implode(',', $postsToUpdate),
'FeaturedImage' => [
'Width' => 1600,
'Height' => 900
]
]
);
} else {
$jsonArray = [
"TotalCount" => 0,
"PageNumber" => 1,
"PageSize" => 20,
"Posts" => []
];
}
// Update status of posts
foreach ($jsonArray['Posts'] as &$post) {
$post['Status'] = 'enabled';
}
unset($post);
// Append posts to delete (disable) to the end
if ( count($postsToDelete) > 0) {
// get matching entries (note: only enabled entries will be matched)
$entriesToDelete = \craft\elements\Entry::find()
->section('posts')
->shortcode($postsToDelete)
->all();
foreach ($entriesToDelete as $entry) {
$jsonArray['Posts'][] = [
"PostTitle" => $entry->title,
"PostSlug" => $entry->slug,
"Status" => 0,
"PostShortcode" => $entry->shortcode
];
}
$jsonArray['TotalCount'] += count($entriesToDelete);
}
// Output feed
$output = $this->asJson($jsonArray);
$output->setStatusCode(200);
return $output;
}
/**
* Make a Passle API request
*
* @param string $url
* @param array $query
* @return array
* @throws GuzzleException
* @throws \RuntimeException
*/
private function passleApi(string $url, array $query) : array
{
$client = new Client();
$response = $client->request('GET', $url, [
'headers' => [
'APIKey' => getenv('PASSLE_API_KEY')
],
'query' => $query
]);
// Get the status code of the response
$statusCode = $response->getStatusCode();
$jsonArray = [];
$error = null;
if ($statusCode === 200) {
try {
// Parse the JSON response
$data = $response->getBody();
$jsonArray = json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (\GuzzleHttp\Exception\GuzzleException $e) {
// Handle the Guzzle exception
$error = 'Guzzle error: ' . $e->getMessage();
} catch (\JsonException $e) {
// Handle the Json exception
$error = 'Json error: ' . $e->getMessage();
}
} else {
// Handle the status error codes from Passle
$error = match($statusCode) {
400 => "Passle error: The request parameters are incorrect",
403 => "Passle error: The API Key is incorrect",
500 => "Passle error: The server couldn't process the request",
default => "Passle error: Sorry, something went wrong",
};
}
if ($error !== null) {
throw new \RuntimeException($error);
}
return $jsonArray;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment