Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Creating a legacy Redia RSS feed for events. DDFHER-60 #1584

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
uuid: 014a5ed5-1c85-432f-a1b8-3814869178fd
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.media.redia_feed_large
- field.field.media.image.field_byline
- field.field.media.image.field_media_image
- image.style.redia_feed_large
- media.type.image
module:
- image
id: media.image.redia_feed_large
targetEntityType: media
bundle: image
mode: redia_feed_large
content:
field_media_image:
type: image
label: hidden
settings:
image_link: ''
image_style: redia_feed_large
image_loading:
attribute: lazy
third_party_settings: { }
weight: 0
region: content
hidden:
created: true
field_byline: true
langcode: true
name: true
search_api_excerpt: true
thumbnail: true
uid: true
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
uuid: 7cd49745-6747-4b76-b64a-f3e2a22a338e
langcode: en
status: true
dependencies:
config:
- core.entity_view_mode.media.redia_feed_small
- field.field.media.image.field_byline
- field.field.media.image.field_media_image
- image.style.redia_feed_small
- media.type.image
module:
- image
id: media.image.redia_feed_small
targetEntityType: media
bundle: image
mode: redia_feed_small
content:
field_media_image:
type: image
label: hidden
settings:
image_link: ''
image_style: redia_feed_small
image_loading:
attribute: lazy
third_party_settings: { }
weight: 0
region: content
hidden:
created: true
field_byline: true
langcode: true
name: true
search_api_excerpt: true
thumbnail: true
uid: true
11 changes: 11 additions & 0 deletions config/sync/core.entity_view_mode.media.redia_feed_large.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
uuid: 74eba980-7ec0-4b81-a6da-5c33830b08d9
langcode: en
status: true
dependencies:
module:
- media
id: media.redia_feed_large
label: 'Redia feed - large'
description: ''
targetEntityType: media
cache: true
11 changes: 11 additions & 0 deletions config/sync/core.entity_view_mode.media.redia_feed_small.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
uuid: fd0b39a0-f244-4d82-af84-54c63cf451bc
langcode: en
status: true
dependencies:
module:
- media
id: media.redia_feed_small
label: 'Redia feed - small'
description: ''
targetEntityType: media
cache: true
1 change: 1 addition & 0 deletions config/sync/core.extension.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ module:
dpl_react: 0
dpl_react_apps: 0
dpl_recommender: 0
dpl_redia_legacy: 0
rasben marked this conversation as resolved.
Show resolved Hide resolved
dpl_related_content: 0
dpl_reservations: 0
dpl_rest_base: 0
Expand Down
15 changes: 15 additions & 0 deletions config/sync/image.style.redia_feed_large.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
uuid: 923e70e6-ea34-446d-ae06-2f6ac0ac4f00
langcode: en
status: true
dependencies: { }
name: redia_feed_large
label: 'Redia feed large'
effects:
71f3eb2a-c6f8-4a85-a16f-651a6e43239e:
uuid: 71f3eb2a-c6f8-4a85-a16f-651a6e43239e
id: image_scale
weight: 1
data:
width: 2000
rasben marked this conversation as resolved.
Show resolved Hide resolved
height: null
upscale: false
15 changes: 15 additions & 0 deletions config/sync/image.style.redia_feed_small.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
uuid: 33f9623f-9eea-4b05-8e9c-f1d3ac224119
langcode: en
status: true
dependencies: { }
name: redia_feed_small
label: 'Redia feed small'
effects:
2cd37185-a9d4-43e1-ba81-66b73d18cae2:
uuid: 2cd37185-a9d4-43e1-ba81-66b73d18cae2
id: image_scale
weight: 1
data:
width: 220
height: null
upscale: true
9 changes: 9 additions & 0 deletions web/modules/custom/dpl_redia_legacy/dpl_redia_legacy.info.yml
rasben marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
name: "DPL Redia Legacy feeds"
type: module
description: "Various legacy feeds, used for the Redia App."
rasben marked this conversation as resolved.
Show resolved Hide resolved
package: DPL
core_version_requirement: ^10

dependencies:
- dpl_opening_hours:dpl_opening_hours
- drupal:drupal_typed
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
dpl_redia_legacy.events:
path: "/ding-redia-rss/event"
defaults:
_controller: '\Drupal\dpl_redia_legacy\Controller\RssFeeds\EventsController::getFeed'
_title: "Redia APP Event"
requirements:
_permission: "access content"
options:
_format: "xml"
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<?php

namespace Drupal\dpl_redia_legacy\Controller\RssFeeds;

