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
3 changes: 2 additions & 1 deletion docs/Attributes/ACCEPTS.MD
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Accepts

Specify the accepted media type for the route.
Specify one or more accepted media type for the route.
Only requests with the corresponding `Content-Type` will match the attributed route which can either be applied to:
- `Class` - applies this accepted media type to all routes contained within, but can be overridden at route level
- `Method` - applies this accepted media type to the route associated to the method
Expand All @@ -16,6 +16,7 @@ Only requests with the corresponding `Content-Type` will match the attributed ro

- `Attribute::TARGET_CLASS`
- `Attribute::TARGET_METHOD`
- `Attribute::IS_REPEATABLE`

## Example

Expand Down
6 changes: 3 additions & 3 deletions src/Attributes/Accepts.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use willitscale\Streetlamp\Models\Controller;
use willitscale\Streetlamp\Models\Route;

#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
readonly class Accepts implements RouteContract
{
public function __construct(private string|MediaType $mediaType)
Expand All @@ -18,12 +18,12 @@ public function __construct(private string|MediaType $mediaType)

public function applyToController(Controller $controller): void
{
$controller->setAccepts($this->getMediaType());
$controller->addAccepts($this->getMediaType());
}

public function applyToRoute(Route $route): void
{
$route->setAccepts($this->getMediaType());
$route->addAccepts($this->getMediaType());
}

public function getMediaType(): string
Expand Down
3 changes: 2 additions & 1 deletion src/CacheRules/CacheRule.php
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public function getCacheTtl(): int

public function getKey(Route $route, array $args = []): string
{
return hash('sha384', $route->getPath() . "__" . $route->getMethod() . "__" . $route->getAccepts());
$accepts = is_array($route->getAccepts()) ? implode(',', $route->getAccepts()) : $route->getAccepts();
return hash('sha384', $route->getPath() . "__" . $route->getMethod() . "__" . $accepts);
}
}
2 changes: 1 addition & 1 deletion src/Enums/HttpMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace willitscale\Streetlamp\Enums;

