RouteEnhancer for custom TYPO3 extension: 404 on detail page - typo3

I have a custom extbase extension that queries read-only data from a ReST service to show it in a list and detail view. There is no data in a TYPO3 table.
The URLs were processed by realURL, and this worked fine up until TYPO3 8.
Now that I'm in the process of updating to TYPO3 9.5.21 I can't get the routeEnhancer configuration for this extension to work. I managed to get the exact same URL for detail views on TYPO3 9, but the detail view returns a 404 error: "TYPO3\CMS\Core\Error\Http\PageNotFoundException: The requested page does not exist"
This is the config.yaml:
...
routeEnhancers:
...
News:
...
CDP_Gemeinden:
type: Extbase
# Pages containing list/detail-plugin
limitToPages:
- 43336
- 11082
# From registerPlugin # ext_tables.php
# Extension key is dvt_cdp
extension: DvtCdp
# From configurePlugin # ext_localconf.php
plugin: CdpGemeinden
routes:
- { routePath: '/gemeinde/{gemoestat}/', _controller: 'Gemeinden::show', _arguments: {'gemoestat': 'gemoestat'} }
defaultController: 'Gemeinden::search'
requirements:
gemoestat: '\d+'
aspects:
gemoestat:
type: StaticRangeMapper
start: '70100'
end: '70999'
On pages 43336 and 11082 lies the plugin that handles both the list view and detail view. "gemoestat" is the unique ID of the city. The links to the detail view are created in the list view template:
<f:link.action arguments="{gemoestat:gemeinde.gemoestat}" action="show">
This URL works on TYPO3 9 (and TYPO 8), without the routeEnhancer:
.../?tx_dvtcdp_cdpgemeinden%5Baction%5D=show&tx_dvtcdp_cdpgemeinden%5Bcontroller%5D=Gemeinden&tx_dvtcdp_cdpgemeinden%5Bgemoestat%5D=70701&cHash=8cabee37a20f804e94e2af1e9f2ce02d
This is the URL which worked on TYPO3 8, and is now generated if I activate my routeEnhancer while also leading to a 404 error:
.../gemeinden/gemeinde/70701/
Any idea what's missing? The detail view works fine without a routeEnhancer, so I don't think the extension is the problem, but rather the routeEnhancer config.

maybe your data sysfolder is out of the root page pagetree. See here: https://forge.typo3.org/issues/91235

With external data we need a own aspect to return the values. Try this
config.yaml
routeEnhancers:
CDPGemeinden:
type: Extbase
extension: DvtCdp
plugin: CdpGemeinden
routes:
- routePath: 'gemeinde/{gemoestat}/'
_controller: 'Gemeinden::show'
_arguments:
gemoestat: 'gemoestat'
aspects:
gemoestat:
type: GemoestatMapper
EXT:dvt_cdp/Classes/Routing/Aspect/GemoestatMapper.php
<?php
namespace Dvt\Cdp\Routing\Aspect;
use TYPO3\CMS\Core\Routing\Aspect\StaticMappableAspectInterface;
/**
* Aspect that maps external gemoestat unique ID
*/
class GemoestatMapper implements StaticMappableAspectInterface
{
/**
* #inheritDoc
*/
public function generate(string $value): ?string
{
return $value;
}
/**
* #inheritDoc
*/
public function resolve(string $value): ?string
{
return isset($value) ? (string)$value : null;
}
}
EXT:dvt_cdp/ext_localconf.php
// Add routing aspect
$GLOBALS['TYPO3_CONF_VARS']['SYS']['routing']['aspects']['GemoestatMapper'] =
\Dvt\Cdp\Routing\Aspect\GemoestatMapper::class;

Check values of extension and plugin in the routeenhancer settings again. Specially notation, uppercase, lowercase and compare with the registerPlugin arguments. Experience has shown that this is where most mistakes are made :) .. my mistakes

Related

TYPO3 v11 persistence.storagePid is ignored

I have an TYPO3 Extbase extension with TYPO3 11.
My problem is, that every item is shown.
In my plugin there is a Flexform Field for the storagePid. But the plugin completely ignored this settings and every time lists all items.
How can I tell my extension that it should only load items from the selected page?
I have tried to set this:
public function getItems()
{
$table = 'TABLE';
$query = $this->itemRepository->createQuery();
$query->getQuerySettings()->setRespectStoragePage(true);
return $query->execute();
}
But that doesn't help.
This doesn't change anything in the repository
public function initializeObject() {
/** #var Typo3QuerySettings $querySettings */
$querySettings = GeneralUtility::makeInstance(Typo3QuerySettings::class);
$querySettings->setRespectStoragePage(TRUE);
$this->setDefaultQuerySettings($querySettings);
}
I am assuming you mean what you set in the default field tt_content.pages and tt_content.recursive.
Here is an example how to get the page IDs from the fields: https://github.com/TYPO3/typo3/blob/11.5/typo3/sysext/felogin/Classes/Controller/AbstractLoginFormController.php#L35-L50
And then you would set that for your Extbase query:
$querySettings = $this->myRepository->createQuery()->getQuerySettings();
$querySettings->setStoragePageIds(...);
Background:
Here is a great write-up how naming a Flexform field persistence.storagePid would give you that, too https://www.derhansen.de/2016/02/how-extbase-determines-storagepid.html

