Skip to content

Commit

Permalink
[FEATURE] Enable basic HTTP caching and use Symfony's reverse proxy
Browse files Browse the repository at this point in the history
Related: #136
  • Loading branch information
eliashaeussler committed Mar 18, 2022
1 parent 31b924c commit 419e070
Show file tree
Hide file tree
Showing 12 changed files with 242 additions and 25 deletions.
1 change: 1 addition & 0 deletions config/packages/framework.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ framework:
when@prod:
framework:
error_controller: App\Controller\ErrorController
http_cache: true

when@test:
framework:
Expand Down
17 changes: 14 additions & 3 deletions src/Controller/AbstractBadgeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,25 @@ public function setBadgeProviderFactory(BadgeProviderFactory $badgeProviderFacto
$this->badgeProviderFactory = $badgeProviderFactory;
}

protected function getBadgeResponse(Badge $badge, string $provider = null): Response
{
protected function getBadgeResponse(
Badge $badge,
string $provider = null,
\DateTime $cacheExpirationDate = null,
): Response {
try {
$providerClass = $this->badgeProviderFactory->get($provider);
} catch (InvalidProviderException $e) {
throw new NotFoundHttpException($e->getMessage());
}

return $providerClass->createResponse($badge);
$response = $providerClass->createResponse($badge);

if (null !== $cacheExpirationDate) {
$response->setPublic();
$response->setExpires($cacheExpirationDate);
$response->headers->addCacheControlDirective('must-revalidate', true);
}

return $response;
}
}
10 changes: 7 additions & 3 deletions src/Controller/DownloadsBadgeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ public function __construct(

public function __invoke(Request $request, string $extension, string $provider = null): Response
{
$apiResponse = $this->apiService->getExtensionMetadata($extension);
$downloads = $apiResponse[0]['downloads']
$extensionMetadata = $this->apiService->getExtensionMetadata($extension);
$downloads = $extensionMetadata[0]['downloads']
?? throw new BadRequestHttpException('Invalid API response.');

return $this->getBadgeResponse(Badge::forDownloads($downloads), $provider);
return $this->getBadgeResponse(
Badge::forDownloads($downloads),
$provider,
$extensionMetadata->getExpiryDate(),
);
}
}
10 changes: 7 additions & 3 deletions src/Controller/ExtensionBadgeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ public function __construct(

public function __invoke(Request $request, string $extension, string $provider = null): Response
{
$apiResponse = $this->apiService->getExtensionMetadata($extension);
$extensionKey = $apiResponse[0]['key']
$extensionMetadata = $this->apiService->getExtensionMetadata($extension);
$extensionKey = $extensionMetadata[0]['key']
?? throw new BadRequestHttpException('Invalid API response.');

return $this->getBadgeResponse(Badge::forExtension($extensionKey), $provider);
return $this->getBadgeResponse(
Badge::forExtension($extensionKey),
$provider,
$extensionMetadata->getExpiryDate(),
);
}
}
10 changes: 7 additions & 3 deletions src/Controller/StabilityBadgeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ public function __construct(

public function __invoke(Request $request, string $extension, string $provider = null): Response
{
$apiResponse = $this->apiService->getExtensionMetadata($extension);
$stability = $apiResponse[0]['current_version']['state']
$extensionMetadata = $this->apiService->getExtensionMetadata($extension);
$stability = $extensionMetadata[0]['current_version']['state']
?? throw new BadRequestHttpException('Invalid API response.');

return $this->getBadgeResponse(Badge::forStability($stability), $provider);
return $this->getBadgeResponse(
Badge::forStability($stability),
$provider,
$extensionMetadata->getExpiryDate(),
);
}
}
10 changes: 7 additions & 3 deletions src/Controller/VersionBadgeController.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,14 @@ public function __construct(

public function __invoke(Request $request, string $extension, string $provider = null): Response
{
$apiResponse = $this->apiService->getExtensionMetadata($extension);
$version = $apiResponse[0]['current_version']['number']
$extensionMetadata = $this->apiService->getExtensionMetadata($extension);
$version = $extensionMetadata[0]['current_version']['number']
?? throw new BadRequestHttpException('Invalid API response.');

return $this->getBadgeResponse(Badge::forVersion($version), $provider);
return $this->getBadgeResponse(
Badge::forVersion($version),
$provider,
$extensionMetadata->getExpiryDate()
);
}
}
77 changes: 77 additions & 0 deletions src/Entity/Dto/ExtensionMetadata.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony project "eliashaeussler/typo3-badges".
*
* Copyright (C) 2022 Elias Häußler <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace App\Entity\Dto;

/**
* ExtensionMetadata.
*
* @author Elias Häußler <[email protected]>
* @license GPL-3.0-or-later
*
* @implements \ArrayAccess<int|string, mixed>
*/
final class ExtensionMetadata implements \ArrayAccess
{
public function __construct(
/**
* @var array<int|string, mixed>
*/
private array $metadata,
private ?\DateTime $expiryDate = null,
) {
}

/**
* @return array<int|string, mixed>
*/
public function getMetadata(): array
{
return $this->metadata;
}

public function getExpiryDate(): ?\DateTime
{
return $this->expiryDate;
}

public function offsetExists(mixed $offset): bool
{
return isset($this->metadata[$offset]);
}

public function offsetGet(mixed $offset): mixed
{
return $this->metadata[$offset] ?? null;
}

public function offsetSet(mixed $offset, mixed $value): void
{
$this->metadata[$offset] = $value;
}

public function offsetUnset(mixed $offset): void
{
unset($this->metadata[$offset]);
}
}
21 changes: 16 additions & 5 deletions src/Service/ApiService.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@

namespace App\Service;

