TYPO3: Reading the values of a TCA 'type' => 'check' (bitmask) - typo3

I need to show a selection of days in an event in the frontend:
in my TCA I set the field like this:
'days' => [
'exclude' => true,
'label' => 'choose weekdays',
'config' => [
'type' => 'check',
'eval' => 'required,unique',
'items' => [
['monday',''],
['thuesday',''],
['wednesday',''],
['thursday',''],
['friday',''],
['saturday',''],
['sunday',''],
],
'cols' => 'inline',
],
],
That stores an integer in the db, but now I have to display the selected days in a fluid template in the frontend.
This is the reference regarding in the TYPO3 documentation which explains that I should check the bit-0 of values ... I've searched a lot but couldn't find anything except this question here on stack overflow, which I cannot get to work.

I strongly recommend not to use the bitmasking feature of the check field. It's rarely worth the overhead to split the values apart again and also is a lot harder to understand for most developers.
Instead you can use a select field, in this case selectCheckBox should serve you well. Given a static list of items you will get a CSV string with the selected values which is a lot easier to split, e.g. in a getter method of an Extbase domain model. If it makes sense you can even use a relation to records instead which is even cleaner but requires additional work.
If you still want to continue with bitmasks this answer may help you.

SOLUTION 1: using Mathias's solution mixed with the one of Dimitri L.
I wanted to give it here as a full solution to this particular question, so add this in the domain model:
/**
* #var int
*/
protected $days;
and then following for all the days:
/**
* Get day 1
*
* #return int
*/
public function getDay1()
{
return $this->days & 0b00000001 ? 1 : 0;
}
/**
* Set day 1
*
* #param int $day1
*/
public function setDay1($day1) {
if ($day1) {
$this->days |= 0b00000001;
} else {
$this->days &= ~0b00000001;
}
}
/**
* And so on for the other 7 days
*/
You can now use it in extbase $object->getDay1() or in fluid {object.day1}
As Mathias stated, it quickly gets very complicated, I preferred this solution since I use it only to display the days an event takes place in a week, and in a calendar so a 0 or 1 solution was just fine.
SOLUTION 2: I ended up using the decimal bitmask value from the database directly in a viewhelper: (solution is adepted for the number of checkboxes used, in my case the 7 weekdays)
use \TYPO3\CMS\Extbase\Utility\LocalizationUtility;
/**
* News extension
*
* #package TYPO3
* #subpackage tx_news
*/
class CoursedaysViewHelper extends \TYPO3\CMS\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper
{
/**
* #param string $days (bitmask)
* #return string checked weekdays seperated by /
*/
public function render($days)
{
// render binary, 7 digits, split into array and reverse
$days = decbin($days);
$days = sprintf('%07d', $days);
$days = str_split($days);
$days = array_reverse($days);
foreach($days as $day){
$key = 'days.' . ++$a;
if($day) $coursedays .= LocalizationUtility::translate($key, 'news_ext') . '/';
}
return substr($coursedays, 0, -1);
}
}