TYPO3 v9.5 Extbase Error Handling with routeEnhancers

In my Extbase TYPO3 Extension I want to show a custom fluid template when the record is not available anymore (hidden or deleted). The error handling loads a fluid template where the path is defined in the setup.typoscript.
But when I add the routEnhancers in site config.yaml file for my Extension then the Error handling doesnt work anymore and it just shows the default TYPO3 Error Page: "The requested page does not exist".
In the docs of the site config I didn't find any way to set a special error handling for my Ext.
Here is the Code that handles it so far:
Controller:
class RecordController extends \TYPO3\CMS\Extbase\Mvc\Controller\ActionController
{
/**
* Error handling if no entry is found
*
* #param string $configuration configuration what will be done
* #throws \InvalidArgumentException
* #return string
*/
protected function handleNoRecordFoundError($configuration)
{
$statusCode = HttpUtility::HTTP_STATUS_404;
HttpUtility::setResponseCode($statusCode);
$this->getTypoScriptFrontendController()->set_no_cache('Record record not found');
$standaloneTemplate = GeneralUtility::makeInstance(StandaloneView::class);
$standaloneTemplate->setTemplatePathAndFilename(GeneralUtility::getFileAbsFileName($configuration));
return $standaloneTemplate->render();
}
/**
* #return TypoScriptFrontendController
*/
protected function getTypoScriptFrontendController()
{
return $GLOBALS['TSFE'];
}
/**
* action show
* #param \Digitalgizmo\Vehicles\Domain\Model\Vehicle $vehicle
* #return void
*
*
*/
public function showAction(\Vendor\MyExt\Domain\Model\Record $record = null)
{
if ($record !== null){
$this->view->assign('record', $record);
}
else {
$errorContent = $this->handleNoRecordFoundError($this->settings['show']['errorTemplate']);
if ($errorContent) {
return $errorContent;
}
}
}
}
config.yaml;
routeEnhancers:
MyExt:
type: Extbase
extension: MyExt
plugin: MyExt
routes:
-
routePath: '/staticName/{uid}/{slug}'
_controller: 'ControllerName::show'
_arguments:
slug: record
uid: id
defaultController: 'ControllerName::show'
aspects:
slug:
type: PersistedAliasMapper
tableName: tx_myext_domain_model_record
routeFieldName: slug
routeValuePrefix: /
uid:
type: PersistedAliasMapper
tableName: tx_myext_domain_model_record
routeFieldName: uid
The RoutEnhancer works just fine if the record is available.
How can I catch that error, so I can handle it and show my fluid template? My showAction isn't even being loaded (tested with XDebug). I assum this is because the TYPO3 core throws the error.
Everything seems fine with that code, the problem is that the RouteEnhancer is affected by the same constraints as your showAction: once the record is deleted, the resolve method in the routeEnhancer will no longer be able to find it nor its slug.
As a reference, see the resolve function in the API: https://api.typo3.org/9.5/_persisted_alias_mapper_8php_source.html . It instanciates a queryBuilder which, by default, builds a deleted=0 clause.
To get deleted redcords by their slug, what you need to do is build a custom RouteEnhancer, maybe by extending the PersistendAliasMapper class in a way that it also finds deleted records, refer https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Routing/ExtendingRouting.html , but be aware of the implications: the slug field in your model will no longer be able to find colliding slugs even with the eval=uniqueInSite option set because that, too, only sees non-deleted records.
Thanks to j4k3
I've created my own aspect type for the routeEnhancers, which removes the deleted and hidden constrains, so it won't throw an error.
I followed this documentation to create it: https://docs.typo3.org/m/typo3/reference-coreapi/master/en-us/ApiOverview/Routing/ExtendingRouting.html
Here is my CustomMapper Class.
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendGroupRestriction;
use TYPO3\CMS\Core\Database\Query\Restriction\FrontendRestrictionContainer;
use TYPO3\CMS\Core\Database\Query\Restriction\HiddenRestriction;
use TYPO3\CMS\Core\Routing\Aspect\PersistedAliasMapper;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class CustomMapper extends PersistedAliasMapper
{
protected function createQueryBuilder(): QueryBuilder
{
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable($this->tableName)
->from($this->tableName);
$queryBuilder->setRestrictions(
GeneralUtility::makeInstance(FrontendRestrictionContainer::class, $this->context)
);
// Frontend Groups are not available at this time (initialized via TSFE->determineId)
// So this must be excluded to allow access restricted records
$queryBuilder->getRestrictions()->removeByType(FrontendGroupRestriction::class);
$queryBuilder->getRestrictions()->removeByType(DeletedRestriction::class);
$queryBuilder->getRestrictions()->removeByType(HiddenRestriction::class);
return $queryBuilder;
}
}
Basically the only thing I added was the removal of the DeletedRestriction and HiddenRestriction.
Further more I had to change the Slug field how it gets built. I added the uid of the dataset to the slug and removed the separate uid GET Parameter, so now the slug is unique in the database. Before I did that I had the problem that the query found multiple of the same slug values and it always took the first one.
And now since the slug is unique in the database table it will return the object and wont throw an error, so I can handle the "error" in the controller.

