<?php
namespace App;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class Blog implements LoggerAwareInterface
{
use LoggerAwareTrait;
private CacheInterface $cache;
private string $hubSpotKey;
private HttpClientInterface $httpClient;
private array $posts;
private array $authors;
private array $tags;
public function __construct(CacheInterface $cache, string $hubSpotKey, HttpClientInterface $httpClient)
{
$this->cache = $cache;
$this->hubSpotKey = $hubSpotKey;
$this->httpClient = $httpClient;
}
/**
* This busts the cached blog data and reloads everything.
* WARNING: This will be slow and should not be called
* unless intentional.
*/
public function bustCache(): void
{
$this->getAuthors(true);
$this->getTags(true);
$this->getAllPosts(true);
}
public function getAuthor(string $slug): ?array
{
foreach ($this->getAuthors() as $author) {
if ($author['slug'] == $slug) {
return $author;
}
}
return null;
}
public function getTag(string $slug): ?array
{
foreach ($this->getTags() as $tag) {
if ($tag['slug'] == $slug) {
return $tag;
}
}
return null;
}
public function getPosts(int $perPage = 3, int $page = 1, string $tag = '', string $author = ''): array
{
$offset = ($page - 1) * $perPage;
$posts = $this->getAllPosts();
if ($tag) {
$postsFiltered = [];
foreach ($posts as $post) {
foreach ($post['tags'] as $tag2) {
if ($tag2['slug'] == $tag) {
$postsFiltered[] = $post;
break;
}
}
}
$posts = $postsFiltered;
}
if ($author) {
$postsFiltered = [];
foreach ($posts as $post) {
if ($post['author']['slug'] == $author) {
$postsFiltered[] = $post;
}
}
$posts = $postsFiltered;
}
if ($perPage < 0) {
return $posts;
}
return array_slice($posts, $offset, $perPage);
}
public function hasMorePosts(int $perPage, int $page, ?string $tag = '', ?string $author = ''): bool
{
$posts = $this->getPosts(-1, 1, $tag, $author);
return $perPage * $page < count($posts);
}
public function getPost(string $id): ?array
{
$posts = $this->getAllPosts();
foreach ($posts as $post) {
if ($post['slug'] == $id) {
return $post;
}
}
return null;
}
public function getPrevious(string $id): ?array
{
$found = false;
foreach ($this->getAllPosts() as $post) {
if ($post['slug'] == $id) {
$found = true;
} elseif ($found) {
return $post;
}
}
return null;
}
public function getNext(string $id): ?array
{
$previous = null;
foreach ($this->getAllPosts() as $post) {
if ($post['slug'] == $id) {
return $previous;
}
$previous = $post;
}
return null;
}
public function getAuthors(bool $bustCache = false): array
{
if (!isset($this->authors)) {
$beta = $bustCache ? INF : null;
$this->authors = $this->cache->get('blog_authors', function (ItemInterface $item) {
$item->expiresAfter(86400); // cache for 1 day
return $this->loadBlogAuthors();
}, $beta);
}
return $this->authors;
}
private function getAuthorById(int $id): array
{
$authors = $this->getAuthors();
foreach ($authors as $author) {
if ($author['id'] == $id) {
return $author;
}
}
return $authors[0];
}
private function loadBlogAuthors(?string $after = null): array
{
try {
$response = $this->httpClient->request('GET', 'https://api.hubapi.com/cms/v3/blogs/authors', [
'query' => [
'hapikey' => $this->hubSpotKey,
'after' => $after,
],
]);
$result = json_decode($response->getContent(), true);
} catch (HttpExceptionInterface $e) {
$this->logger->error('Could not load blog posts from HubSpot', ['exception' => $e]);
throw $e;
}
$authors = $result['results'];
foreach ($authors as &$author) {
$author['slug'] = $this->slugify($author['name']);
}
// load all pages of data
if (isset($result['paging']['next'])) {
$authors = array_merge(
$result['results'],
$this->loadBlogAuthors($result['paging']['next']['after'])
);
}
return $authors;
}
public function getAllPosts(bool $bustCache = false): array
{
if (!isset($this->posts)) {
$beta = $bustCache ? INF : null;
$this->posts = $this->cache->get('blog_posts', function (ItemInterface $item) {
$item->expiresAfter(86400); // cache for 1 day
return $this->loadBlogPosts();
}, $beta);
}
return $this->posts;
}
private function loadBlogPosts(?string $after = null): array
{
try {
$response = $this->httpClient->request('GET', 'https://api.hubapi.com/cms/v3/blogs/posts', [
'query' => [
'hapikey' => $this->hubSpotKey,
'after' => $after,
],
]);
$result = json_decode($response->getContent(), true);
} catch (HttpExceptionInterface $e) {
$this->logger->error('Could not load blog posts from HubSpot', ['exception' => $e]);
throw $e;
}
$posts = [];
foreach ($result['results'] as $post) {
// Remove draft and archived posts
if ('PUBLISHED' != $post['currentState']) {
continue;
}
// determine the author
$post['author'] = $this->getAuthorById($post['blogAuthorId']);
// determine the primary tag
$post['tags'] = [];
foreach ($post['tagIds'] as $tagId) {
$post['tags'][] = $this->getTagById($tagId);
}
$post['tag'] = count($post['tags']) > 0 ? $post['tags'][0] : null;
// strip not allowed characters from slug
$post['slug'] = $this->slugify($post['slug']);
$posts[] = $post;
}
// load all pages of data
if (isset($result['paging']['next'])) {
$posts = array_merge(
$posts,
$this->loadBlogPosts($result['paging']['next']['after'])
);
}
// sort by date
usort($posts, [$this, 'sortPost']);
return $posts;
}
public function getTags(bool $bustCache = false): array
{
if (!isset($this->tags)) {
$beta = $bustCache ? INF : null;
$this->tags = $this->cache->get('blog_tags', function (ItemInterface $item) {
$item->expiresAfter(86400); // cache for 1 day
return $this->loadBlogTags();
}, $beta);
}
return $this->tags;
}
private function getTagById(int $id): ?array
{
foreach ($this->getTags() as $tagDetail) {
if ($tagDetail['id'] == $id) {
return $tagDetail;
}
}
return null;
}
private function loadBlogTags(?string $after = null): array
{
try {
$response = $this->httpClient->request('GET', 'https://api.hubapi.com/cms/v3/blogs/tags', [
'query' => [
'hapikey' => $this->hubSpotKey,
'after' => $after,
],
]);
$result = json_decode($response->getContent(), true);
} catch (HttpExceptionInterface $e) {
$this->logger->error('Could not load blog posts from HubSpot', ['exception' => $e]);
throw $e;
}
$tags = $result['results'];
foreach ($tags as &$tag) {
$tag['slug'] = $this->slugify($tag['name']);
}
// load all pages of data
if (isset($result['paging']['next'])) {
$tags = array_merge(
$tags,
$this->loadBlogTags($result['paging']['next']['after'])
);
}
// sort by name
usort($tags, [$this, 'sortTag']);
return $tags;
}
/**
* Sorts two blog posts in reverse date order.
*/
public function sortPost(array $a, array $b): int
{
return $b['publishDate'] <=> $a['publishDate'];
}
/**
* Sorts two blog tags in alphabetical order.
*/
public function sortTag(array $a, array $b): int
{
return $a['name'] <=> $b['name'];
}
private function slugify(string $input): string
{
$slug = strtolower(str_replace(' ', '-', $input));
return preg_replace('/[^0-9a-zA-Z\-]/', '', $slug);
}
}