use App\Entity\Dto\ExtensionMetadata;
use Symfony\Contracts\Cache\CacheInterface;
use Symfony\Contracts\Cache\ItemInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
Expand All @@ -42,17 +43,27 @@ public function __construct(
) {
}

/**
* @return array<int|string, mixed>
*/
public function getExtensionMetadata(string $extension): array
public function getExtensionMetadata(string $extension): ExtensionMetadata
{
$apiPath = $this->buildApiPath('/extension/{extension}', ['extension' => $extension]);

return $this->cache->get(
// Fetch extension metadata from cache or external API
$extensionMetadata = $this->cache->get(
$this->calculateCacheIdentifier('typo3_api.extension_metadata', ['apiPath' => $apiPath]),
fn (ItemInterface $item) => $this->sendRequestAndCacheResponse($apiPath, $item),
null,
$cacheMetadata,
);

// Define cache expiry date from cache metadata
if (isset($cacheMetadata[ItemInterface::METADATA_EXPIRY])) {
$timestamp = (int) $cacheMetadata[ItemInterface::METADATA_EXPIRY];
$expiryDate = \DateTime::createFromFormat('U', (string) $timestamp) ?: null;
} else {
$expiryDate = null;
}

return new ExtensionMetadata($extensionMetadata, $expiryDate);
}

/**
Expand Down
15 changes: 15 additions & 0 deletions tests/Controller/AbstractBadgeControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,19 @@ public function getBadgeResponseReturnsResponseForRequestedProvider(): void

self::assertEquals($expected, $this->subject->testGetBadgeResponse($badge, 'badgen'));
}

/**
* @test
*/
public function getBadgeResponseReturnsCachedResponse(): void
{
$badge = new Badge();
$cacheExpirationDate = new \DateTime();
$expected = $this->badgeProviderFactory->get('badgen')->createResponse($badge)
->setPublic()
->setExpires($cacheExpirationDate);
$expected->headers->addCacheControlDirective('must-revalidate', true);

self::assertEquals($expected, $this->subject->testGetBadgeResponse($badge, 'badgen', $cacheExpirationDate));
}
}
83 changes: 83 additions & 0 deletions tests/Entity/Dto/ExtensionMetadataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

/*
* This file is part of the Symfony project "eliashaeussler/typo3-badges".
*
* Copyright (C) 2022 Elias Häußler <[email protected]>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/

namespace App\Tests\Entity\Dto;

use App\Entity\Dto\ExtensionMetadata;
use PHPUnit\Framework\TestCase;

/**
* ExtensionMetadataTest.
*
* @author Elias Häußler <[email protected]>
* @license GPL-3.0-or-later
*/
final class ExtensionMetadataTest extends TestCase
{
private \DateTime $expiryDate;
private ExtensionMetadata $subject;

protected function setUp(): void
{
$this->expiryDate = new \DateTime();
$this->subject = new ExtensionMetadata(['foo' => 'baz'], $this->expiryDate);
}

/**
* @test
*/
public function getMetadataReturnsExtensionMetadata(): void
{
self::assertSame(['foo' => 'baz'], $this->subject->getMetadata());
}

/**
* @test
*/
public function getExpiryDateReturnsExpiryDate(): void
{
self::assertSame($this->expiryDate, $this->subject->getExpiryDate());
}

/**
* @test
*/
public function subjectCanBeAccessedAsArray(): void
{
// offsetExists()
self::assertTrue(isset($this->subject['foo']));
self::assertFalse(isset($this->subject['baz']));

// offsetGet()
self::assertSame('baz', $this->subject['foo']);
self::assertNull($this->subject['baz']);

// offsetSet()
$this->subject['baz'] = 'foo';
self::assertSame('foo', $this->subject['baz']);

// offsetUnset()
unset($this->subject['baz']);
self::assertFalse(isset($this->subject['baz']));
}
}
9 changes: 6 additions & 3 deletions tests/Fixtures/AbstractBadgeControllerTestClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,11 @@
*/
final class AbstractBadgeControllerTestClass extends AbstractBadgeController
{
public function testGetBadgeResponse(Badge $badge, string $provider = null): Response
{
return $this->getBadgeResponse($badge, $provider);
public function testGetBadgeResponse(
Badge $badge,
string $provider = null,
\DateTime $cacheExpirationDate = null,
): Response {
return $this->getBadgeResponse($badge, $provider, $cacheExpirationDate);
}
}
4 changes: 2 additions & 2 deletions tests/Service/ApiServiceTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public function getExtensionMetadataReturnsMetadataFromCache(): void

$this->cache->get($cacheIdentifier, fn () => ['foo' => 'baz']);

self::assertSame(['foo' => 'baz'], $this->apiService->getExtensionMetadata('foo'));
self::assertSame(['foo' => 'baz'], $this->apiService->getExtensionMetadata('foo')->getMetadata());
self::assertSame(0, $this->client->getRequestsCount());

$this->cache->delete($cacheIdentifier);
Expand All @@ -56,7 +56,7 @@ public function getExtensionMetadataReturnsMetadataFromApiAndStoresResponseInCac
{
$this->mockResponses[] = new MockResponse(json_encode(['foo' => 'baz'], JSON_THROW_ON_ERROR));

self::assertSame(['foo' => 'baz'], $this->apiService->getExtensionMetadata('foo'));
self::assertSame(['foo' => 'baz'], $this->apiService->getExtensionMetadata('foo')->getMetadata());
self::assertSame(1, $this->client->getRequestsCount());
self::assertSame(['foo' => 'baz'], $this->cache->get($this->getCacheIdentifier(), fn () => null));
}
Expand Down

0 comments on commit 419e070

Please sign in to comment.