TYPO3 Routing: Multiple Path Segments

I am trying to generate tree URLs for some categories.
The URLs should look like this: /category1/subcategory1/subcategory2.
The amount of path segments should be flexible since I don't know how deep the categories will be.
I got the first path segment working with the following config and a custom Mapper:
ProductsPlugin:
type: Extbase
limitToPages:
- 5
extension: MyExt
plugin: Products
routes:
-
routePath: '/{category_title}'
_controller: 'Products::list'
_arguments:
category_title: id
defaultController: 'Products::list'
requirements:
category_title: '[0-9]{1..6}'
aspects:
category_title:
type: ProductsValueMapper
The Mapper looks like this:
<?php
namespace Vendor\MyExt\Routing\Aspect;
use TYPO3\CMS\Core\Routing\Aspect\PersistedMappableAspectInterface;
use TYPO3\CMS\Core\Site\SiteLanguageAwareTrait;
class ProductsValueMapper implements PersistedMappableAspectInterface
{
use SiteLanguageAwareTrait;
/**
* #param string $value
*
* #return string|null
*/
public function generate(string $value): ?string
{
/**
* returns one or more path segments
* e.g. category1 or category1/subcategory1
*/
return $slug;
}
/**
* #param string $value
*
* #return string|null
*/
public function resolve(string $value): ?string
{
/**
* returns the id of the category from the last path segment
*/
return $id;
}
}
For just a single category it works fine. But as soon as a subcategory is requested the code breaks. The link to the subcategory only contains the path segment of the subcategory, e.g. /subcategory1, but not the one of the category.
It's clear to me why that is, since I only add the id of the subcategory to LinkViewHelper: <f:link.action controller="Products" action="list" arguments="{id: 5}">Test Link</f:link.action>. But I can
Which screws do I need to adjust to make the URLs work also for any depth of subcategories?
Thanks in advance!
You have to allow / as requirements. The default requirement is [^/]+ and you specified only a number.
This should work:
ProductsPlugin:
requirements:
category_title: .+
See also Symfony-Docs: https://symfony.com/doc/4.1/routing/slash_in_parameter.html

Typo3 Neos: How to insert a form into a template

Is it possible to insert a form build with TYPO3.Form into a template? I defined all needed fields in the .yaml file and would like to render this directly in the template and not insert it via the backend.
I'm not quite sure if i understand you correctly, but in your Typoscript configuration you can specifiy nodetypes which will be rendered on the page. I needed to render a form in a footer on all sites, here's my working configuration:
Root.ts2
footerData {
form = TYPO3.Neos.NodeTypes:Form {
formIdentifier = "minimal-contact-form"
}
}
Default.html
...
{footerData.form -> f:format.raw()}
...
minimal-contact-form.yaml
type: 'TYPO3.Form:Form'
identifier: minimal-contact-form
label: 'Minimal Contact form'
...

Redirect to different Actions in symfony2 having same url

I want create API which has same URL say http://stackoverflow.com/profile but having different request method like POST, PUT, GET, etc. I want to do different action based on method type. One way is write common action which for comparing methods and redirect using
$this->forward('anotherModule','anotherAction');
Is there any other way to check it's method and redirect to different actions without using one general action?
In your routing.yml you can define different controllers for the same pattern based on request method
read:
pattern: /yourUrl
defaults: { _controller: your.controller:readAction }
requirements:
_method: GET
write:
pattern: /yourUrl
defaults: { _controller: your.controller:writeAction }
requirements:
_method: POST
Or if you use annotations:
/**
* #Route("/yourRoute", requirements={"_method" = "GET"})
*/
public function showAction($id)
{
...
}
/**
* #Route("/yourRoute", requirements={"_method" = "POST"})
*/
public function writeAction($id)
{
...
}