use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\Field\FieldItemListInterface;
use Drupal\Core\File\FileUrlGeneratorInterface;
use Drupal\Core\Url;
use Drupal\file\FileInterface;
use Drupal\image\ImageStyleInterface;
use Drupal\media\MediaInterface;
use Drupal\recurring_events\Entity\EventInstance;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\Response;
use function Safe\filesize;
use function Safe\getimagesize;

/**
* Building a Redia-legacy RSS feed, showing eventinstances.
*/
class EventsController extends ControllerBase {

/**
* {@inheritdoc}
*/
public function __construct(
protected FileUrlGeneratorInterface $fileUrlGenerator,
protected RequestStack $requestStack,
rasben marked this conversation as resolved.
Show resolved Hide resolved
) {}

/**
* {@inheritdoc}
*/
public static function create(ContainerInterface $container): static {
return new static(
$container->get('file_url_generator'),
$container->get('request_stack'),

rasben marked this conversation as resolved.
Show resolved Hide resolved
);
}

/**
* Getting the RSS/XML feed of the items.
*/
public function getFeed(): Response {
$items = $this->getItems();

$rss_content = $this->buildRss($items);

$response = new Response();
$response->setContent($rss_content);
$response->headers->set('Content-Type', 'application/rss+xml');
return $response;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please update this to return a cacheable response.

I do not know how often the feed is requested but I do not see a reason why we should not be able to cache it in Varnish.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see it you have changes the implementation to return a cacheable response but please also add some cacheable dependencies to make it behave right 😄

}

/**
* Loading events, and turning it into a simple array of relevant values.
*
* @return array<mixed>
* An array of necessary item fields, used in buildRss().
*/
private function getItems(): array {

$storage = $this->entityTypeManager()->getStorage('eventinstance');
$query = $storage->getQuery()
->condition('status', TRUE)
->accessCheck(TRUE)
->sort('date.value');
rasben marked this conversation as resolved.
Show resolved Hide resolved

$ids = $query->execute();

$events = EventInstance::loadMultiple($ids);
rasben marked this conversation as resolved.
Show resolved Hide resolved

$items = [];

foreach ($events as $event) {
/** @var \Drupal\node\NodeInterface[] $branches */
$branches = $event->get('branch')->referencedEntities();
$branch = reset($branches);
rasben marked this conversation as resolved.
Show resolved Hide resolved
$event_dates = $event->get('date')->getValue();
rasben marked this conversation as resolved.
Show resolved Hide resolved
$changed_date = DrupalDateTime::createFromFormat('U', strval($event->getChangedTime()));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider moving this to the EventWrapper.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As I see it the code remains. Is that intentional?


$items[] = [
'title' => $event->label(),
'description' => $this->getEventDescription($event),
'author' => $event->getOwner()->get('field_author_name')->getString(),
'id' => $event->id(),
'date' => $changed_date->format('r'),
'promoted' => FALSE,
rasben marked this conversation as resolved.
Show resolved Hide resolved
'subtitle' => $event->get('event_description')->getString(),
rasben marked this conversation as resolved.
Show resolved Hide resolved
'start_time' => $event_dates[0]['value'] ?? NULL,
'end_time' => $event_dates[0]['end_value'] ?? NULL,
'media' => $this->getEventImageFields($event, 'redia_feed_large'),
'media_thumbnail' => $this->getEventImageFields($event, 'redia_feed_small'),
'branch' => [
'label' => $branch ? $branch->label() : NULL,
'id' => $branch ? $branch->id() : NULL,
],
];
rasben marked this conversation as resolved.
Show resolved Hide resolved
}

return $items;
}

/**
* Turning event image into fields that Redia understands.
*
* @return array<mixed>|null
* The fields that Redia understands (or nothing).
*/
private function getEventImageFields(EventInstance $event, string $image_style) {
$media_field = $event->get('event_image');

if (!($media_field instanceof FieldItemListInterface)) {
return NULL;
}

$media = $media_field->referencedEntities()[0] ?? NULL;
$file_field_name = 'field_media_image';

if (!($media instanceof MediaInterface) || !$media->hasField($file_field_name)) {
return NULL;
}

// @phpstan-ignore-next-line PHPStan does not know that entity is available.
$file = $media->get($file_field_name)->first()?->entity;

if (!($file instanceof FileInterface)) {
return NULL;
}

$file_uri = $file->getFileUri();
$style = $this->entityTypeManager()->getStorage('image_style')->load($image_style);

if (empty($file_uri) || !($style instanceof ImageStyleInterface)) {
return NULL;
}
rasben marked this conversation as resolved.
Show resolved Hide resolved

$image_url = $style->buildUrl($file_uri);
$image_sizes = getimagesize($file_uri);
$file_size = filesize($file_uri);

return [
// Generating a unique MD5.
'md5' => md5($image_url . $file_size),
'url' => $image_url,
'type' => $file->getMimeType(),
rasben marked this conversation as resolved.
Show resolved Hide resolved
'size' => filesize($file_uri),
'width' => $image_sizes[0] ?? NULL,
'height' => $image_sizes[1] ?? NULL,
];
}

/**
* Getting the first paragraph as text, to use as description.
*/
private function getEventDescription(EventInstance $event): ?string {
/** @var \Drupal\paragraphs\ParagraphInterface[] $paragraphs */
$paragraphs = $event->get('event_paragraphs')->referencedEntities();

foreach ($paragraphs as $paragraph) {
if ($paragraph->bundle() === 'text_body') {
return $paragraph->get('field_body')->getValue()[0]['value'] ?? NULL;
}
}

return NULL;
}

/**
* Building the actual RSS feed, from the items and site information.
*
* @param array<mixed> $items
* See $this->getItems();.
*/
private function buildRss(array $items): string {
$config = $this->config('system.site');
$site_title = $config->get('name');
$site_url = $this->requestStack->getCurrentRequest()?->getSchemeAndHttpHost();
$feed_url = Url::fromRoute('dpl_redia_legacy.events');
$feed_url->setAbsolute();
$feed_url = $feed_url->toString();
rasben marked this conversation as resolved.
Show resolved Hide resolved

$current_date = new DrupalDateTime();
$date = $current_date->format('r');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use the TimeService to get the current time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uh, why? The time service only gives me unix timestamp, that I'll need to parse as a date anyway?
Isnt it better just to use DrupalDateTime directly?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting the current time is a dependency which should be injected instead of accessed directly.


$rss_feed = '<?xml version="1.0" encoding="UTF-8"?>';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider replacing this approach to XML generation with e.g. SimpleXML.

To me the current singly based approach is frail. It is easy to introduce errors and they are hard to spot in the code.

I understand if this may be too late though.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is that the old RSS feed uses some "custom" XML, and using SimpleXML would just create a headache.

I've cleaned up the output though, using <<< so it's easier to read and maintain.

$rss_feed .= '<rss version="2.0" xml:base="' . $site_url . '" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:media="http://search.yahoo.com/mrss/" xmlns:content-rss="http://xml.redia.dk/rss">';
$rss_feed .= '<channel>';
$rss_feed .= "<title>$site_title</title>";
$rss_feed .= "<link>$site_url</link>";
$rss_feed .= '<atom:link rel="self" href="' . $feed_url . '" />';
$rss_feed .= '<language>da</language>';
$rss_feed .= "<pubDate>$date</pubDate>";
$rss_feed .= "<lastBuildDate>$date</lastBuildDate>";

foreach ($items as $item) {
$rss_feed .= '<item>';
$rss_feed .= "<title>{$item['title']}</title>";
$rss_feed .= "<description>{$item['description']}</description>";
$rss_feed .= "<author>{$item['author']}</author>";
$rss_feed .= "<guid isPermaLink=\"false\">{$item['id']}</guid>";
$rss_feed .= "<pubDate>{$item['date']}</pubDate>";
$rss_feed .= "<source url=\"$feed_url\">$site_title</source>";
$rss_feed .= "<media:content url=\"{$item['media']['url']}\" fileSize=\"{$item['media']['size']}\"
type=\"{$item['media']['type']}\" contentmedium=\"image\"
width=\"{$item['media']['width']}\" height=\"{$item['media']['height']}\">
<media:hash algo=\"md5\">{$item['media']['md5']}</media:hash>
</media:content>";

$rss_feed .= "<media:thumbnail url=\"{$item['media_thumbnail']['url']}\"
width=\"{$item['media']['width']}\" height=\"{$item['media']['height']}\" />";

$rss_feed .= "<content-rss:subheadline>{$item['subtitle']}</content-rss:subheadline>";
$rss_feed .= "<content-rss:arrangement-starttime>{$item['start_time']}</content-rss:arrangement-starttime>";
$rss_feed .= "<content-rss:arrangement-endtime>{$item['end_time']}</content-rss:arrangement-endtime>";
$rss_feed .= "<content-rss:arrangement-location>{$item['branch']['label']}</content-rss:arrangement-location>";
$rss_feed .= "<content-rss:library-id>{$item['branch']['id']}</content-rss:library-id>";

$promoted_title = $item['promoted'] ? 'Sandt' : 'Falsk';
$rss_feed .= "<content-rss:promoted>$promoted_title</content-rss:promoted>";

$rss_feed .= '</item>';
}

$rss_feed .= '</channel>';
$rss_feed .= '</rss>';

return $rss_feed;
}

}
Loading
Loading