Filtering for seleted categories and subcategories in extbase TYPO3 frontend plugin - typo3

I am using TYPO3 8.7.
I have got an extension which allows to list events. In the list plugin settings the redactor selects the categories. He wants that all events which have the selected category or a subcategory of it assigned are show in the frontend.
I have been reading the documentation on docs.typo3.org and have been looking at the class CategoryCollection. As far as I understand, this class helps me to get the keywords for a certain record, but does not help to select records by a keyword.
I would like to filter also for subcategories because it makes the handling of the extension much easier. Let's imagine the following event categories:
course of studies
master's degree
bachaelor's degree
training
internal training
external training
The backend editor wants to have the choice to display either e.g. internal trainings or all trainings, without activating the parent categories on every event explicitly.
What is the correct way to filter for categories and its subcategories in an extbase repository to display a list of records in the frontend?
Do I have to implement the logic manually to filter for categories and subcategories?

My solution looks like this:
<?php
namespace Snowflake\Events\Domain\Repository;
/***************************************************************
* Copyright notice
*
* (c) 2018 snowflake productions gmbh <support#snowflake.ch>
* All rights reserved
*
* This script is part of the TYPO3 project. The TYPO3 project is
* free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* The GNU General Public License can be found at
* http://www.gnu.org/copyleft/gpl.html.
*
* This script is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* This copyright notice MUST APPEAR in all copies of the script!
***************************************************************/
use TYPO3\CMS\Core\Database\ConnectionPool;
use TYPO3\CMS\Core\Database\Query\QueryBuilder;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Extbase\Persistence\Repository;
class EventRepository extends Repository {
/**
* #param $categories
* #return array|\TYPO3\CMS\Extbase\Persistence\QueryResultInterface
* #throws \TYPO3\CMS\Extbase\Persistence\Exception\InvalidQueryException
*/
public function findByCategoryFilter($categories)
{
$categories = array_map('intval', explode(',', $categories));
$categories = $this->expandCategories($categories);
$uids = $this->getUidsByCategories($categories);
if (count($uids) == 0)
return array();
$query = $this->createQuery();
$query->matching(
$query->in('uid', $uids)
);
return $query->execute(true);
}
/**
* Return the categories and all subcategories (recursive)
*
* #param $categories
* #return array
*/
private function expandCategories($categories)
{
// get all categories from database
/** #var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
->select('uid', 'parent')
->from('sys_category');
$allCategoriesFromDatabase = $queryBuilder->execute()->fetchAll();
// index the categories by parent
$categoriesByParent = array();
foreach($allCategoriesFromDatabase as $categoryFromDatabase) {
$categoriesByParent[(int)$categoryFromDatabase['parent']][] = (int)$categoryFromDatabase['uid'];
}
// expand the categories to all subcategories
$categoriesToExpand = $categories;
$expandedCategories = $categories;
while(count($categoriesToExpand) > 0) {
$currentSubCategories = array();
foreach($categoriesToExpand as $category) {
foreach ($categoriesByParent[$category] as $subCategory) {
$currentSubCategories[] = $subCategory;
}
}
$categoriesToExpand = array_diff($currentSubCategories, $expandedCategories);
$expandedCategories = array_unique(array_merge($expandedCategories, $currentSubCategories));
}
return $expandedCategories;
}
/**
* This is a workaround because
*
* $query = $this->createQuery();
* $query->matching(
* $query->contains('category', $categories)
* );
* return $query->execute(true);
*
* generate a useless SQL query (equals instead of in, see first WHERE condition in subquery)
*
* SELECT `tx_events_domain_model_event`.*
* FROM `tx_events_domain_model_event` `tx_events_domain_model_event`
* WHERE (`tx_events_domain_model_event`.`uid` IN
* (SELECT `uid_foreign`
* FROM `sys_category_record_mm`
* WHERE (`uid_local` = '1,3,5,4,6')
* AND ((`sys_category_record_mm`.`tablenames` = 'tx_events_domain_model_event')
* AND (`sys_category_record_mm`.`fieldname` = 'category'))))
* AND (`tx_events_domain_model_event`.`sys_language_uid` IN (0, -1))
* AND (`tx_events_domain_model_event`.`pid` = 161)
* AND ((`tx_events_domain_model_event`.`deleted` = 0)
* AND (`tx_events_domain_model_event`.`hidden` = 0)
* AND (`tx_events_domain_model_event`.`starttime` <= 1516715340)
* AND (
* (`tx_events_domain_model_event`.`endtime` = 0)
* OR (`tx_events_domain_model_event`.`endtime` > 1516715340)))
*
* #param $categories
* #return array
*/
private function getUidsByCategories($categories) {
$result = array();
/** #var QueryBuilder $queryBuilder */
$queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
->getQueryBuilderForTable('sys_category_record_mm');
$queryBuilder->getRestrictions()->removeAll();
$queryBuilder
->select('uid_foreign')
->from('sys_category_record_mm')
->where(
$queryBuilder->expr()->andX(
$queryBuilder->expr()->in('uid_local', $categories),
$queryBuilder->expr()->eq('tablenames', '\'tx_events_domain_model_event\''),
$queryBuilder->expr()->eq('fieldname', '\'category\'')
)
);
$records = $queryBuilder->execute()->fetchAll();
foreach($records as $record) {
$result[] = $record['uid_foreign'];
}
return $result;
}
}

Related

Contentsliding is not stopping at sysfolder since TYPO3 v10 - How to solve?

The TSref entry for slide explains:
Up to Version 9 of TYPO3 the sliding stopped when reaching a folder.
Beginning with TYPO3 10 this is not longer the case. See
$cObj->checkPid_badDoktypeList.
Ok, this variable is still 255 (formerly directly, now via constant PageRepository::DOKTYPE_RECYCLER).
What exactly should I see there that will help me? Or better, how to get content sliding still working like before?
You have to extend the ContentObjectRenderer class and overwrite the getSlidePids method with your own extension.
In the boot function of ext_localconf.php:
$GLOBALS['TYPO3_CONF_VARS']['SYS']['Objects'][\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class] = [
'className' => \YourVendor\YourExtensionKey\ContentObject\ContentObjectRenderer::class
];
Then you have to create your own "Classes/ContentObject/ContentObjectRenderer.php" with:
<?php
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
namespace YourVendor\YourExtension\ContentObject;
use TYPO3\CMS\Core\Domain\Repository\PageRepository;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class ContentObjectRenderer extends \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
{
/**
* Returns all parents of the given PID (Page UID) list
*
* #param string $pidList A list of page Content-Element PIDs (Page UIDs) / stdWrap
* #param array $pidConf stdWrap array for the list
* #return string A list of PIDs
* #internal
*/
public function getSlidePids($pidList, $pidConf)
{
// todo: phpstan states that $pidConf always exists and is not nullable. At the moment, this is a false positive
// as null can be passed into this method via $pidConf. As soon as more strict types are used, this isset
// check must be replaced with a more appropriate check like empty or count.
$pidList = isset($pidConf) ? trim($this->stdWrap($pidList, $pidConf)) : trim($pidList);
if ($pidList === '') {
$pidList = 'this';
}
$tsfe = $this->getTypoScriptFrontendController();
$listArr = null;
if (trim($pidList)) {
$listArr = GeneralUtility::intExplode(',', str_replace('this', (string)$tsfe->contentPid, $pidList));
$listArr = $this->checkPidArray($listArr);
}
$pidList = [];
if (is_array($listArr) && !empty($listArr)) {
foreach ($listArr as $uid) {
$page = $tsfe->sys_page->getPage($uid);
if($page['doktype'] == PageRepository::DOKTYPE_SYSFOLDER)
break;
if (!$page['is_siteroot']) {
$pidList[] = $page['pid'];
}
}
}
return implode(',', $pidList);
}
}

How do I get uid of a File Reference Object in TYPO3?

I am trying to get a file through this code $f = $resourceFactory->getFileObject($uid); but the problem is the uid is a protected field in the file reference object, as seen below so I am not able to get the uid, and getUid() obviously wont work either.
So how can I get the uid of the file reference (FAL)
/**
* A file reference object (File Abstraction Layer)
*
* #api experimental! This class is experimental and subject to change!
*/
class FileReference extends
\TYPO3\CMS\Extbase\Domain\Model\AbstractFileFolder
{
/**
* Uid of the referenced sys_file. Needed for extbase to serialize the
* reference correctly.
*
* #var int
*/
protected $uidLocal;
/**
* #param \TYPO3\CMS\Core\Resource\ResourceInterface $originalResource
*/
public function setOriginalResource(\TYPO3\CMS\Core\Resource\ResourceInterface $originalResource)
{
$this->originalResource = $originalResource;
$this->uidLocal = (int)$originalResource->getOriginalFile()->getUid();
}
/**
* #return \TYPO3\CMS\Core\Resource\FileReference
*/
public function getOriginalResource()
{
if ($this->originalResource === null) {
$this->originalResource = \TYPO3\CMS\Core\Resource\ResourceFactory::getInstance()->getFileReferenceObject($this->getUid());
}
return $this->originalResource;
}
}
Given you have an instance of TYPO3\CMS\Extbase\Domain\Model\FileReference then you can use getOriginalResource() to get the wrapped TYPO3\CMS\Core\Resource\FileReference. If you need the referenced file, you can then use getOriginalFile(). Thus as a chained call:
$file = $fileReference->getOriginalResource()->getOriginalFile();
Notice that you don't have to use the ResourceFactory yourself in all of this, this is taken care of internally.
Work form me.
You can find or get file refernce uid using custom query.
In Controller :
$uid = $yourObject->getUid();
$fileReference = $this->yourRepository->getFileReferenceObject($uid);
In Repository
public function getFileRefernceHeaderLogo($uid){
$query = $this->createQuery();
$queryString = "SELECT *
FROM sys_file_reference
WHERE deleted = 0
AND hidden = 0
AND tablenames='your_table_name'
AND fieldname='your_field_name'
AND uid_foreign =".$uid;
$query->statement($queryString);
return $res = $query->execute(true);
}
In Controller
$fileRefUid = $fileReference[0]['uid'];
Here you can get uid of file reference table.It is long process.
You can also get sys_file table uid for getFileObject.like,
$sys_file_uid = $fileReference[0]['uid_local'];

TYPO3 8 show layout selection in backend preview for textmedia

I try to use and customize the CTypes of fluid_styled_content as much as possible. Therefore the select-field "Layout" is very useful to have a possibility to select some different styles (like red box, shadow, or image-stuff). But if you have some possibilities to select it is not shown in backend preview an every element is looking the same.
Is there a way to show the selected value the layout field in backend preview for textmedia?
To get this done register a hook (path: yourextension/Classes/Hooks/PageLayoutView/TextMediaCustomPreviewRenderer.php) like that:
<?php
namespace Vendor\Yourextension\Hooks\PageLayoutView;
/*
* This file is part of the TYPO3 CMS project.
*
* It is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License, either version 2
* of the License, or any later version.
*
* For the full copyright and license information, please read the
* LICENSE.txt file that was distributed with this source code.
*
* The TYPO3 project - inspiring people to share!
*/
use \TYPO3\CMS\Backend\View\PageLayoutViewDrawItemHookInterface;
use \TYPO3\CMS\Backend\View\PageLayoutView;
/**
* Contains a preview rendering for the page module of CType="textmedia"
*/
class TextMediaCustomPreviewRenderer implements PageLayoutViewDrawItemHookInterface
{
/**
* Preprocesses the preview rendering of a content element of type "Text Media"
*
* #param \TYPO3\CMS\Backend\View\PageLayoutView $parentObject Calling parent object
* #param bool $drawItem Whether to draw the item using the default functionality
* #param string $headerContent Header content
* #param string $itemContent Item content
* #param array $row Record row of tt_content
*
* #return void
*/
public function preProcess(
PageLayoutView &$parentObject,
&$drawItem,
&$headerContent,
&$itemContent,
array &$row
)
{
$pageTs = \TYPO3\CMS\Backend\Utility\BackendUtility::getPagesTSconfig($row['pid']);
if ($row['CType'] === 'textmedia') {
$itemContent .= '<p>Layoutversion: ' . $pageTs['TCEFORM.']['tt_content.']['layout.']['types.']['textmedia.']['addItems.'][$row['layout']] . '</p>';
if ($row['bodytext']) {
$itemContent .= $parentObject->linkEditContent(
$parentObject->renderText($row['bodytext']),
$row
) . '<br />';
}
if ($row['assets']) {
$itemContent .= $parentObject->thumbCode($row, 'tt_content', 'assets') . '<br />';
}
$drawItem = false;
}
}
}
And in your ext_localconf.php you put like that:
// Register for hook to show preview of tt_content element of CType="textmedia" in page module
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['cms/layout/class.tx_cms_layout.php']['tt_content_drawItem']['textmedia'] = \Vendor\Yourextension\Hooks\PageLayoutView\TextMediaCustomPreviewRenderer::class;
In my case I offer the different options of the select in pageTsconfig like that:
TCEFORM.tt_content.layout.types.textmedia.addItems {
50 = Textbox grau, Bildergalerie oben
60 = Textbox grau, Bildergalerie unten
110 = Blau, rechtsbündig
210 = Hellblau, linksbündig
220 = Rot, linksbündig
310 = Akkordeon
}
It is the better way to use correct language handling by locallang.xlf for that. If you do it like that maybe you have to change the code example a bit...
This was the result of a thread at "TYPO3 Fragen, Antworten, inoffizielle Gruppe" on Facebook. Thanks a lot to every helping hand specially to Wolfgang Klinger :-)

