Complex for/if conditions for rendering newsItems in Fluid Template - typo3

I am working on a special template for the news extension tx_news in Typo3.
I am completely new to Typo3 and especially Fluid.
What I want is an output of exactly 4 news items, but each of these items must have an image.
What I need is programming logic, something like:
If the newsItem has an image, and less than 4 items have been rendered so far, then render. Else don't do anything.
I read this question and answer:
TYPO3 Fluid complex if conditions
so I suspect I need something like a viewhelper.
So far my templates has this code to output items:
<f:for each="{news}" as="newsItem" iteration="iterator">
<f:if condition="{newsItem.falMedia}">
<f:if condition="{iterator.cycle}<=4">
<f:render partial="List/TeaserItem" arguments="{newsItem: newsItem,settings:settings,iterator:iterator, contentObjectData:contentObjectData}" />
</f:if>
</f:if>
</f:for>
But this will of course stop after iterating through news 4 times. So if one entry without image didn't get rendered, I will only have three items output.
I'd need an if condition kind of like this:
if ({newsItem.falMedia} && {iterator.cycle}<=4){
render image }
else {iterator.cycle--}
but I can't figure out how to pass the iterator variable of my for-loop to the new viewhelper, and especially to pass it back to the for-loop.

In short words this kind of logic isn't possible in Fluid - reason is simple -it's template engine.
You need to create your own extension and create a ViewHelper in it, which will take the collection of News will check if it has required settings (falMedia existing in this case) and will return limited array which you can iterate. Indeed, reusing f:for will be fastest solution.
I'm afraid, that's only way.
Here's the sample (compare it to original f:for viewhelper):
<?php
namespace TYPO3\CMS\Fluid\ViewHelpers;
class ForNewsWithMediaViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractViewHelper {
/**
* Iterates through elements of $each and renders child nodes
*
* #param array $each The array or \TYPO3\CMS\Extbase\Persistence\ObjectStorage to iterated over
* #param string $as The name of the iteration variable
* #param string $key The name of the variable to store the current array key
* #param boolean $reverse If enabled, the iterator will start with the last element and proceed reversely
* #param string $iteration The name of the variable to store iteration information (index, cycle, isFirst, isLast, isEven, isOdd)
* #param int $limit Limit of the news items to show
* #return string Rendered string
* #api
*/
public function render($each, $as, $key = '', $reverse = FALSE, $iteration = NULL, $limit = NULL) {
return self::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext, $limit);
}
/**
* #param array $arguments
* #param \Closure $renderChildrenClosure
* #param \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext
* #param int $limit Limit of the news items to show
* #return string
* #throws \TYPO3\CMS\Fluid\Core\ViewHelper\Exception
*/
static public function renderStatic(array $arguments, \Closure $renderChildrenClosure, \TYPO3\CMS\Fluid\Core\Rendering\RenderingContextInterface $renderingContext, $limit = NULL) {
$templateVariableContainer = $renderingContext->getTemplateVariableContainer();
if ($arguments['each'] === NULL) {
return '';
}
if (is_object($arguments['each']) && !$arguments['each'] instanceof \Traversable) {
throw new \TYPO3\CMS\Fluid\Core\ViewHelper\Exception('ForViewHelper only supports arrays and objects implementing \Traversable interface', 1248728393);
}
if ($arguments['reverse'] === TRUE) {
// array_reverse only supports arrays
if (is_object($arguments['each'])) {
$arguments['each'] = iterator_to_array($arguments['each']);
}
$arguments['each'] = array_reverse($arguments['each']);
}
$iterationData = array(
'index' => 0,
'cycle' => 1,
'total' => count($arguments['each'])
);
$limitCycle = 1;
$output = '';
/**
* #type $singleElement Tx_News_Domain_Model_News
*/
foreach ($arguments['each'] as $keyValue => $singleElement) {
if (is_null($singleElement->getFalMedia())
|| !is_null($limit) && $limitCycle > $limit
) {
continue;
}
$limitCycle++;
$templateVariableContainer->add($arguments['as'], $singleElement);
if ($arguments['key'] !== '') {
$templateVariableContainer->add($arguments['key'], $keyValue);
}
if ($arguments['iteration'] !== NULL) {
$iterationData['isFirst'] = $iterationData['cycle'] === 1;
$iterationData['isLast'] = $iterationData['cycle'] === $iterationData['total'];
$iterationData['isEven'] = $iterationData['cycle'] % 2 === 0;
$iterationData['isOdd'] = !$iterationData['isEven'];
$templateVariableContainer->add($arguments['iteration'], $iterationData);
$iterationData['index']++;
$iterationData['cycle']++;
}
$output .= $renderChildrenClosure();
$templateVariableContainer->remove($arguments['as']);
if ($arguments['key'] !== '') {
$templateVariableContainer->remove($arguments['key']);
}
if ($arguments['iteration'] !== NULL) {
$templateVariableContainer->remove($arguments['iteration']);
}
}
return $output;
}
}
So you can use it in your view as:
<f:forNewsWithMedia each="{news}" as="newsItem" iteration="iterator" limit="4">
<f:render partial="List/TeaserItem" arguments="{newsItem: newsItem,settings:settings,iterator:iterator, contentObjectData:contentObjectData}" />
</f:forNewsWithMedia>

