TYPO3 v9.5 Extbase Error Handling with routeEnhancers - typo3

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.

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

RouteEnhancer for custom TYPO3 extension: 404 on detail page

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

Virtual properties in TYPO3 extbase domain models?

I'm trying to use a virtual domain model property in TYPO3 9.5.x that doesn't have a database field representation but I can't get it to work.
My model looks like this
class Project extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity {
/**
* participants
*
* #var string
*/
protected $participants;
...
/**
* Returns the participants
*
* #return string $participants
*/
public function getParticipants()
{
$this->participants = "foo";
return $this->participants;
}
}
I do see the property when I debug the model but it's always null as if it doesn't even recognise the getter method getParticipants().
Any idea what I might be doing wrong?
Already added a database field to ext_tables.sql and the TCA, but it didn't seem to make a difference.
The property is null because that's the state when the Extbase debugger inspects it. Notice that the Extbase debugger knows nothing about getters and also does not call them.
So if you want to initialize your property you must do this at the declaration time:
protected $participants = 'foo';
You can debug this property by simpy accessing it.
In Fluid, if you use <f:debug>{myModel}</f:debug>, you will see NULL for your property.
But if you directly use <f:debug>{myModel.participants}</f:debug>, you will see 'foo'.

TYPO3: get all FE users

