webman
webman

Reputation: 1203

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

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.

Upvotes: 0

Views: 1518

Answers (3)

footrotflat
footrotflat

Reputation: 11

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!

Upvotes: 1

webman
webman

Reputation: 1203

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);
    }
}

Upvotes: 2

Mathias Brodala
Mathias Brodala

Reputation: 6460

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.

Upvotes: 3

Related Questions