Related

LinkHandler hook on TYPO3 8

Is there any hook to handle links with the new LinkHandler for TYPO3 8.7?
On the old LinkHandler extension is possible to define a hook to process the links as we want.
I need to overwrite the parameter of typolink based on some rules. Is there a way to do this on my extension?
There are multiple points to hook into.
TypoLink post-processing
You can hook into the TypoLink post-processing to modify the typolink itself before it gets rendered.
For this, you first register your custom class in ext_tables/ext_localconf:
$GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['tslib/class.tslib_content.php']['typoLink_PostProc'][] = 'Vendor\\Name\\Service\\TypoLinkProcessingService->postProcessTypoLink';
Then, inside your TypoLinkProcessingService class (with your name of choice, of course), you'd handle it inside your own method. For visualisation purposes, in this example, I'm altering an URL if it is a link to a youtube video in order to turn off "related videos" at the end, and to modify the color used by the controls inside youtube's player.
public function postProcessTypoLink(&$parameters, ContentObjectRenderer &$parentObject)
{
if (isset($parameters['finalTagParts']['url'])) {
$urlParts = parse_url($parameters['finalTagParts']['url']);
if (stristr($urlParts['host'], 'youtube.com') !== false && stristr($urlParts['path'], 'watch') !== false) {
$parameters['finalTag'] = str_replace(
'"' . htmlspecialchars($parameters['finalTagParts']['url']) . '"',
'"' . htmlspecialchars($parameters['finalTagParts']['url'] . '&rel=0&color=ffffff') . '"',
$parameters['finalTag']
);
}
}
}
TypoLink UserFunc
Another option is to utilise userFunc and adapt links.
For this, you configure your linkhandler configuration (PageTS) to provide userFunc inside typolink. Add TypoScript as needed to later fetch the configured data.
config.recordLinks {
tx_myest {
typolink {
userFunc = Vendor\Name\UserFunc\TypolinkUserFunc->parseLinkHandlerTypolink
userFunc {
newsUid = TEXT
newsUid.data = field:uid
newsClass = TEXT
newsClass.data = parameters:class
defaultDetailPid = 53
}
}
}
}
Inside your parseLinkHandlerTypolink method, you can access configured properties and adapt as required:
class TypolinkUserFunc
{
/**
* #var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer
* #inject
*/
public $cObj;
/**
* Add a method description here
*
* #param array $content
* #param array $configuration
* #return string
*/
public function parseNewsLinkHandlerTypolink(array $content, array $configuration)
{
if (!$configuration['newsUid']) {
return;
}
$params = $this->cObj->cObjGetSingle($configuration['newsClass'], $configuration['newsClass.']);
$newsUid = (int)$this->cObj->cObjGetSingle($configuration['newsUid'], $configuration['newsUid.']);
// ... your code goes here ...
$url = $this->cObj->typolink('', $typolink);
return '<a href="' . $url . '" ' . $attributes . '>';
}
}
Alternatively, this hook that has been introduced in 8.6 may also help you: https://docs.typo3.org/typo3cms/extensions/core/Changelog/8.6/Feature-79121-ImplementHookInTypolinkForModificationOfPageParams.html

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)

Fluidtypo3: Use custom field as title of FCE