A possible solution for multiple checkboxes by evaluating the bitmask
Programmers often want to read some data into a form and then output it as text. There are a few examples of this here.
Sometimes programmers want to display the form data in the same form with multiple checkboxes so that the user can change the data. There are no examples of this and many programmers find it difficult to read the data bit by bit and then output it again.
Here is a working example (in BE and FE):
(Tested with Typo3 9.5.20 and 10.4.9)
In TCA the example of the question:
'days' => [
'exclude' => false,
'label' => 'LLL:EXT:example/Resources/Private/Language/locallang_db.xlf:tx_example_domain_model_week.days',
'config' => [
'type' => 'check',
'items' => [
['monday', ''],
['thuesday', ''],
['wednesday', ''],
['thursday', ''],
['friday', ''],
['saturday', ''],
['sunday', ''],
],
'default' => 0,
]
],
The model:
The type of the property must be integer.
However, getters and setters are arrays because we have a multiple checkbox and this is implemented with an array.
It is important to keep this in mind as it will create a problem that needs to be resolved.
class Week extends \TYPO3\CMS\Extbase\DomainObject\AbstractEntity
{
/**
* Days
*
* #var int
*/
protected $days = 0;
/**
* Returns the days
*
* #return array $days
*/
public function getDays()
{
return $this->days;
}
/**
* Sets the days
*
* #param array $days
* #return void
*/
public function setDays($days)
{
$this->days = $days;
}
}
In controller
In the initializeCreateAction and the initializeUpdateAction we solve the problem of the different property types between integer and arrays.
Without this, we receive an error message that the array cannot be converted to an integer.
This code means that Extbase should keep the property type.
In the createAction and the updateAction we branch to the method countBits in CheckboxUtility to add the values of the selected checkboxes.
In the editAction and the updateAction we branch to the method convertDataForMultipleCheckboxes in CheckboxUtility in order to convert the values to be input and output.
/**
* initializeCreateAction
* #return void
*/
public function initializeCreateAction(): void
{
if ($this->arguments->hasArgument('newWeek')) {
$this->arguments->getArgument('newWeek')->getPropertyMappingConfiguration()->setTargetTypeForSubProperty('days', 'array');
}
}
/**
* action create
*
* #param Week $newWeek
* #return void
*/
public function createAction(Week $newWeek)
{
$days = (int)CheckboxUtility::countBits($newWeek->getDays());
$newWeek->setDays($days);
$this->weekRepository->add($newWeek);
$this->redirect('list');
}
/**
* action edit
*
* #param Week $week
* #return void
*/
public function editAction(Week $week)
{
$week->setDays(CheckboxUtility::convertDataForMultipleCheckboxes((int)$week->getDays()));
$this->view->assign('week', $week);
}
/**
* initializeUpdateAction
* #return void
*/
public function initializeUpdateAction(): void
{
if ($this->arguments->hasArgument('week')) {
$this->arguments->getArgument('week')->getPropertyMappingConfiguration()->setTargetTypeForSubProperty('days', 'array');
}
}
/**
* action update
*
* #param Week $week
* #return void
*/
public function updateAction(Week $week)
{
$days = (int)CheckboxUtility::countBits($week->getDays());
$week->setDays($days);
$this->weekRepository->update($week);
$this->redirect('list');
}
In Classes/Utility/CheckboxUtility.php
Read the code. The procedure is described at each point.
In method convertDataForMultipleCheckboxes the basic direction is as follows:
We have an integer value in the database, e.g. 109.
In binary notation: 1011011 (64 + 32 + 0 + 8 + 4 + 0 + 1 = 109)
In the form, this means that the first, third, fourth, sixth and seventh checkboxes are selected.
We read the binary value from left to right, at 1011011 in seven loops.
For example, let's read the first character (from the left) we overwrite the six characters on the right with 0. This results in the binary number 1000000, in decimal notation = 64.
For example, let's read the fourth character (from the left) we overwrite the three characters on the right with 0. This results in the binary number 1000, in decimal notation = 8.
When we have read this, we will get the result 64 + 32 + 0 + 8 + 4 + 0 + 1 because we read from left to right.
Therefore we turn the result around at the end so that each checkbox receives the correct value!
So we get this 1 + 0 + 4 + 8 + 0 + 32 + 64 because the first, third, fourth, sixth and seventh checkboxes are selected.
In method countBits we just add all integer values to one number.
namespace Vendor\Example\Utility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
class CheckboxUtility extends GeneralUtility
{
/**
* Convert an integer to binary and then convert each bit back to an integer for use with multiple checkboxes.
*
* #param int $value
* #return array
*/
public static function convertDataForMultipleCheckboxes(int $value): array
{
$bin = decbin($value); // convert dec to bin
$num = strlen($bin); // counts the bits
$res = array();
for ($i = 0; $i < $num; $i++) {
// loop through binary value
if ($bin[$i] !== 0) {
$bin_2 = str_pad($bin[$i], $num - $i, '0'); //pad string
$res[] = bindec($bin_2); // convert that bit to dec and push in array
}
}
return array_reverse($res); // reverse order and return
}
/**
* Adds the values of the checkboxes
*
* #param array $value
* #return int
*/
public static function countBits(array $value): int
{
foreach ($value as $key => $item) {
$res = $res + $item;
}
return $res;
}
}
In Templates or Partials
The argument multiple="1" is important here. This adds an additional dimension to the array of property days. (This can be seen in the website's source code).
It is important that we give the checkbox the correct value according to the binary notation.
When we have read the values from the database, the result is available to us as an array. So we read the additional dimension at the appropriate place (starting with 0) in the same order as the order of the checkboxes. e.g. the seventh value / checkbox: checked = "{week.days.6} == 64"
<f:form.checkbox
id="day_1"
property="days"
value="1"
multiple="1"
checked="{week.days.0} == 1" />
<label for="day_1" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day1" />
</label>
<f:form.checkbox
id="day_2"
property="days"
value="2"
multiple="1"
checked="{week.days.1} == 2" />
<label for="day_2" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day2" />
</label>
<f:form.checkbox
id="day_3"
property="days"
value="4"
multiple="1"
checked="{week.days.2} == 4" />
<label for="day_3" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day3" />
</label>
<f:form.checkbox
id="day_4"
property="days"
value="8"
multiple="1"
checked="{week.days.3} == 8" />
<label for="day_4" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day4" />
</label>
<f:form.checkbox
id="day_5"
property="days"
value="16"
multiple="1"
checked="{week.days.4} == 16" />
<label for="day_5" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day5" />
</label>
<f:form.checkbox
id="day_6"
property="days"
value="32"
multiple="1"
checked="{week.days.5} == 32" />
<label for="day_6" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day6" />
</label>
<f:form.checkbox
id="day_7"
property="days"
value="64"
multiple="1"
checked="{week.days.6} == 64" />
<label for="day_7" class="form-control-label">
<f:translate key="tx_example_domain_model_week.day7" />
</label>
... and now happy coding!

Related

TYPO3 f:form with additional arguments, where submit not update the arguments

I am using TYPO3 10.4.15
My edit view:
f:section name="content">
<h1>Edit Album</h1>
<f:flashMessages />
<f:render partial="FormErrors" />
<f:form id='fNew' action="update" name="album" object="{album}" arguments="{mode:mode, disc:disc}" >
<f:render partial="Album/FormFields" arguments="{album:album, disc:disc}" />
<f:form.submit value="Save" />
</f:form>
</f:section>
</html>
This is the relevant part of the partial formfields.html:
<f:if condition='{disc}'>
<input type='text' name="disc[0][name][]" />
</f:if>
The error_log with the disc structure looks:
Update-Disc: array (
0 =>
array (
'name' => '',
'trackNum' => '1',
'track' =>
array (
0 =>
array (
'title' => '',
'duration' => '0',
'composer' => '',
'texter' => '',
'musicFile' => '',
'imageFile' => '',
),
),
),
)
And this is the "updateAction" part of the controller
/**
* action update
*
* #param \HGA\Album\Domain\Model\Album $album
* #param string $mode
* #param array $disc
* #return string|object|null|void
*/
public function updateAction(\HGA\Album\Domain\Model\Album $album, $mode, $disc)
{
error_log("Update-Disc: " . var_export($disc, true) . " Mode: " . $mode, 0);
if ($mode == 'tracks') {
$this->editAction($album, $mode, $disc);
}
error_log("Update: " . var_export($album, true) . " Mode: " . $mode, 0);
$this->addFlashMessage('The object was updated. Please be aware that this action is publicly accessible unless you implement an access check. See https://docs.typo3.org/typo3cms/extensions/extension_builder/User/Index.html', '', \TYPO3\CMS\Core\Messaging\AbstractMessage::WARNING);
$this->albumRepository->update($album);
$this->redirect('list');
}
If I write something into the text input field and execute submit, I get the error_log you can see above. The value I have typed in the input field is missing. It is only the array, as I have send it to the view.
The mode string will be transmitted correctly, but with the disc array is maybe something wrong!
The disc array is more complex, but I made it simple, because I need to understand how it works in general.
I also need this additional disc array and can not doing it with the album object!
Thanks in advance for your help.
You are ignoring your plugin's namespace in combination with misinterpretation of f:form arguments.
Each field for your plugin has a prefix like tx_hgaalbum... followed by your property's name in square brackets. So the fieldname for disc should look like tx_hgaalbum...[disc]. Have a look into the HTML code and see which names are generated for the other properties.
The second problem is using the arguments in the form-ViewHelper. This will only add the arguments to the action URI of your form. That's why you're getting your initial values for disc.

TYPO3 FileReference does not save the tablename on the DB. Uploading file from frontend on TYPO3

In my custom extension on TYPO3 10.4 I'm trying to upload a file (image) from the frontend. The file gets uploaded just fine, the rows on the DB seemed to be inserted just fine but there is some data missing.
This is my form:
<f:form method="post" action="create" name="blackboard"
object="{blackboard}" enctype="multipart/form-data">
<f:form.textfield placeholder="Titel*" required="true" property="title"></f:form.textfield>
<f:form.upload property="image" name="image" />
<f:form.submit class="btn btn-primary" value="{f:translate(key: 'submit', default: 'Absenden')}"></f:form.submit>
</f:form>
The model:
/**
* image
*
* #var \TYPO3\CMS\Extbase\Domain\Model\FileReference
* #TYPO3\CMS\Extbase\Annotation\ORM\Cascade("remove")
*/
protected $image = null;
/**
* Returns the image
*
* #return \TYPO3\CMS\Extbase\Domain\Model\FileReference $image
*/
public function getImage()
{
return $this->image;
}
/**
* Sets the image
*
* #param \TYPO3\CMS\Extbase\Domain\Model\FileReference $image
* #return void
*/
public function setImage(\TYPO3\CMS\Extbase\Domain\Model\FileReference $image)
{
$this->image = $image;
}
The controller:
/**
* action create
* #param Blackboard
*/
public function createAction(Blackboard $blackboard)
{
$blackboard->setPid($GLOBALS['TYPO3_CONF_VARS']['EXTENSIONS']['hebo_blackboards']['BlackboardsStoragePId']);
$blackboard->setUser($GLOBALS['TSFE']->fe_user->user['uid']);
$this->blackboardRepository->add($blackboard);
}
Surprisingly, just that easy, this seems to work just fine. I get the image uploaded on the server, the correct UID of that sys_file_reference on my custom table, the sys_file_reference gets the correct UID of that sys_file... but as you can see in the pic that follows there are a few data missing, "tablename" and "table_local" and as soon as I add that data manually the relationships work (the first rows, where this data is not missing is from rows created from the backend, working fine)
My question is, why? What do I do to fix that?
The problem is that extbase doesn't know those values, therefore you need to state those in the TCA. Given this example
'extra_files' => [
'label' => 'A file',
'config' => \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::getFileFieldTCAConfig(
'extra_files',
[
'foreign_match_fields' => [
'tablenames' => 'tx_yourtable_domain_model_fo',
'table_local' => 'sys_file'
]
],
$GLOBALS['TYPO3_CONF_VARS']['GFX']['imagefile_ext']
),
],
The foreign_match_fields part is the relevant one which is not needed if you don't handle file uploads in the Frontend.

Check TYPO3 link type in fluid

I want to render a typolink in fluid, but i need to check if it is a download file (t3://file?uid=1), a page link (t3://page?uid=1) or an external link (https://www.abc.de _blank).
Is there a way or viewhelper to check the linktype in fluid?
What i found were only via typoscript or a hacky way with VHS like
<f:if condition="{target -> v:math.round()} > 0">
It's for TYPO3 9.x
$linkService = $this->objectManager->get(LinkService::class);
$result = $linkService->resolve($linkValue);
That could help you in a custom ViewHelper
Possible Values for $linkValue:
t3://page?uid=1 => [string (pageuid), 'page']
info#example.com => [string (email), 'email']
https://typo3.org => [string (url), 'url']
t3://file?uid=226 => [TYPO3\CMS\Core\Resource\File, 'file']
$result returns an array. Every case has the argument "type". Depending on the type, another value or object is returned. I have listed this above.
The class is available from TYPO3 Version 8.
You could also check the linktype with the vhs extension, e.g. to set a different target:
{namespace v=FluidTYPO3\Vhs\ViewHelpers}
...
<f:variable name="target">_self</f:variable>
<v:condition.string.contains haystack="{url}" needle="t3://file?uid">
<f:variable name="target">_blank</f:variable>
</v:condition.string.contains>
<v:condition.string.contains haystack="{url}" needle="http">
<f:variable name="target">_blank</f:variable>
</v:condition.string.contains>
<v:condition.string.contains haystack="{url}" needle="www">
<f:variable name="target">_blank</f:variable>
</v:condition.string.contains>
<f:link.typolink parameter="{url}" target="{target}">the link</f:link.typolink>
This is the ViewHelper i am using now:
/**
* A view helper for rendering the linktype
*
* = Examples =
*
* <code>
* {nc:linkType(parameter: link)}
* </code>
* <output>
* page, file, url, email, folder, unknown
* </output>
*/
class LinkTypeViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;
/**
* Initialize arguments
*/
public function initializeArguments()
{
$this->registerArgument('parameter', 'string', 'stdWrap.typolink style parameter string', true);
}
/**
* #param array $arguments
* #param \Closure $renderChildrenClosure
* #param RenderingContextInterface $renderingContext
* #return string Linktype (page, file, url, email, folder, unknown)
*/
public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext)
{
$parameter = $arguments['parameter'];
// workaround if parameter has _blank or other additional params
$arr = explode(' ',trim($parameter));
$firstparameter = $arr[0];
$linkService = GeneralUtility::makeInstance(LinkService::class);
$linkDetails = $linkService->resolve($firstparameter);
return $linkDetails['type'];
}
}