I am trying to create a user management module. I would like to get all FE users.
This is my Controller:
/**
* #var \TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository
* #inject
*/
protected $feUserRepository;
then I use:
$users = $this->feUserRepository->findAll();
$this->view->assign('users', $users);
but all I get is an empty object.
EDIT:
for some reason
$this->feUserRepository->findByUId(1);
does work but findAll() not...
This is because extbase will silently disable the respectStoragePage setting on the querySettings for a findByUid($uid) call.
So, you have two options:
Provide the correct storage pid in the TypoScript configuration of your plugin (plugin.tx_myextension.persistence.storagePid). This way, you will find every frontenduser that is stored on the given page.
You could implement your own FrontendUserRepository that extends the repository from extbase but disables the respectStoragePage for all calls (this way you'll get every frontendUser regardless of the page the record is stored on). Here is how you do it:
use TYPO3\CMS\Extbase\Domain\Repository\FrontendUserRepository as ExtbaseFrontendUserRepository;
class FrontendUserRepository extends ExtbaseFrontendUserRepository
{
/**
* Disable respecting of a storage pid within queries globally.
*/
public function initializeObject()
{
$defaultQuerySettings = $this->objectManager->get(\TYPO3\CMS\Extbase\Persistence\Generic\Typo3QuerySettings:class);
$defaultQuerySettings->setRespectStoragePage(false);
$this->setDefaultQuerySettings($defaultQuerySettings);
}
}
In your Controller you then inject your FrontendUserRepository. Then you should do the same for the FrontendUser Model and tell extbase afterwards that you are using the fe_users table for your Model:
config.tx_extbase {
persistence {
classes {
Vendor\MyExtension\Domain\Model\FrontendUser {
mapping {
tableName = fe_users
}
}
}
}
}

ManyToOne with FOSUSerBundle ignoring exclusion policy

Building a JSON response for an API type thing, to retrieve a specific set of data that includes a ManyToOne relationship in the entity for my entity that extends FOSUSerBundle's User entity (called Account in my case).
The problem is, the Account entity thats included as a field in the response, is wanted, but I dont want to include all of the password and role type stuff.
I've been browing the internet for a couple hours now, and I've followed many guides on this, and I've cleared my cache every single time, and to no avail; So here's where I ended up:
// app/config/config.yml
jms_serializer:
metadata:
auto_detection: true
directories:
FOSUserBundle:
namespace_prefix: "FOS\\UserBundle"
path: "%kernel.root_dir%/Resources/serializer/FOS"
I've for below I've tried User.Model.yml and Model.User.yml and User.Entity.yml as well in a vain thought that the file name actually matters
// app/Resources/serializer/FOS/Entity.User.yml
FOS\UserBundle\Model\User:
exclusion_policy: ALL
properties:
id:
expose: true
and what I get still looks like this:
{
"status":"ok",
"api_version":"1.0",
"code":200,
"data":{
"video":{
"id":1,
"published":true,
"visibility":true,
"title":"Megaman 2",
"slug":"megaman-2",
"summary":"A rap song about Megaman",
"description":"A rap song\r\nAbout megaman",
"youtube_id":"R6L9bUouDr8",
"date_published":"2014-07-02T14:09:26-0700",
"date_created":"2014-07-02T14:09:26-0700",
"date_updated":"2014-07-02T14:09:26-0700",
"author_id":3,
"author":{
"id":3,
"username":"kharrison",
"username_canonical":"kharrison",
"email":"(sorry private)",
"email_canonical":"(sorry, private)",
"enabled":true,
"salt":"(sorry, private)",
"password":"(sorry, private)",
"last_login":"2014-07-04T15:17:34-0700",
"locked":false,
"expired":false,
"roles":[
"ROLE_SUPER_ADMIN"
],
"credentials_expired":false,
"display_name":"Kyle Harrison",
"slug":"kyle-harrison",
"bio":"Test"
}
}
}
}
The "author" field, is my Account entity thats being run through the JMSSerializer
I want to exclude ALL of that, except the user ID, Display name, and slug.
And finally this is how the API works:
// My/Bundle/Controller/BaseAPIController.php
//......... other code
/**
* #param string $status
* #param integer $code
* #return Response
*/
public function render_api($status, $code)
{
$this->apiResponse->setStatus($status);
$this->apiResponse->setCode($code);
return new Response($this->apiResponse->serialize($this->get('jms_serializer')), $this->apiResponse->getCode(), ["Content-type"=>"application/json"]);
}
//............. other code
and finally, that calls this:
// My/Bundle/Models
class APIResponse {
protected $status;
protected $apiVersion;
protected $code;
protected $data;
public function __construct($apiVersion, $status = "OK", $code = 500)
{
$this->status = $status;
$this->code = $code;
$this->apiVersion = $apiVersion;
$this->data = [];
}
// ... getters and setters
/**
* #return mixed
*/
public function serialize($serializer) {
return $serializer->serialize($this, "json");
}
}
I've for below I've tried User.Model.yml and Model.User.yml and
User.Entity.yml as well in a vain thought that the file name actually
matters.
It does matter, actually. It's a concatenation of the namespace and class name. In this case, you're trying to configure the FOS\UserBundle\Model\User class, so the file name should be Model.User.yml. (FOS\UserBundle\ should be excluded from the file name, since you configured it as namespace_prefix in your config.yml)
Also make sure that your Account class doesn't re-declare (overwrite) the properties, as the serializer config only works if you configure it for the class that actually declares the properties.
Ok So, the actual answer, couldn't have been arrived to via the information I provided. But Nic's Answer did lead me towards the solution. The description of how the the serializer looks at and deciphers the config file lead me to the real problem at hand.
This is what I failed to show:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User as BaseUser;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends BaseUser
{
The problem lays with the Alias I provided the FOS\UserBundle\Model\User namespace. I no longer remember why I wrote that that way. However, the moment I remove the Alias and rewrote the extends to resemble this instead:
<?php
namespace [PRIVATE]\[PRIVATE]Bundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use FOS\UserBundle\Model\User;
use JMS\Serializer\Annotation\ExclusionPolicy;
use JMS\Serializer\Annotation\Expose;
use JMS\Serializer\Annotation\Groups;
use JMS\Serializer\Annotation\VirtualProperty;
/**
* Account
*
* #ORM\Table()
* #ORM\Entity(repositoryClass="[PRIVATE]\[PRIVATE]Bundle\Entity\AccountRepository")
*/
class Account extends User
{
combined with the new correct filename from Nic's answer, the config based Exclusion policy for JMSSerializerBundle totally kicks in, and every instance of FOSUserBundle's items are now completely hidden, except for the fields I've now explicitly told it to expose.
This is exactly what I wanted :)
Thanks everyone for your help! Cheers
~k
I'm not sure it's the exact way you want it, more a way around:
way around 1: Select only the properties you want (via the entity manager) and then serialize the array obtained.
It's what I do with what I call my API (which is not a class as you but controllers)