Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 66 additions & 86 deletions Classes/ContentObject/JsonContentObject.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,24 +14,18 @@
use FriendsOfTYPO3\Headless\Json\JsonDecoderInterface;
use FriendsOfTYPO3\Headless\Json\JsonEncoder;
use FriendsOfTYPO3\Headless\Utility\HeadlessUserInt;
use Generator;
use Psr\Log\LoggerAwareInterface;
use Psr\Log\LoggerAwareTrait;
use RecursiveArrayIterator;
use RecursiveIteratorIterator;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Frontend\ContentObject\AbstractContentObject;
use TYPO3\CMS\Frontend\ContentObject\ContentDataProcessor;

use function is_array;
use function strpos;

class JsonContentObject extends AbstractContentObject implements LoggerAwareInterface
{
use LoggerAwareTrait;

private array $conf;

public function __construct(
protected ContentDataProcessor $contentDataProcessor,
protected JsonEncoder $jsonEncoder,
Expand All @@ -41,25 +35,28 @@ public function __construct(

/**
* Rendering the cObject, JSON
*
* @param array $conf Array of TypoScript properties
* @return string The HTML output
* @return string JSON-encoded content
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}

if (!empty($conf['if.']) && !$this->cObj->checkIf($conf['if.'])) {
return '';
}

$data = [];

if (!is_array($conf)) {
$conf = [];
}
$nullableFieldsIfEmpty = array_flip(
GeneralUtility::trimExplode(',', $conf['nullableFieldsIfEmpty'] ?? '', true)
);

$this->conf = $conf;
$data = [];

if (isset($conf['fields.'])) {
$data = $this->cObjGet($conf['fields.']);
$data = $this->cObjGet($conf['fields.'], '', $nullableFieldsIfEmpty);
}
if (isset($conf['dataProcessing.'])) {
$data = $this->processFieldWithDataProcessing($conf);
Expand All @@ -79,80 +76,71 @@ public function render($conf = []): string
}

/**
* Rendering of a "string array" of cObjects from TypoScript
* Will call ->cObjGetSingle() for each cObject found and accumulate the output.
*
* @param array $setup array with cObjects as values.
* @param string $addKey A prefix for the debugging information
* @return array Rendered output from the cObjects in the array.
* @see cObjGetSingle()
* @param array $setup
* @param string $addKey
* @param array $nullableFieldsIfEmpty
* @return array
*/
public function cObjGet(array $setup, string $addKey = ''): array
public function cObjGet(array $setup, string $addKey = '', array $nullableFieldsIfEmpty = []): array
{
$content = [];
$nullableFieldsIfEmpty = GeneralUtility::trimExplode(',', $this->conf['nullableFieldsIfEmpty'] ?? '', true);

$sKeyArray = $this->filterByStringKeys($setup);
foreach ($sKeyArray as $theKey) {
$theValue = $setup[$theKey];
if ((string)$theKey && !str_contains($theKey, '.')) {
foreach ($setup as $theKey => $theValue) {
if (!is_string($theKey) || $theKey === '') {
continue;
}

if (!str_contains($theKey, '.')) {
$conf = $setup[$theKey . '.'] ?? [];
$contentDataProcessing['dataProcessing.'] = $conf['dataProcessing.'] ?? [];
$content[$theKey] = $this->cObj->cObjGetSingle($theValue, $conf, $addKey . $theKey);
if ((isset($conf['intval']) && $conf['intval']) || $theValue === 'INT') {

if (!empty($conf['intval']) || $theValue === 'INT') {
$content[$theKey] = (int)$content[$theKey];
}
if ((isset($conf['floatval']) && $conf['floatval']) || $theValue === 'FLOAT') {
} elseif (!empty($conf['floatval']) || $theValue === 'FLOAT') {
$content[$theKey] = (float)$content[$theKey];
}
if ((isset($conf['boolval']) && $conf['boolval']) || $theValue === 'BOOL') {
} elseif (!empty($conf['boolval']) || $theValue === 'BOOL') {
$content[$theKey] = (bool)(int)$content[$theKey];
}
if ($theValue === 'USER_INT' || str_starts_with((string)$content[$theKey], '<!--INT_SCRIPT.')) {
$content[$theKey] = $this->headlessUserInt->wrap($content[$theKey], (int)($conf['ifEmptyReturnNull'] ?? 0) === 1 ? HeadlessUserInt::STANDARD_NULLABLE : HeadlessUserInt::STANDARD);

$ifEmptyReturnNull = (int)($conf['ifEmptyReturnNull'] ?? 0) === 1;

if ($theValue === 'USER_INT' || (is_string($content[$theKey]) && str_starts_with($content[$theKey], '<!--INT_SCRIPT.'))) {
$content[$theKey] = $this->headlessUserInt->wrap(
$content[$theKey],
$ifEmptyReturnNull ? HeadlessUserInt::STANDARD_NULLABLE : HeadlessUserInt::STANDARD
);
}
if ($content[$theKey] === '' && ((int)($conf['ifEmptyReturnNull'] ?? 0) === 1 || in_array($theKey, $nullableFieldsIfEmpty, true))) {

if ($content[$theKey] === '' && ($ifEmptyReturnNull || isset($nullableFieldsIfEmpty[$theKey]))) {
$content[$theKey] = null;
}

if ((int)($conf['ifEmptyUnsetKey'] ?? 0) === 1 && ($content[$theKey] === '' || $content[$theKey] === false)) {
unset($content[$theKey]);
}
if (!empty($contentDataProcessing['dataProcessing.'])) {
$content[rtrim($theKey, '.')] = $this->processFieldWithDataProcessing($contentDataProcessing);

if (isset($conf['dataProcessing.'])) {
$content[$theKey] = $this->processFieldWithDataProcessing(
['dataProcessing.' => $conf['dataProcessing.']]
);
}
}
if ((string)$theKey && strpos($theKey, '.') > 0 && !isset($setup[rtrim($theKey, '.')])) {
$contentFieldName = $theValue['source'] ?? rtrim($theKey, '.');
$contentFieldTypeProcessing['dataProcessing.'] = $theValue['dataProcessing.'] ?? [];
} elseif ($theKey[0] !== '.' && !isset($setup[rtrim($theKey, '.')])) {
$trimmedKey = rtrim($theKey, '.');
$contentFieldName = $theValue['source'] ?? $trimmedKey;

if (array_key_exists('fields.', $theValue)) {
$content[$contentFieldName] = $this->cObjGet($theValue['fields.']);
$content[$contentFieldName] = $this->cObjGet($theValue['fields.'], '', $nullableFieldsIfEmpty);
}
if (!empty($contentFieldTypeProcessing['dataProcessing.'])) {
$content[rtrim($theKey, '.')] = $this->processFieldWithDataProcessing($contentFieldTypeProcessing);

if (isset($theValue['dataProcessing.'])) {
$content[$trimmedKey] = $this->processFieldWithDataProcessing(
['dataProcessing.' => $theValue['dataProcessing.']]
);
}
}
}
return $content;
}

/**
* Takes a TypoScript array as input and returns an array which contains all string properties found which had a value (not only properties).
*
* @param array $setupArr TypoScript array with string array in
* @param bool $acceptAnyKeys If set, then a value is not required - the properties alone will be enough.
* @return array An array with all string properties.
*/
protected function filterByStringKeys(array $setupArr, bool $acceptAnyKeys = false): array
{
$filteredKeys = [];
$keys = array_keys($setupArr);
foreach ($keys as $key) {
if ($acceptAnyKeys || is_string($key)) {
$filteredKeys[] = (string)$key;
}
}
return array_unique($filteredKeys);
return $content;
}

/**
Expand All @@ -169,37 +157,29 @@ protected function processFieldWithDataProcessing(array $dataProcessing): mixed
]
);

$dataProcessingData = null;

foreach ($this->recursiveFind($dataProcessing, 'as') as $value) {
foreach ($this->findAsKeys($dataProcessing) as $value) {
if (isset($data[$value])) {
$dataProcessingData = $data[$value];
return $data[$value];
}
}
return $dataProcessingData;

return null;
}

/**
* @param array<string, mixed> $haystack
* Collects all 'as' alias keys from the top-level dataProcessing processor configs.
*
* @param array $dataProcessing
* @return array
*/
protected function recursiveFind(array $haystack, string $needle): Generator
private function findAsKeys(array $dataProcessing): array
{
$iterator = new RecursiveArrayIterator($haystack);
$recursive = new RecursiveIteratorIterator(
$iterator,
RecursiveIteratorIterator::SELF_FIRST
);
$iteration = 0;
foreach ($recursive as $key => $value) {
if ($key === 'dataProcessing.') {
$iteration++;
if ($iteration > 1) {
return;
}
}
if ($key === $needle) {
yield $value;
$asKeys = [];
foreach ($dataProcessing['dataProcessing.'] ?? [] as $value) {
if (is_array($value) && isset($value['as'])) {
$asKeys[] = $value['as'];
}
}
return $asKeys;
}
}
44 changes: 24 additions & 20 deletions Classes/Json/JsonDecoder.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,58 +12,62 @@
namespace FriendsOfTYPO3\Headless\Json;

use function is_array;
use function is_numeric;
use function is_object;
use function is_string;
use function json_decode;
use function json_last_error;
use function trim;

use const JSON_ERROR_NONE;
use const PHP_VERSION_ID;

class JsonDecoder implements JsonDecoderInterface
{
/**
* @inheritDoc
*/
public function decode(array $data): array
{
$json = [];
$result = [];

foreach ($data as $key => $singleData) {
if (is_string($singleData)) {
if ($this->isJson($singleData)) {
$json[$key] = json_decode($singleData);
foreach ($data as $key => $value) {
if (is_string($value)) {
if ($value !== '' && ($value[0] === '{' || $value[0] === '[')) {
$decoded = json_decode($value);
$result[$key] = (is_object($decoded) || is_array($decoded)) ? $decoded : $value;
} else {
$json[$key] = $singleData;
$result[$key] = $value;
}
} elseif (is_array($singleData)) {
$json[$key] = $this->decode($singleData);
} elseif (is_array($value)) {
$result[$key] = $this->decode($value);
} else {
$json[$key] = $singleData;
$result[$key] = $value;
}
}
return $json;

return $result;
}

/**
* @param mixed $possibleJson
*/
public function isJson($possibleJson): bool
public function isJson(mixed $possibleJson): bool
{
if (is_numeric($possibleJson)) {
if (!is_string($possibleJson) || $possibleJson === '') {
return false;
}

$possibleJson = trim((string)$possibleJson);
$trimmed = trim($possibleJson);

if ($possibleJson === '') {
if ($trimmed === '' || ($trimmed[0] !== '{' && $trimmed[0] !== '[')) {
return false;
}

$data = json_decode($possibleJson);

if (!is_object($data) && !is_array($data)) {
return false;
if (PHP_VERSION_ID >= 80300) {
return json_validate($possibleJson);
}

return $data !== null;
json_decode($possibleJson);
return json_last_error() === JSON_ERROR_NONE;
}
}
4 changes: 3 additions & 1 deletion Tests/Unit/ViewHelpers/Format/Json/DecodeViewHelperTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ public function testRender(): void
$GLOBALS['TYPO3_CONF_VARS']['FE']['debug'] = true;
$decodeViewHelper = new DecodeViewHelper();
$decodeViewHelper->setArguments(['json' => null]);
$decodeViewHelper->setRenderChildrenClosure(function () { return "\n \n"; });
$decodeViewHelper->setRenderChildrenClosure(function () {
return "\n \n";
});
$result = $decodeViewHelper->render();
self::assertNull($result);
}
Expand Down
Loading