<?php
namespace Can\RestBundle\EventListener;
use Can\RestBundle\CanRestBundle;
use Can\RestBundle\Preference\DeclarationCallbackInterface;
use Can\RestBundle\Preference\Preference;
use Can\RestBundle\Preference\DeclarationPolicy;
use Can\RestBundle\Preference\PreferenceConfiguration;
use Can\RestBundle\Preference\PreferenceEventDispatcher;
use Can\RestBundle\Preference\PreferenceList;
use Can\RestBundle\Util\Types;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ResponseEvent;
use DomainException;
use Throwable;
/**
* A listener which declares honoured (applied) client preferences as elements
* in the HTTP Preference-Applied response header.
*
* @see https://tools.ietf.org/html/rfc7240
*
* @group preference
*
* @package can/rest-bundle
* @author lechecacharro <lechecacharro@gmail.com>
*/
class PreferenceAppliedListener
{
/**
* @var PreferenceConfiguration
*/
protected $configuration;
/**
* @var PreferenceEventDispatcher
*/
protected $dispatcher;
/**
* PreferenceAppliedListener constructor.
*
* @param PreferenceConfiguration $configuration
* @param PreferenceEventDispatcher $dispatcher
*/
public function __construct(PreferenceConfiguration $configuration, PreferenceEventDispatcher $dispatcher)
{
$this->configuration = $configuration;
$this->dispatcher = $dispatcher;
}
/**
* @param ResponseEvent $event
*/
public function onResponse(ResponseEvent $event): void
{
// Skip processing if the component is not enabled
if (! $this->configuration->isEnabled()) {
return;
}
$request = $event->getRequest();
// Skip processing if not in the service zone
if (! $request->attributes->get(CanRestBundle::ATTR_SERVICE_ZONE)) {
return;
}
// Skip processing if no preferences to apply/applied
if (! $request->attributes->has(CanRestBundle::ATTR_PREFERENCES) ||
! $request->attributes->has(CanRestBundle::ATTR_PREFERENCES_APPLIED)) {
return;
}
$appliedPreferenceList = $request->attributes->get(CanRestBundle::ATTR_PREFERENCES_APPLIED);
if (! $appliedPreferenceList instanceof PreferenceList) {
throw new DomainException(sprintf('Expected a %s instance, but got %s', PreferenceList::class,
Types::getClassOrType($appliedPreferenceList)
));
}
$declaredPreferenceList = $this->declareAppliedPreferences($event->getResponse(), $appliedPreferenceList);
// Allow third parties to modify the list of preferences to declare
$event = $this->dispatcher->dispatchPreferencesDeclared($declaredPreferenceList);
$request->attributes->set(CanRestBundle::ATTR_PREFERENCES_DECLARED, $event->getPreferenceList());
}
/**
* Declares honoured (applied) preferences as elements of the HTTP
* Preference-Applied response header, according to the configured
* declaration policy.
*
* @see PreferenceConfiguration::getPolicyApplied()
*
* @param Response $response the HTTP response
* @param PreferenceList $appliedPreferenceList the applied preference list
*
* @return PreferenceList the declared preferences
*/
protected function declareAppliedPreferences(Response $response, PreferenceList $appliedPreferenceList): PreferenceList
{
$declarationPolicy = $this->configuration->getDeclarationPolicy();
switch ($declarationPolicy) {
case DeclarationPolicy::NONE:
$preferences = [];
break;
case DeclarationPolicy::WHITELISTED:
$preferences = $appliedPreferenceList->filter(function (Preference $preference) {
return $this->configuration->isDeclarationWhitelisted($preference);
});
break;
case DeclarationPolicy::BLACKLISTED:
$preferences = $appliedPreferenceList->filter(function (Preference $preference) {
return ! $this->configuration->isDeclarationBlacklisted($preference);
});
break;
case DeclarationPolicy::NECESSARY:
$preferences = $appliedPreferenceList->filter(function (Preference $preference) {
return $this->configuration->isDeclarationNecessary($preference);
});
break;
case DeclarationPolicy::CUSTOM:
$preferences = $this->getCustomPreferenceList($appliedPreferenceList);
break;
case DeclarationPolicy::ALL:
$preferences = $appliedPreferenceList->all();
break;
default:
throw new DomainException(sprintf('Unsupported preference applied policy: %d', $declarationPolicy));
}
$preferenceList = new PreferenceList(...$preferences);
// Skip if no preferences are to be declared
if ($preferenceList->isEmpty()) {
return $preferenceList;
}
// Else, declare the honoured (applied) preferences
$response->headers->set('Preference-Applied', implode(', ', array_map(function (Preference $preference) {
return $preference->toString(false);
}, $preferences)));
if ($this->configuration->isVariantCacheEnabled()) {
// If a server supports the optional application of a preference
// that might result in a variance to a cache's handling of a
// response entity, a Vary header field MUST be included in the
// response listing the Prefer header field regardless of whether
// the client actually used Prefer in the request.
$response->headers->set('Vary', 'Prefer');
} else {
// Alternatively, the server MAY include a Vary header with the
// special value "*" (...) Note, however, that use of the
// "Vary: *" header will make it impossible for a proxy to
// cache the response.
$response->headers->set('Vary', '*');
}
return $preferenceList;
}
/**
* Applies the custom declaration service to figure out which honoured
* (applied) preferences should be included as elements of the HTTP
* Preference-Applied response header.
*
* @param PreferenceList $appliedPreferenceList the applied preference list
*
* @return string[]
*/
protected function getCustomPreferenceList(PreferenceList $appliedPreferenceList): array
{
$declarationService = $this->configuration->getDeclarationService();
if (is_callable($declarationService)) {
try {
return $declarationService($appliedPreferenceList);
} catch (Throwable $t) {
throw new DomainException(sprintf(
'Failed to obtain the list of applied preferences to '.
'declare: %s',
$t->getMessage()
), $t->getCode(), $t);
}
}
if ($declarationService instanceof DeclarationCallbackInterface) {
try {
return $declarationService->getDeclarablePreferences($appliedPreferenceList);
} catch (Throwable $t) {
throw new DomainException(sprintf(
'Failed to obtain the list of applied preferences to '.
'declare: %s',
$t->getMessage()
), $t->getCode(), $t);
}
}
throw new DomainException(sprintf(
'Invalid custom declaration service: expected either a callable '.
'or a %s instance, but got %s',
DeclarationCallbackInterface::class,
Types::getClassOrType($declarationService)
));
}
}