Skip to content

Commit

Permalink
Merge pull request #44 from slub/36-detail-pages-bibliography
Browse files Browse the repository at this point in the history
36 detail pages bibliography
  • Loading branch information
dikastes authored Feb 26, 2025
2 parents 34c4ff3 + 9866a11 commit f7daa1b
Show file tree
Hide file tree
Showing 26 changed files with 737 additions and 54 deletions.
16 changes: 16 additions & 0 deletions Classes/Common/PageTitleProvider.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);

namespace Slub\LisztCommon\Common;


use TYPO3\CMS\Core\PageTitle\AbstractPageTitleProvider;


final class PageTitleProvider extends AbstractPageTitleProvider
{
public function setTitle(string $title): void
{
$this->title = $title;
}
}
197 changes: 161 additions & 36 deletions Classes/Controller/SearchController.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,73 +3,198 @@
declare(strict_types=1);

namespace Slub\LisztCommon\Controller;
use Illuminate\Support\Collection;
use Psr\Http\Message\ResponseInterface;
use Slub\LisztCommon\Interfaces\ElasticSearchServiceInterface;
use Slub\LisztCommon\Common\Paginator;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;

// ToDo:
// Organize the transfer of the necessary parameters (index name, fields, etc.) from the other extensions (ExtensionConfiguration?) -> see in ElasticSearchServic
// Elastic Search Index return standardized fields? Standardized search fields or own params from the respective extension?
// process search parameters from the URL query parameters to search
use TYPO3\CMS\Core\Log\LogManager;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use Elastic\Elasticsearch\Exception\ClientResponseException;
use Elastic\Transport\Exception\RuntimeException;
use TYPO3\CMS\Core\MetaTag\MetaTagManagerRegistry;
use Slub\LisztCommon\Common\PageTitleProvider;
use TYPO3\CMS\Core\Page\AssetCollector;
use TYPO3\CMS\Core\Cache\CacheManager;
use TYPO3\CMS\Core\Cache\Frontend\FrontendInterface;