Doctrine Custom ID with auto increment

I want to define some id with prefix.
For example, for one order entity its : "OR17000001"
In this example, the prefix is "OR17"
So i have declare my id entities like this :
/**
* #var string
*
* #ORM\Column(name="id", type="string", length=8)
* #ORM\Id
* #ORM\GeneratedValue(strategy="CUSTOM")
* #ORM\CustomIdGenerator(class="My\Bundle\Generator\OrderCodeGenerator")
*/
private $id;
And my Generator is :
<?php
namespace My\Bundle\Generator;
use Doctrine\ORM\Id\AbstractIdGenerator;
use Doctrine\ORM\EntityManager;
use My\Bundle\Entity\Order;
class OrderCodeGenerator extends AbstractIdGenerator
{
/**
* Format :
* $prefix - string
* $year - take 2 first letters (17)
* $increment - Take the last code + 1
*
* #param EntityManager $em
* #param \Doctrine\ORM\Mapping\Entity $entity
* #return bool|string
*/
public function generate(EntityManager $em, $entity)
{
if ($entity instanceof Order) {
$now = new \DateTime();
$year = $now->format('y');
$prefix = 'OR';
$maxCode = $em->getRepository('MyRepo:Order')->findMaxCode($year, $prefix);
if ($maxCode) {
$increment = substr($maxCode[1], -4);
$increment = (int)$increment + 1;
} else
$increment = 0;
$code = $prefix . $year . sprintf('%04d', $increment);
return $code;
}
return false;
}
}
Without forget the method findMaxCode :
public function findMaxCode($year, $prefix)
{
$qb = $this->createQueryBuilder('entity');
$qb->where($qb->expr()->like('entity.id', ':code'))
->setParameter('code', '%' . $prefix . $year . '%');
$qb->select($qb->expr()->max('entity.id'));
return $qb->getQuery()->getOneOrNullResult();
}
That's work fine =)
My problem is when i try to add some entities in same time.
My case is :
Order entity with some items (its a form collection)
Item entity
So i need to custom id of Items Order with this strategy. And the problem is for found the max code. I have this error :
SQLSTATE[23000]: Integrity constraint violation: 1062 Duplicata du
champ 'IT170000001' pour la clef 'PRIMARY'
The generator can't found the max code for generate the second item, because there is no flush.
How can i save the increment value between 2 id generation before the flush ??
Solution :
I keep numeric id for my Item. Its useful for my Order entity, its more readable than an simple int. But i don't care for Item.
Thx to Honza Rydrych
Querying DB for last inserted ID and then inserting "+one" isn't reliable solution.
My solution for this case would be let doctrine generate ID's by the standard way and add the prefix "OR/Year/" when you need to present the data.
(Optionaly you can write custom Twig extension for presenting the ID http://symfony.com/doc/current/templating/twig_extension.html)

How do I get the "converted" action name for Zend Framework MVC?

I know update-profile-picture translates to the updateProfilePictureAction() function.
Where does the conversion of the action param take place?
I'd like to get updateProfilePicture as a value, I could write the function but it must already be in the library somewhere.
When using $this->_getParam('action') it returns update-profile-picture;
/**
* Auto call scripts
* #see Zend_Controller_Action::postDispatch()
*/
public function postDispatch(){
$action = $this->_getParam('action',false);
$method = $action.'Scripts';
if ($action && method_exists($this, $method))
$this->$method();
}
this works fine for indexAction - indexScripts but not for updateProfilePictureScripts (looking for update-profile-pictureScripts)
Get it with
$this->getFrontController()->getDispatcher()->formatActionName($this->_getParam('action',null));
It is happening in
/Zend/Controller/Dispatcher/Abstract.php
/**
* Formats a string into an action name. This is used to take a raw
* action name, such as one that would be stored inside a Zend_Controller_Request_Abstract
* object, and reformat into a proper method name that would be found
* inside a class extending Zend_Controller_Action.
*
* #param string $unformatted
* #return string
*/
public function formatActionName($unformatted)
{
$formatted = $this->_formatName($unformatted, true);
return strtolower(substr($formatted, 0, 1)) . substr($formatted, 1) . 'Action';
}
/**
* Formats a string from a URI into a PHP-friendly name.
*
* By default, replaces words separated by the word separator character(s)
* with camelCaps. If $isAction is false, it also preserves replaces words
* separated by the path separation character with an underscore, making
* the following word Title cased. All non-alphanumeric characters are
* removed.
*
* #param string $unformatted
* #param boolean $isAction Defaults to false
* #return string
*/
protected function _formatName($unformatted, $isAction = false)
{
// preserve directories
if (!$isAction) {
$segments = explode($this->getPathDelimiter(), $unformatted);
} else {
$segments = (array) $unformatted;
}
foreach ($segments as $key => $segment) {
$segment = str_replace($this->getWordDelimiter(), ' ', strtolower($segment));
$segment = preg_replace('/[^a-z0-9 ]/', '', $segment);
$segments[$key] = str_replace(' ', '', ucwords($segment));
}
return implode('_', $segments);
}
Have a look at Zend_Filter_Inflector : http://framework.zend.com/manual/en/zend.filter.inflector.html