I don't want to use the default header in my FCE's, but only custom flux fields. In the backend list views my FCE's are shown as "[no title]" because the default header is not filled. This leads to much confusion for editors.
How can I define one of my custom flux fields to be used as title for the FCE in TYPO3 Backend list views etc.?
You can't just use a field from the flexform, because all fields from the FCE are stored in the same field in the database (pi_flexform).
What you can do is to render the content element title with a user function. It is registered with a line like this in the TCA config:
$GLOBALS['TCA']['tt_content']['ctrl']['label_userFunc'] = 'Vendor\\Extkey\\Utility\\ContentElementLabelRenderer->getContentElementTitle';
The user function itself could look like this:
<?php
namespace Vendor\Extkey\Utility;
/**
* This class renders a human readable title for FCEs,
* so one is able to find a content element by its headline.
*/
class ContentElementLabelRenderer implements \TYPO3\CMS\Core\SingletonInterface {
/**
* #var \TYPO3\CMS\Extbase\Service\FlexFormService
* #inject
*/
protected $flexFormService = null;
/**
* Returns the content element title for a given content element
*/
public function getContentElementTitle(&$params) {
if (null === $this->flexFormService) {
$this->flexFormService = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Extbase\\Service\\FlexFormService');
}
if (ctype_digit($params['row']['uid']) && 'fluidcontent_content' === $params['row']['CType']) {
// If this is a FCE, parse the flexform and template name and generate the
// title in a template specific way.
$row = $params['row'];
$additionalRowData = $GLOBALS['TYPO3_DB']->exec_SELECTgetSingleRow('pi_flexform, tx_fed_fcefile', 'tt_content', 'uid = ' . $row['uid']);
$flexFormContent = $this->flexFormService->convertFlexFormContentToArray($additionalRowData['pi_flexform']);
$lastColonPosition = strrpos($additionalRowData['tx_fed_fcefile'], ':');
$contentElementType = (FALSE === $lastColonPosition) ? 'invalidtype' : substr($additionalRowData['tx_fed_fcefile'], $lastColonPosition + 1);
switch ($contentElementType) {
case 'Image.html':
$params['title'] = 'Image: "' . ($flexFormContent['title'] ?: $flexFormContent['subtitle']) . '"';
break;
default:
$params['title'] = 'Unknown content element type';
break;
}
}
else {
// If this is not a FCEm, print out "normal"
// title. Not the real thing, but comes pretty close, hopefully.
$params['title'] = $params['row']['header'] ?: ($params['row']['subheader'] ?: $params['row']['bodytext']);
}
}
}
This produces a maintainance problem though: Every time you add or change a content element, you have to update this file.

Prestashop display all products form category without pagination

I am building a Module for the category page in prestashop.
Basically in my module.php I have this code:
$category = new Category(Context::getContext()->shop->getCategory(),(int)Context::getContext()->language->id);
$nb = (int)(Configuration::get('MOD_NBR'));
$products = $category->getProducts((int)Context::getContext()->language->id, 1, ($nb ? $nb : 10));
$this->smarty->assign(array(
'myproducts' => $products,
'add_prod_display' => Configuration::get('PS_ATTRIBUTE_CATEGORY_DISPLAY'),
'homeSize' => Image::getSize(ImageType::getFormatedName('home')),
));
Then in mymodule.tpl I have this:
{foreach from=$products item=product name=myproducts}
+ other stuff
The problem is that I need to get all the products inside the category, but it is only displaying the products on the first page. I can't delete or modify pagination completely, because I need the other products on the category page to be paginated, but in my module I want to get all the products at once (after I will filter them to show only some of them).
As you may see I am kind of lost, but also so desperate, I will appreciate any guidance :)
thanks
In your code you have:
$products = $category->getProducts((int)Context::getContext()->language->id, 1, ($nb ? $nb : 10));
which corresponds to:
/**
* Return current category products
*
* #param integer $id_lang Language ID
* #param integer $p Page number
* #param integer $n Number of products per page
* #param boolean $get_total return the number of results instead of the results themself
* #param boolean $active return only active products
* #param boolean $random active a random filter for returned products
* #param int $random_number_products number of products to return if random is activated
* #param boolean $check_access set to false to return all products (even if customer hasn't access)
* #return mixed Products or number of products
*/
public function getProducts($id_lang, $p, $n, $order_by = null, $order_way = null, $get_total = false, $active = true, $random = false, $random_number_products = 1, $check_access = true, Context $context = null)
So you are asking for page 1 and $nb or 10 elements.
Try adding before that line $nb = 10000; to show up to 10k products (and feel free to increase it if your category has more than 10k products)
So it should be something like:
$category = new Category(Context::getContext()->shop->getCategory(),(int)Context::getContext()->language->id);
$nb = 10000;
$products = $category->getProducts((int)Context::getContext()->language->id, 1, ($nb ? $nb : 10));
$this->smarty->assign(array(
'myproducts' => $products,
'add_prod_display' => Configuration::get('PS_ATTRIBUTE_CATEGORY_DISPLAY'),
'homeSize' => Image::getSize(ImageType::getFormatedName('home')),
));
UPDATE: Reviewing your question I've found that in your template you are iterating $products variable, but assigning it as myproducts. I'm guessing smarty has the assigned variables $products with only the first page and $myproducts with the ones you have obtained.
Try updating your template to:
{foreach from=$myproducts item=product name=myproducts}