final class SearchController extends ClientEnabledController
{
// set resultLimit as intern variable from $this->settings['resultLimit'];
protected int $resultLimit;
private FrontendInterface $runtimeCache;


// Dependency Injection of Repository
// https://docs.typo3.org/m/typo3/reference-coreapi/main/en-us/ApiOverview/DependencyInjection/Index.html#Dependency-Injection

public function __construct(
private readonly ElasticSearchServiceInterface $elasticSearchService,
protected ExtensionConfiguration $extConf
)
{
protected ExtensionConfiguration $extConf,
private readonly PageTitleProvider $titleProvider,
private readonly AssetCollector $assetCollector,
CacheManager $cacheManager,
) {
$this->resultLimit = $this->settings['resultLimit'] ?? 25;
$this->runtimeCache = $cacheManager->getCache('runtime');
}

public function indexAction(array $searchParams = []): ResponseInterface
{
$language = $this->request->getAttribute('language');
$locale = $language->getLocale();
if (
isset($searchParams['page']) &&
(int) $searchParams['page'] > 0
) {
$currentPage = (int) $searchParams['page'];
} else {
$currentPage = 1;
}

// $totalItems = $this->elasticSearchService->count($searchParams, $this->settings);
//$totalItems = 100;
$locale = $this->request->getAttribute('language')->getLocale();
$currentPage = $this->getCurrentPage($searchParams);
$this->addViewTransitionStyle();

$elasticResponse = $this->elasticSearchService->search($searchParams, $this->settings);
$paginator = (new Paginator())->
setPage($currentPage)->
setTotalItems($elasticResponse['hits']['total']['value'])->
setExtensionConfiguration($this->extConf);
$totalItems = 0;
if (isset($elasticResponse['hits'], $elasticResponse['hits']['total'], $elasticResponse['hits']['total']['value'])) {
$totalItems = (int)$elasticResponse['hits']['total']['value'];
}

$paginator = (new Paginator())
->setPage($currentPage)
->setTotalItems($totalItems)
->setExtensionConfiguration($this->extConf);
$pagination = $paginator->getPagination();
$showPagination = $paginator->getTotalPages() > 1 ? true : false;
$showPagination = $paginator->getTotalPages() > 1;

$this->view->assign('locale', $locale);
$this->view->assign('totalItems', $elasticResponse['hits']['total']['value']);
$this->view->assign('searchParams', $searchParams);
$this->view->assign('searchResults', $elasticResponse);
$this->view->assign('pagination', $pagination);
$this->view->assign('showPagination', $showPagination);
// $this->view->assign('totalItems', $totalItems);
$this->view->assign('currentString', Paginator::CURRENT_PAGE);
$this->view->assign('dots', Paginator::DOTS);
$this->view->assignMultiple([
'locale' => $locale,
'totalItems' => $totalItems,
'searchParams' => $searchParams,
'searchResults' => $elasticResponse,
'pagination' => $pagination,
'showPagination'=> $showPagination,
'currentString' => Paginator::CURRENT_PAGE,
'dots' => Paginator::DOTS,
'detailPageId' => $this->extConf->get('liszt_common', 'detailPageId'),
]);

return $this->htmlResponse();
}


public function searchBarAction(array $searchParams = []): ResponseInterface
{
$this->view->assign('searchParams', $searchParams);
return $this->htmlResponse();
}


public function detailsHeaderAction(array $searchParams = []): ResponseInterface
{
$documentId = $this->getDocumentIdFromRouting();
if (!$documentId) {
return $this->redirectToNotFoundPage();
}

try {
$elasticResponse = $this->loadDetailPageFromElastic($documentId);
} catch (ClientResponseException $e) {
if ($e->getCode() === 404) {
return $this->redirectToNotFoundPage();
}
throw $e;
}

$this->view->assign('searchResult', $elasticResponse);
return $this->htmlResponse();
}


public function detailsAction(array $searchParams = []): ResponseInterface
{
$documentId = $this->getDocumentIdFromRouting();
if (!$documentId) {
return $this->redirectToNotFoundPage();
}

try {
$elasticResponse = $this->loadDetailPageFromElastic($documentId);
} catch (ClientResponseException $e) {
// Handle 404 errors
if ($e->getCode() === 404) {
return $this->redirectToNotFoundPage();
}
throw $e; // Re-throw for other client errors

}

// set page title
$pageTitle = $this->formatPageTitle($elasticResponse['_source']['title'] ?? 'Details');
$this->titleProvider->setTitle($pageTitle);

// set meta description
$metaDescription = '';
if (isset($elasticResponse['_source'], $elasticResponse['_source']['tx_lisztcommon_searchable'])) {
$metaDescription = (string)$elasticResponse['_source']['tx_lisztcommon_searchable'];
}
$metaTagManager = GeneralUtility::makeInstance(MetaTagManagerRegistry::class)
->getManagerForProperty('description');
$metaTagManager->addProperty('description', $metaDescription);

$this->addViewTransitionStyle();

$this->view->assignMultiple([
'searchParams' => $searchParams,
'routingArgs' => $this->request->getAttribute('routing')->getArguments(),
'detailId' => $documentId,
'searchResult' => $elasticResponse,
'detailPageId' => $this->extConf->get('liszt_common', 'detailPageId'),
]);

return $this->htmlResponse();
}

private function getCurrentPage(array $params): int
{
return (isset($params['page']) && (int)$params['page'] > 0) ? (int)$params['page'] : 1;
}


// add view transition styles as inline style for animation
private function addViewTransitionStyle(): void
{
$this->assetCollector->addInlineStyleSheet(
'view-transitions-root',
'@media screen and (prefers-reduced-motion: no-preference) { @view-transition { navigation: auto; } }'
);
}

private function getDocumentIdFromRouting(): ?string
{
$routingArgs = $this->request->getAttribute('routing')->getArguments();
return $routingArgs['tx_lisztcommon_searchdetails']['documentId'] ?? null;
}

private function formatPageTitle(string $title): string
{
$maxTitleLength = 50;
return (mb_strlen($title) > $maxTitleLength)
? mb_substr($title, 0, $maxTitleLength - 3) . '...'
: $title;
}

private function loadDetailPageFromElastic(string $documentId): Collection
{
$cachedResult = $this->runtimeCache->get($documentId);
if (!$cachedResult instanceof Collection) {
$result = $this->elasticSearchService->getDocumentById($documentId, []);
if (!$result instanceof Collection) {
throw new \UnexpectedValueException("The return value of getDocumentById does not correspond to the expected collection type."); }
$this->runtimeCache->set($documentId, $result);
return $result;
}
return $cachedResult;
}



public function redirectToNotFoundPage(): ResponseInterface
{
// see: https://docs.typo3.org/m/typo3/reference-coreapi/12.4/en-us/ExtensionArchitecture/Extbase/Reference/Controller/ActionController.html
// $uri could also be https://example.com/any/uri
// $this->resourceFactory is injected as part of the `ActionController` inheritance
return $this->responseFactory->createResponse(301)
->withHeader('Location', '/404/'); // the uri of the 404 page from typo installation
}
}
25 changes: 25 additions & 0 deletions Classes/Event/Listener/SiteConfigurationLoadedEventListener.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