enum HttpMethod: string
#[\Attribute] enum HttpMethod: string
{
case GET = 'GET';
case HEAD = 'HEAD';
Expand Down
17 changes: 13 additions & 4 deletions src/Models/Context.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ abstract class Context
public function __construct(
protected string $class,
protected string|null $path = null,
protected string|null $accepts = null,
protected array $accepts = [],
protected array $middleware = [],
protected array $attributes = []
) {
Expand Down Expand Up @@ -50,14 +50,23 @@ public function appendPath(string $path): self
return $this;
}

public function getAccepts(): ?string
public function getAccepts(): array
{
return $this->accepts;
}

public function setAccepts(string|MediaType $accepts): self
public function addAccepts(string|MediaType $accepts): self
{
$this->accepts = ($accepts instanceof MediaType) ? $accepts->value : $accepts;
$this->accepts [] = ($accepts instanceof MediaType) ? $accepts->value : $accepts;
return $this;
}

public function setAccepts(array $accepts): self
{
$this->accepts = array_map(
fn($accept) => ($accept instanceof MediaType) ? $accept->value : $accept,
$accepts
);
return $this;
}

Expand Down
4 changes: 2 additions & 2 deletions src/Models/Controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ class Controller extends Context
public function __construct(
string $class,
private string $namespace,
string|null $path = '',
string|null $accepts = null,
?string $path = '',
array $accepts = [],
private bool $isController = false,
array $middleware = [],
array $attributes = []
Expand Down
8 changes: 5 additions & 3 deletions src/Models/Route.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,9 @@ class Route extends Context
public function __construct(
string $class,
private string $function,
string|null $path = null,
?string $path = null,
private HttpMethod|null $method = null,
string|null $accepts = null,
array $accepts = [],
private array $parameters = [],
array $middleware = [],
array $attributes = [],
Expand Down Expand Up @@ -95,6 +95,8 @@ public function matchesRoute(ServerRequestInterface $request, array &$matches):

public function matchesContentType(ServerRequestInterface $request): bool
{
return !isset($this->accepts) || $request->getHeaderLine('Content-Type') === $this->accepts;
$requestedAccepts = explode(',', $request->getHeaderLine('Content-Type'));
$requestedAccepts = array_map('trim', $requestedAccepts);
return empty(array_diff($this->accepts, $requestedAccepts));
}
}
2 changes: 1 addition & 1 deletion src/Traits/BuildMethodRoutes.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ private function buildMethodRoutes(
$controller->getPath()
);

if ($controller->getAccepts()) {
if (!empty($controller->getAccepts())) {
$route->setAccepts($controller->getAccepts());
}

Expand Down
22 changes: 20 additions & 2 deletions tests/Attributes/AcceptsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function testProcessRouteAnnotationExtractsValidMediaTypeFromAcceptAnnota
$acceptsAnnotation = new Accepts($expected);
$route = new Route('Test', 'test');
$acceptsAnnotation->applyToRoute($route);
$this->assertEquals($expected, $route->getAccepts());
$this->assertEquals([$expected], $route->getAccepts());
}

#[Test]
Expand All @@ -32,7 +32,25 @@ public function testProcessControllerAnnotationExtractsValidMediaTypeFromAcceptA
$acceptsAnnotation = new Accepts($expected);
$controller = new Controller('Test', 'Test');
$acceptsAnnotation->applyToController($controller);
$this->assertEquals($expected, $controller->getAccepts());
$this->assertEquals([$expected], $controller->getAccepts());
}

#[Test]
public function itShouldAddMultipleAcceptsToRoute(): void
{
$acceptsAnnotation1 = new Accepts('text/event-stream');
$acceptsAnnotation2 = new Accepts('application/json');
$route = new Route('Test', 'test');

$acceptsAnnotation1->applyToRoute($route);
$acceptsAnnotation2->applyToRoute($route);

$this->assertEmpty(
array_diff(
['application/json', 'text/event-stream'],
$route->getAccepts()
)
);
}

public static function validAcceptAnnotations(): array
Expand Down
27 changes: 27 additions & 0 deletions tests/Controllers/MultiAcceptTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace willitscale\StreetlampTests\Controllers;

use PHPUnit\Framework\Attributes\Test;

class MultiAcceptTest extends ControllerTestCase
{
#[Test]
public function itShouldReturnPongForPingWithMultiAcceptHeaderRequest(): void
{
$router = $this->setupRouter(
'GET',
'/ping',
$this->getTestRoot(),
$this->getComposerTestFile(),
null,
['Content-Type' => 'application/json, text/event-stream']
);

$response = $router->route();
$this->assertEquals('pong', (string) $response->getBody());
$this->assertEquals(200, $response->getStatusCode());
}
}
31 changes: 31 additions & 0 deletions tests/TestApp/Controllers/MultiAcceptController.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

declare(strict_types=1);

namespace willitscale\StreetlampTests\TestApp\Controllers;

use Psr\Http\Message\ResponseInterface;
use willitscale\Streetlamp\Attributes\Accepts;
use willitscale\Streetlamp\Attributes\Controller\RouteController;
use willitscale\Streetlamp\Attributes\Path;
use willitscale\Streetlamp\Attributes\Route\Method;
use willitscale\Streetlamp\Builders\ResponseBuilder;
use willitscale\Streetlamp\Enums\HttpMethod;
use willitscale\Streetlamp\Enums\HttpStatusCode;
use willitscale\Streetlamp\Enums\MediaType;

#[RouteController]
class MultiAcceptController
{
#[Method(HttpMethod::GET)]
#[Path('/ping')]
#[Accepts(MediaType::TEXT_EVENT_STREAM)]
#[Accepts(MediaType::APPLICATION_JSON)]
public function ping(): ResponseInterface
{
return new ResponseBuilder()
->setData('pong')
->setHttpStatusCode(HttpStatusCode::HTTP_OK)
->build();
}
}