How can I set the order of Zend Form Elements and avoid duplicates

In Zend Form, if two elements have the same order, then Zend will totally ignores the second element (instead of displaying it under the first). Take the following code as an example. Notice that the City and Zip Code elements have the same order of 4
$address = new Zend_Form_Element_Textarea('address');
$address->setLabel('Address')
->setAttrib('cols', 20)
->setAttrib('rows', 2)
->setOrder(3)
;
$city = new Zend_Form_Element_Text('city');
$city->setLabel('City')
->setOrder(4)
;
$postal = new Zend_Form_Element_Text('postal');
$postal->setLabel('Zip Code')
->setOrder(4);
When this form renders, the Zip Code element is nowhere to be found.
If I want to set elements like a buttons dynamically, but tell it to render at the end of the form, how would I do this and not run into the problem of having two elements with the same order?
public function addSubmitButton($label = "Submit", $order = null)
{
$form_name = $this->getName();
// Convert Label to a lowercase no spaces handle
$handle = strtolower(str_replace(" ","_",$label));
$submit = new Zend_Form_Element_Submit($handle);
$submit->setLabel($label)
->setAttrib('id', $form_name . "_" . $handle)
;
///////// Set the button order to be at the end of the form /////////
$submit->setOrder(??????);
$this->addElement($submit);
}
If you really need to use the setOrder() method, I'd work with order numbers 10, 20, 30, 40, ... This way it will be easy to add elements in between already set Elements.
Furthermore, in order to avoid using order-numbers twice, you could use an array, where you store all the numbers from 1 to X. Whenever you set an order number, you set it via a method called getOrderNumberFromArray() which returns the next higher or lower order number still available in the array and unsets this array element.
Alternatively, and maybe even better, you could do getOrder() on the element you want to have before the new element, then increment this order number by X and then loop through the existing form elements and check that the order number doesn't exist yet.
Or you could just use getOrder() on the Element you want to show before and after the new element and make sure you don't use the same order numbers for the new element.
Sorry to be late to the question. What I did was extend Zend_Form and override the _sort() method as follows:
/**
* Sort items according to their order
*
* #return void
*/
protected function _sort()
{
if ($this->_orderUpdated) {
$items = array();
$index = 0;
foreach ($this->_order as $key => $order) {
if (null === $order) {
if (null === ($order = $this->{$key}->getOrder())) {
while (array_search($index, $this->_order, true)) {
++$index;
}
$items[$index][]= $key;
++$index;
} else {
$items[$order][]= $key;
}
} else {
$items[$order][]= $key;
}
}
ksort($items);
$index = 0;
foreach($items as $i=>$item){
foreach($item as $subItem){
$newItems[$index++]=$subItem;
}
}
$items = array_flip($newItems);
asort($items);
$this->_order = $items;
$this->_orderUpdated = false;
}
}
This differs from the original sort method by putting the items in an array based off of their index and then doing a depth-first traversal to flatten the array.
Try this code:
$elements = array();
$elements[] = new Zend_Form_Element_Textarea('address');
......
$elements[] = new Zend_Form_Element_Text('city');
.......
$elements[] = new Zend_Form_Element_Submit($handle);
.....
$this->addElements($elements);
All you need to do is add them in the order you want them to show
what i would do is - use a temp array for that - in that keep the element names in desired order (don't mind the keys). Then use foreach like this:
foreach(array_values($tempArray) as $order => $name) {
$form->$name->setOrder($order+1);
}
Note the array_values - it will return the values as numbered array ;) Not sure if setOrder(0) works - that's why there is +1