namespace Slub\LisztCommon\Event\Listener;

use TYPO3\CMS\Core\Configuration\Event\SiteConfigurationLoadedEvent;
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
use TYPO3\CMS\Core\Utility\ArrayUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;

/**
* Event to modify the site configuration array before loading the configuration
*/
final class SiteConfigurationLoadedEventListener
{

public function modify(SiteConfigurationLoadedEvent $event): void
{
$configuration = $event->getConfiguration();
$fileLoader = GeneralUtility::makeInstance(YamlFileLoader::class);
$routeEnhancersConfiguration = $fileLoader->load('EXT:liszt_common/Configuration/Routing/routeEnhancers.yaml');
ArrayUtility::mergeRecursiveWithOverrule($configuration, $routeEnhancersConfiguration);
$event->setConfiguration($configuration);
}

}
2 changes: 2 additions & 0 deletions Classes/Interfaces/ElasticSearchServiceInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public function getElasticInfo(): array;

public function search(array $searchParams, array $settings): Collection;

public function getDocumentById(string $documentId, array $settings): Collection;

// public function count(array $searchParams, array $settings): int;

}
27 changes: 27 additions & 0 deletions Classes/Routing/Aspect/DetailpageDocumentIdMapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace Slub\LisztCommon\Routing\Aspect;


use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;

class DetailpageDocumentIdMapper implements StaticMappableAspectInterface
{


public function generate(string $value): ?string
{
// check 8 Digits Zotero id, return not found if id not match
if (preg_match('/^[A-Z0-9]{8}$/', $value)) {
return $value;
}
return null;
}

public function resolve(string $value): ?string
{
return $this->generate($value);
}
}
15 changes: 12 additions & 3 deletions Classes/Services/ElasticSearchService.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,10 @@
namespace Slub\LisztCommon\Services;

use Elastic\Elasticsearch\Client;
use Elastic\Elasticsearch\Response\Elasticsearch;
use Http\Promise\Promise;
use Slub\LisztCommon\Common\Collection;
use Slub\LisztCommon\Common\ElasticClientBuilder;
use Slub\LisztCommon\Common\QueryParamsBuilder;
use Slub\LisztCommon\Interfaces\ElasticSearchServiceInterface;

use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;

Expand Down Expand Up @@ -63,4 +60,16 @@ public function search(array $searchParams, array $settings): Collection

return new Collection($response);
}

public function getDocumentById(string $documentId, array $settings): Collection
{
$this->init();
$params = [
'index' => $this->bibIndex,
'id' => $documentId,
];
// exceptions handled in controller
$response = $this->client->get($params)->asArray();
return new Collection($response);
}
}
23 changes: 23 additions & 0 deletions Classes/ViewHelpers/GetDetailBreadCrumbArrayViewHelper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);

namespace Slub\LisztCommon\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;

class GetDetailBreadCrumbArrayViewHelper extends AbstractViewHelper
{

public static function renderStatic(
array $arguments,
\Closure $renderChildrenClosure,
RenderingContextInterface $renderingContext)
: ?array
{

$request = $renderingContext->getRequest();$frontendController = $request->getAttribute('frontend.controller');
$rootline = $frontendController->rootLine;
return array_reverse($rootline);
}
}
16 changes: 16 additions & 0 deletions Configuration/Routing/routeEnhancers.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
routeEnhancers:
SearchDetailsRoute:
type: Extbase
limitToPages: [38]
extension: LisztCommon
plugin: SearchDetails
routes:
- routePath: '/{documentId}'
_controller: 'Search::details'
_arguments:
documentId: 'documentId'
defaultController: 'Search::index'
aspects:
documentId:
type: DetailpageDocumentIdMapper

Loading

0 comments on commit f7daa1b

Please sign in to comment.