How to set __referrer of a form to another action and controller

I have two domain models, called Foo and Bar. For both of them, I have a controller with the default CRUD operations. In the displayAction of Foos, there is a form to create a new Bar (rendered using a partial). Thanks to Helmut Hummels Extension "typoscript_rendering" I can easily submit that form to the BarController with AJAX.
However, if validation fails for the created Bar-object, the newAction of the BarController should be called, so the user can fix the problems.
But instead, the displayAction of the FooController is called. This happens, because Fluid automatically inserts the currently executed action as referrer into the form:
<input name="tx_myextension_display[__referrer][#extension]" value="MyExtension" type="hidden">
<input name="tx_myextension_display[__referrer][#vendor]" value="MyVendor" type="hidden">
<input name="tx_myextension_display[__referrer][#controller]" value="FooController" type="hidden">
<input name="tx_myextension_display[__referrer][#action]" value="display" type="hidden">
Is there any hack-free way to change the __referrer to the newAction of the BarController?
Edit: As requested, the form as it is in fluid. Unnecessary markup is removed, variable names are anonymized and it is rendered as part of the displayAction of the FooController:
{namespace helhum=Helhum\TyposcriptRendering\ViewHelpers}
<f:form
name="bar"
object="{MyBar}"
action="create"
controller="Bar"
enctype="multipart/form-data"
additionalAttributes="{data-ajax-uri: '{helhum:uri.ajaxAction(action: \\'create\\', controller: \\'Bar\\')}'}"
>
<f:form.textarea property="b"/>
<f:form.submit value="Submit"/>
</f:form>
It is just a 08/15-form.
If I submit it with invalid data, I am forwarded to the display-Action of the FooController, instead of the new-Action of the BarController. I guess that makes sense, as long as the form is not submitted by AJAX.
I've found a "solution" that is still hacky, because it is susceptible to the open base class problem.
It works by overriding the method errorAction, because it is called if a validation error occurs. It also does the redirecting to the referring controller.
My override looks like this:
/**
* If an error occurred while creating a new Bar with an
* AJAX request, redirect to the new action of this controller.
*/
public function errorAction() {
if ('createByAjaxAction' === $this->actionMethodName) {
$this->clearCacheOnError();
$this->addErrorFlashMessage();
$this->forward('new');
}
else {
parent::errorAction();
}
}
/*
* If an error occurred during this request, this adds
* a flash message describing the error to the flash
* message container.
* #TODO Remove after upgrade to 7.x
* #return void
*/
protected function addErrorFlashMessage() {
$errorFlashMessage = $this->getErrorFlashMessage();
if ($errorFlashMessage !== FALSE) {
$this->addFlashMessage($errorFlashMessage, '', \TYPO3\CMS\Core\Messaging\FlashMessage::ERROR);
}
}
/**
* Create action that is supposed to be used by AJAX.
* #param \MyVendor\MyExt\Domain\Model\Bar
*/
public function createByAjaxAction($bar) {
$this->forward('create');
}
/**
* Normal create action.
* #param \MyVendor\MyExt\Domain\Model\Bar
*/
public function createAction($bar) {
// Normal entity creation stuff
}
My problem with this approach is this: If the original errorAction of the ActionController from extbase changes in the future (as it does in 7.x, see this change for example), this code will happily break. Same thing if the member variable actionMethodName changes.
So if someone knows how to do this in a more future-proof way, please add an answer.
Your problem is an open #todo in the viewhelper f:form
* #todo filter out referrer information that is equal to the target (e.g. same packageKey)
*/
protected function renderHiddenReferrerFields()
...
My copy&paste-Workaround was to extend the viewhelper f:form. I defined a child-class in my extension.
<?php
namespace Porth\Positioner\ViewHelpers;
class FormViewHelper extends \TYPO3\CMS\Fluid\ViewHelpers\FormViewHelper {
/**
* Renders hidden form fields for referrer information about
* the current controller and action.
*
* Renders hidden form field for secured referrer information about the current controller and action.
*
* This method is called twice, to deal with subclasses of this class in a most compatible way
*
* #param string|null $action
* #param string|null $controller
* #param string|null $vendor
* #param string|null $extension
*
* #return string Hidden field with secured referrer information
* #todo filter out referrer information that is equal to the target (e.g. same packageKey)
*/
protected function renderHiddenReferrerFields($action =null, $controller = null, $vendor = null, $extension = null )
{
$request = $this->controllerContext->getRequest();
$extensionName = ((is_null($extension))? $request->getControllerExtensionName() : $extension);
$vendorName = ((is_null($vendor))? $request->getControllerVendorName() : $vendor);
$controllerName = ((is_null($controller)) ? $request->getControllerName() : $controller);
$actionName = ((is_null($action))? $request->getControllerActionName() : $action);
$result = LF;
$result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[#extension]') . '" value="' . $extensionName . '" />' . LF;
if ($vendorName !== null) {
$result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[#vendor]') . '" value="' . $vendorName . '" />' . LF;
}
$result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[#controller]') . '" value="' . $controllerName . '" />' . LF;
$result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[#action]') . '" value="' . $actionName . '" />' . LF;
$result .= '<input type="hidden" name="' . $this->prefixFieldName('__referrer[arguments]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(base64_encode(serialize($request->getArguments())))) . '" />' . LF;
$result .= $this->renderHiddenSecuredReferrerField($actionName, $controllerName, $vendorName, $extensionName);
return $result;
}
/**
* Renders hidden form field for secured referrer information about the current controller and action.
*
* This method is called twice, to deal with subclasses of this class in a most compatible way
*
* #param string|null $action
* #param string|null $controller
* #param string|null $vendor
* #param string|null $extension
*
* #return string Hidden field with secured referrer information
*/
protected function renderHiddenSecuredReferrerField($action = null, $controller = null, $vendor = null, $extension = null )
{
if ($this->securedReferrerFieldRendered) {
return '';
}
$request = $this->renderingContext->getControllerContext()->getRequest();
$extensionName = ((is_null($extension))? $request->getControllerExtensionName() : $extension);
$vendorName = ((is_null($vendor))? $request->getControllerVendorName() : $vendor);
$controllerName = ((is_null($controller)) ? $request->getControllerName() : $controller);
$actionName = ((is_null($action))? $request->getControllerActionName() : $action);
$actionRequest = [
'#extension' => $extensionName,
'#controller' => $controllerName,
'#action' => $actionName,
];
if ($vendorName !== null) {
$actionRequest['#vendor'] = $vendorName;
}
$result = '<input type="hidden" name="' . $this->prefixFieldName('__referrer[#request]') . '" value="' . htmlspecialchars($this->hashService->appendHmac(serialize($actionRequest))) . '" />' . LF;
$this->securedReferrerFieldRendered = true;
return $result;
}
/**
* Render the form.
*
* #param string $action Target action
* #param array $arguments Arguments
* #param string $controller Target controller
* #param string $vendor Target vendor
* #param string $extensionName Target Extension Name (without "tx_" prefix and no underscores). If NULL the current extension name is used
* #param string $pluginName Target plugin. If empty, the current plugin name is used
* #param int $pageUid Target page uid
* #param mixed $object Object to use for the form. Use in conjunction with the "property" attribute on the sub tags
* #param int $pageType Target page type
* #param bool $noCache set this to disable caching for the target page. You should not need this.
* #param bool $noCacheHash set this to suppress the cHash query parameter created by TypoLink. You should not need this.
* #param string $section The anchor to be added to the action URI (only active if $actionUri is not set)
* #param string $format The requested format (e.g. ".html") of the target page (only active if $actionUri is not set)
* #param array $additionalParams additional action URI query parameters that won't be prefixed like $arguments (overrule $arguments) (only active if $actionUri is not set)
* #param bool $absolute If set, an absolute action URI is rendered (only active if $actionUri is not set)
* #param bool $addQueryString If set, the current query parameters will be kept in the action URI (only active if $actionUri is not set)
* #param array $argumentsToBeExcludedFromQueryString arguments to be removed from the action URI. Only active if $addQueryString = TRUE and $actionUri is not set
* #param string $fieldNamePrefix Prefix that will be added to all field names within this form. If not set the prefix will be tx_yourExtension_plugin
* #param string $actionUri can be used to overwrite the "action" attribute of the form tag
* #param string $objectName name of the object that is bound to this form. If this argument is not specified, the name attribute of this form is used to determine the FormObjectName
* #param string $hiddenFieldClassName
* #return string rendered form
*/
public function render($action = null, array $arguments = array(), $controller = null, $vendor = null, $extensionName = null, $pluginName = null, $pageUid = null, $object = null, $pageType = 0, $noCache = false, $noCacheHash = false, $section = '', $format = '', array $additionalParams = array(), $absolute = false, $addQueryString = false, array $argumentsToBeExcludedFromQueryString = array(), $fieldNamePrefix = null, $actionUri = null, $objectName = null, $hiddenFieldClassName = null)
{
$this->setFormActionUri();
if (strtolower($this->arguments['method']) === 'get') {
$this->tag->addAttribute('method', 'get');
} else {
$this->tag->addAttribute('method', 'post');
}
$this->addFormObjectNameToViewHelperVariableContainer();
$this->addFormObjectToViewHelperVariableContainer();
$this->addFieldNamePrefixToViewHelperVariableContainer();
$this->addFormFieldNamesToViewHelperVariableContainer();
$formContent = $this->renderChildren();
if ($this->arguments['hiddenFieldClassName'] !== null) {
$content = LF . '<div class="' . htmlspecialchars($this->arguments['hiddenFieldClassName']) . '">';
} else {
$content = LF . '<div>';
}
$content .= $this->renderHiddenIdentityField($this->arguments['object'], $this->getFormObjectName());
$content .= $this->renderAdditionalIdentityFields();
$content .= $this->renderHiddenReferrerFields($action , $controller , $vendor , $extensionName );
// $content .= $this->renderHiddenSecuredReferrerField($action , $controller , $vendor , $extensionName );
// Render the trusted list of all properties after everything else has been rendered
$content .= $this->renderTrustedPropertiesField();
$content .= LF . '</div>' . LF;
$content .= $formContent;
$this->tag->setContent($content);
$this->removeFieldNamePrefixFromViewHelperVariableContainer();
$this->removeFormObjectFromViewHelperVariableContainer();
$this->removeFormObjectNameFromViewHelperVariableContainer();
$this->removeFormFieldNamesFromViewHelperVariableContainer();
$this->removeCheckboxFieldNamesFromViewHelperVariableContainer();
return $this->tag->render();
}
}

Use 4-digit years for localised dates in a Twig template

I use the Twig Intl Extension in my Symfony 2.4 project to localize dates:
{{ post.published_at|localizeddate('short', 'none', locale) }}
This works well, but does not display the full year in the yyyy format and uses the yy format instead.
This way, December 31, 2013 would be displayed like 31/12/2013 or 12/31/2013 depending on the locale, instead of 31/12/13 or 31/12/13.
Is there a way to use a 4 digit year with the localised date?
I finally managed to do it!
I extended the default Twig Intl Extension this way:
<?php
namespace Acme\DemoBundle\Twig;
class AcmeIntlExtension extends \Twig_Extensions_Extension_Intl
{
public function getFilters()
{
return array(
new \Twig_SimpleFilter('localizeddate', array($this, 'twigLocalizedDateFilter'), array('needs_environment' => true)),
);
}
public function twigLocalizedDateFilter($env, $date, $dateFormat = 'medium', $timeFormat = 'medium', $locale = null, $timezone = null, $format = null)
{
$date = twig_date_converter($env, $date, $timezone);
$formatValues = array(
'none' => \IntlDateFormatter::NONE,
'short' => \IntlDateFormatter::SHORT,
'medium' => \IntlDateFormatter::MEDIUM,
'long' => \IntlDateFormatter::LONG,
'full' => \IntlDateFormatter::FULL,
);
$formatter = \IntlDateFormatter::create(
$locale,
$formatValues[$dateFormat],
$formatValues[$timeFormat],
$date->getTimezone()->getName(),
\IntlDateFormatter::GREGORIAN,
$format
);
if ($format === null) {
// Replace yy to y (but yyy should be kept yyy, and yyyy kept as yyyy)
// This way, years are NEVER shown as "yy" (eg. 14), but always like "yyyy" (eg. 2014)
$pattern = preg_replace(':(^|[^y])y{2,2}([^y]|$):', '$1y$2', $formatter->getPattern());
$formatter->setPattern($pattern);
}
return $formatter->format($date->getTimestamp());
}
}
It is declared in my Symfony's app/config.yml file:
services:
acme.twig.extension.intl:
class: Acme\DemoBundle\Twig\AcmeIntlExtension
tags:
- { name: twig.extension }
I change the default pattern to replace "yy" to "y" (which is the format for 4-digit years, as described here: http://userguide.icu-project.org/formatparse/datetime).
This way, it works for all formats and for all locales.
Your solution to extend intl extension is good, but you can do it more easily. If you give a deep look in the documentation, you'll see the extension has a format argument. So you can do:
{{ post.published_at|localizeddate('short', 'none', locale, null, 'd/m/y HH:mm') }}
The format follow ICU format guide. For 4 digit years you can use yor yyyy. Full documentation: http://userguide.icu-project.org/formatparse/datetime
Hope it helps.
A change on the answer of Michaƫl Perrin to be able to use the old and new way by adding _year to the date format.
Create the file app/src/Myproject/MyBundle/Twig/Extension/IntlExtension.php with:
<?php
namespace Myproject\MyBundle\Twig\Extension;
/**
* Extension of Twig Intl Extension to add support fo showing 4-digit year. Add
* '_year' to the date format for it to work, i.e. short_year instead of short.
*/
class IntlExtension extends \Twig_Extensions_Extension_Intl
{
/**
* A new filter must be added for this extension to work. Since this extension
* extends an existing one, the filter should be loaded after the original one.
*
* #return array
*/
public function getFilters()
{
$filters = [];
// Add all filters but the localizeddate created by the parent.
foreach (parent::getFilters() as $filter) {
if ('localizeddate' !== $filter->getName()) {
$filters[] = $filter;
}
}
// Add custom version of 'localizeddate' which adds support showing 4-digit years.
$filters[] = new \Twig_SimpleFilter('localizeddate', array($this, 'twigLocalizedDateFilter'), array('needs_environment' => true));
return $filters;
}
/**
* If the date format has _year in it, the format returned by Intl, will be
* changed to show a 4-digit year.
*
* #param \Twig_Environment $env
* #param \DateTime $date
* #param string $dateFormat
* #param string $timeFormat
* #param string $locale
* #param string $timezone
* #param string $format
* #return string
*/
public function twigLocalizedDateFilter(\Twig_Environment $env, $date, $dateFormat = 'medium', $timeFormat = 'medium', $locale = null, $timezone = null, $format = null, $calendar = 'gregorian')
{
$date = twig_date_converter($env, $date, $timezone);
$formatValues = array(
'none' => \IntlDateFormatter::NONE,
'short' => \IntlDateFormatter::SHORT,
'medium' => \IntlDateFormatter::MEDIUM,
'long' => \IntlDateFormatter::LONG,
'full' => \IntlDateFormatter::FULL,
);
$formatter = \IntlDateFormatter::create(
$locale,
$formatValues[$this->getDateFormat($dateFormat)],
$formatValues[$timeFormat],
PHP_VERSION_ID >= 50500 ? $date->getTimezone() : $date->getTimezone()->getName(),
'gregorian' === $calendar ? \IntlDateFormatter::GREGORIAN : \IntlDateFormatter::TRADITIONAL,
$format
);
if ($format === null && $this->showFullYear($dateFormat)) {
// Replace yy to y (but yyy should be kept yyy, and yyyy kept as yyyy)
// This way, years are NEVER shown as "yy" (eg. 14), but always like "yyyy" (eg. 2014)
$pattern = preg_replace(':(^|[^y])y{2,2}([^y]|$):', '$1y$2', $formatter->getPattern());
$formatter->setPattern($pattern);
}
return $formatter->format($date->getTimestamp());
}
/**
* If the given date format has _year in it, true is returned, otherwise false.
*
* #param string $dateFormat
* #return boolean
*/
protected function showFullYear($dateFormat)
{
return (strpos($dateFormat, '_year') !== false);
}
/**
* The date format will be returned without the part '_year' if it contains it.
* #param string $dateFormat
* #return string
*/
protected function getDateFormat($dateFormat)
{
return str_replace('_year', '', $dateFormat);
}
}
And, off course the addition in app/config/config.yml
services:
myproject.twig.extension.intl:
class: Myproject\MyBundle\Twig\Extension\IntlExtension
tags:
- { name: twig.extension }
Try this:
{{ post.published_at|localizeddate('d/m/Y', 'none', locale) }}
You can use your own format for this with special pkaceholders, for example Y mean YYYY date format, check documentation about localizeddate filter.