diff --git a/CHANGELOG.md b/CHANGELOG.md index c3b9084af..576eeb863 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +* [PR-437](https://github.com/itk-dev/hoeringsportal/pull/437) + Cleaned up API and added caching * [PR-435](https://github.com/itk-dev/hoeringsportal/pull/435) Add usable config values for oidc Update OIDC documentation diff --git a/web/modules/custom/hoeringsportal_data/src/Controller/Api/ApiController.php b/web/modules/custom/hoeringsportal_data/src/Controller/Api/ApiController.php index 0ac1715b7..a16c212f8 100644 --- a/web/modules/custom/hoeringsportal_data/src/Controller/Api/ApiController.php +++ b/web/modules/custom/hoeringsportal_data/src/Controller/Api/ApiController.php @@ -2,11 +2,15 @@ namespace Drupal\hoeringsportal_data\Controller\Api; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheableJsonResponse; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Routing\RouteMatchInterface; use Drupal\hoeringsportal_data\Helper\GeoJsonHelper; use Symfony\Component\DependencyInjection\ContainerInterface; -use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\RequestStack; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Serializer\Serializer; /** @@ -14,34 +18,15 @@ */ abstract class ApiController extends ControllerBase { - /** - * The request stack. - * - * @var \Symfony\Component\HttpFoundation\RequestStack - */ - protected RequestStack $requestStack; - - /** - * Helper. - * - * @var \Drupal\hoeringsportal_data\Helper\GeoJsonHelper - */ - private GeoJsonHelper $geoJsonHelper; - - /** - * The serializer. - * - * @var \Symfony\Component\Serializer\Serializer - */ - protected Serializer $serializer; - /** * Constructor. */ - final public function __construct(RequestStack $requestStack, GeoJsonHelper $geoJsonHelper, Serializer $serializer) { - $this->requestStack = $requestStack; - $this->geoJsonHelper = $geoJsonHelper; - $this->serializer = $serializer; + final public function __construct( + protected readonly RequestStack $requestStack, + protected readonly RouteMatchInterface $routeMatch, + protected readonly GeoJsonHelper $geoJsonHelper, + protected readonly Serializer $serializer, + ) { } /** @@ -50,6 +35,7 @@ final public function __construct(RequestStack $requestStack, GeoJsonHelper $geo public static function create(ContainerInterface $container) { return new static( $container->get('request_stack'), + $container->get('current_route_match'), $container->get('hoeringsportal_data.geojson_helper'), $container->get('serializer') ); @@ -79,13 +65,49 @@ protected function generateUrl($name, $parameters = [], $options = []) { /** * Create a GeoJSON response. */ - protected function createGeoJsonResponse(array $features, string $type = 'FeatureCollection') { - $response = new JsonResponse([ - 'features' => $features, - 'type' => 'FeatureCollection', + protected function createGeoJsonResponse(array $features, string $type = 'FeatureCollection', ?array $cacheContexts = NULL, ?array $cacheTags = NULL): CacheableJsonResponse { + $response = new CacheableJsonResponse([ + 'features' => array_values( + array_filter( + $features, + static fn (array $item) => isset($item['geometry']) + ) + ), + 'type' => $type, ]); $response->headers->set('content-type', 'application/geo+json'); + if ($cacheContexts || $cacheTags) { + // @see https://www.drupal.org/docs/drupal-apis/cache-api/cache-tags#s-what + $response->addCacheableDependency( + (new CacheableMetadata()) + // @see https://www.drupal.org/docs/drupal-apis/cache-api/cache-contexts + ->setCacheContexts($cacheContexts ?? []) + ->setCacheMaxAge(Cache::PERMANENT) + ->setCacheTags($cacheTags ?? []) + ); + } + + return $response; + } + + /** + * Adds rels to response. + * + * @see https://datatracker.ietf.org/doc/html/rfc8288 + */ + protected function addRels(Response $response, int $page, bool $hasNext): Response { + $routeName = $this->routeMatch->getRouteName(); + $rels['self'] = $this->generateUrl($routeName, ['page' => $page]); + if ($page > 1) { + $rels['prev'] = $this->generateUrl($routeName, ['page' => $page - 1]); + } + if ($hasNext) { + $rels['next'] = $this->generateUrl($routeName, ['page' => $page + 1]); + } + $links = array_map(static fn (string $rel, string $url) => sprintf('<%s>; rel="%s"', $url, $rel), array_keys($rels), array_values($rels)); + $response->headers->add(['link' => implode(', ', $links)]); + return $response; } diff --git a/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/HearingController.php b/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/HearingController.php index e82faef16..8a2b6e844 100644 --- a/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/HearingController.php +++ b/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/HearingController.php @@ -29,11 +29,12 @@ public function index() { } $entities = $this->helper()->getHearings($conditions); + $features = array_map($this->helper()->serializeGeoJsonHearing(...), $entities); - $features = array_values(array_map([$this->helper(), 'serializeGeoJsonHearing'], $entities)); - $response = $this->createGeoJsonResponse($features, 'FeatureCollection'); - - return $response; + return $this->createGeoJsonResponse( + $features, + cacheTags: ['node_list:hearing'], + ); } } diff --git a/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/PublicMeetingController.php b/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/PublicMeetingController.php index bc080f5ca..2ab5e1a37 100644 --- a/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/PublicMeetingController.php +++ b/web/modules/custom/hoeringsportal_data/src/Controller/Api/GeoJSON/PublicMeetingController.php @@ -14,9 +14,12 @@ class PublicMeetingController extends ApiController { */ public function dates() { $entities = $this->getDates(); - $features = array_values(array_map([$this->helper(), 'serializeGeoJsonPublicMeetingDate'], $entities)); + $features = array_map($this->helper()->serializeGeoJsonPublicMeetingDate(...), $entities); - return $this->createGeoJsonResponse($features); + return $this->createGeoJsonResponse( + $features, + cacheTags: ['node_list:public_meeting'], + ); } /** diff --git a/web/modules/custom/hoeringsportal_data/src/Controller/Api/V2/GeoJSON/HearingController.php b/web/modules/custom/hoeringsportal_data/src/Controller/Api/V2/GeoJSON/HearingController.php index 962d62621..25c864c2d 100644 --- a/web/modules/custom/hoeringsportal_data/src/Controller/Api/V2/GeoJSON/HearingController.php +++ b/web/modules/custom/hoeringsportal_data/src/Controller/Api/V2/GeoJSON/HearingController.php @@ -31,24 +31,19 @@ public function index() { } // We get one more entity that actually needed to check if we have a "next" relation. - $entities = $this->helper()->getHearings($conditions, ['created' => 'DESC'], $pageSize+1, $pageSize *($page-1)); + $entities = $this->helper()->getHearings($conditions, ['created' => 'DESC'], $pageSize + 1, $pageSize * ($page - 1)); $hasNext = count($entities) > $pageSize; array_pop($entities); $entities = array_slice($entities, 0, $pageSize); - $features = array_values(array_map([$this->helper(), 'serializeGeoJsonHearing'], $entities)); - $response = $this->createGeoJsonResponse($features, 'FeatureCollection'); + $features = array_map($this->helper()->serializeGeoJsonHearing(...), $entities); + $response = $this->createGeoJsonResponse( + $features, + cacheContexts: ['url.query_args:geometry', 'url.query_args:page', 'url.query_args:page_size'], + cacheTags: ['node_list:hearing'], + ); - // @see https://datatracker.ietf.org/doc/html/rfc8288 - $rels['self'] = $this->generateUrl('hoeringsportal_data.api_geojson_v2_hearings', ['page' => $page]); - if ($page > 1) { - $rels['prev'] = $this->generateUrl('hoeringsportal_data.api_geojson_v2_hearings', ['page' => $page-1]); - } - if ($hasNext) { - $rels['next'] = $this->generateUrl('hoeringsportal_data.api_geojson_v2_hearings', ['page' => $page + 1]); - } - $links = array_map(function ($rel, $url) { return sprintf('<%s>; rel="%s"', $url, $rel); }, array_keys($rels), array_values($rels)); - $response->headers->add(['link' => implode(', ', $links)]); + $this->addRels($response, $page, $hasNext); return $response; } diff --git a/web/modules/custom/hoeringsportal_data/src/Helper/GeoJsonHelper.php b/web/modules/custom/hoeringsportal_data/src/Helper/GeoJsonHelper.php index 7ff301cff..3404159b8 100644 --- a/web/modules/custom/hoeringsportal_data/src/Helper/GeoJsonHelper.php +++ b/web/modules/custom/hoeringsportal_data/src/Helper/GeoJsonHelper.php @@ -159,8 +159,7 @@ public function serializeGeoJsonHearing(NodeInterface $hearing) { $geometryType = $this->getGeometryType($hearing); - $data = [ - 'properties' => [ + $properties = [ 'hearing_id' => (int) $hearing->id(), 'hearing_title' => $hearing->getTitle(), 'hearing_content_state' => $hearing->get('field_content_state')->value, @@ -189,11 +188,18 @@ public function serializeGeoJsonHearing(NodeInterface $hearing) { 'hearing_local_plan_ids' => array_map(function ($lokalplan) { return (int) $lokalplan->id; }, $lokalplaner), - ], ]; + // Additional properties for Septima widget. + $properties['start_date'] = $properties['hearing_start_date']; + $properties['end_date'] = $properties['hearing_reply_deadline']; + + $data = [ + 'properties' => $properties, + ]; + $geometry = $this->getGeometry($hearing); - if (NULL !== $geometry) { + if (isset($geometry['geometry'])) { $data['geometry'] = $geometry['geometry']; $data['type'] = 'Feature'; } @@ -244,6 +250,10 @@ public function serializeGeoJsonPublicMeetingDate(object $date) { 'date_spots' => (int) $data->spots, ]; + // Additional properties for Septima widget. + $properties['start_date'] = $properties['date_time_from']; + $properties['end_date'] = $properties['date_time_to']; + if (isset($data->data->coordinates)) { $serialized['geometry'] = [ 'type' => 'Point',