Skip to content

Commit

Permalink
[FEATURE] ViewHelpers to return first/last item of an array (#866)
Browse files Browse the repository at this point in the history
The FirstViewHelper and LastViewHelper return the first or last
item of a specified array, respectively.

## Examples

```xml
<f:first value="{0: 'first', 1: 'second', 2: 'third'}" /> <!-- Outputs "first" -->
<f:last value="{0: 'first', 1: 'second', 2: 'third'}" /> <!-- Outputs "third" -->
```
  • Loading branch information
s2b committed Apr 5, 2024
1 parent 2038523 commit 392c7d5
Show file tree
Hide file tree
Showing 4 changed files with 362 additions and 0 deletions.
64 changes: 64 additions & 0 deletions src/ViewHelpers/FirstViewHelper.php
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3Fluid\Fluid\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

/**
* The FirstViewHelper returns the first item of an array.
*
* Example
* ========
* ::
*
* <f:first value="{0: 'first', 1: 'second'}" />
*
* .. code-block:: text
*
* first
*/
final class FirstViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;

public function initializeArguments(): void
{
$this->registerArgument('value', 'array', '', false);
}

public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): mixed
{
$value = $arguments['value'] ?? $renderChildrenClosure();

if ($value === null || !is_iterable($value)) {
$givenType = get_debug_type($value);
throw new \InvalidArgumentException(
'The argument "value" was registered with type "array", but is of type "' .
$givenType . '" in view helper "' . static::class . '".',
1712220569
);
}

$value = self::iteratorToArray($value);

return array_shift($value);
}

/**
* This ensures compatibility with PHP 8.1
*/
private static function iteratorToArray(\Traversable|array $iterator): array
{
return is_array($iterator) ? $iterator : iterator_to_array($iterator);
}
}
64 changes: 64 additions & 0 deletions src/ViewHelpers/LastViewHelper.php
@@ -0,0 +1,64 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3Fluid\Fluid\ViewHelpers;

use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;

/**
* The LastViewHelper returns the last item of an array.
*
* Example
* ========
* ::
*
* <f:last value="{0: 'first', 1: 'second'}" />
*
* .. code-block:: text
*
* second
*/
final class LastViewHelper extends AbstractViewHelper
{
use CompileWithRenderStatic;

public function initializeArguments(): void
{
$this->registerArgument('value', 'array', '', false);
}

public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext): mixed
{
$value = $arguments['value'] ?? $renderChildrenClosure();

if ($value === null || !is_iterable($value)) {
$givenType = get_debug_type($value);
throw new \InvalidArgumentException(
'The argument "value" was registered with type "array", but is of type "' .
$givenType . '" in view helper "' . static::class . '".',
1712221620
);
}

$value = self::iteratorToArray($value);

return array_pop($value);
}

/**
* This ensures compatibility with PHP 8.1
*/
private static function iteratorToArray(\Traversable|array $iterator): array
{
return is_array($iterator) ? $iterator : iterator_to_array($iterator);
}
}
117 changes: 117 additions & 0 deletions tests/Functional/ViewHelpers/FirstViewHelperTest.php
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3\CMS\Fluid\Tests\Functional\ViewHelpers;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\ArrayAccessExample;
use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\IterableExample;
use TYPO3Fluid\Fluid\View\TemplateView;

final class FirstViewHelperTest extends AbstractFunctionalTestCase
{
public static function renderValidDataProvider(): iterable
{
yield 'empty value' => [
'arguments' => [
'value' => [],
],
'src' => '<f:first value="{value}" />',
'expectation' => null,
];
yield 'empty value inline' => [
'arguments' => [
'value' => [],
],
'src' => '{value -> f:first()}',
'expectation' => null,
];
yield 'single item' => [
'arguments' => [
'value' => [1],
],
'src' => '<f:first value="{value}" />',
'expectation' => 1,
];
yield 'multiple items' => [
'arguments' => [
'value' => ['first', 'second', 'third'],
],
'src' => '<f:first value="{value}" />',
'expectation' => 'first',
];
yield 'value inline as iterable' => [
'arguments' => [
'value' => new IterableExample(['first', 'second', 'third']),
],
'src' => '{value -> f:first()}',
'expectation' => 'first',
];
}

#[DataProvider('renderValidDataProvider')]
#[Test]
public function renderValid(array $arguments, string $src, mixed $expectation): void
{
$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
self::assertSame($expectation, $view->render());

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
self::assertSame($expectation, $view->render());
}

public static function renderInvalidDataProvider(): iterable
{
yield 'invalid string content' => [
'arguments' => [
],
'src' => '<f:first>SOME TEXT</f:first>',
];
yield 'invalid string inline' => [
'arguments' => [
'value' => 'string',
],
'src' => '{value -> f:first()}',
];
yield 'arrayaccess inline' => [
'arguments' => [
'value' => new ArrayAccessExample(['foo' => 'bar']),
],
'src' => '{value -> f:first()}',
];
}

#[DataProvider('renderInvalidDataProvider')]
#[Test]
public function renderInvalid(array $arguments, string $src): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(1712220569);

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
$view->render();

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
$view->render();
}
}
117 changes: 117 additions & 0 deletions tests/Functional/ViewHelpers/LastViewHelperTest.php
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

/*
* This file belongs to the package "TYPO3 Fluid".
* See LICENSE.txt that was shipped with this package.
*/

namespace TYPO3\CMS\Fluid\Tests\Functional\ViewHelpers;

use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use TYPO3Fluid\Fluid\Tests\Functional\AbstractFunctionalTestCase;
use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\ArrayAccessExample;
use TYPO3Fluid\Fluid\Tests\Functional\Fixtures\Various\IterableExample;
use TYPO3Fluid\Fluid\View\TemplateView;

final class LastViewHelperTest extends AbstractFunctionalTestCase
{
public static function renderValidDataProvider(): iterable
{
yield 'empty value' => [
'arguments' => [
'value' => [],
],
'src' => '<f:last value="{value}" />',
'expectation' => null,
];
yield 'empty value inline' => [
'arguments' => [
'value' => [],
],
'src' => '{value -> f:last()}',
'expectation' => null,
];
yield 'single item' => [
'arguments' => [
'value' => [1],
],
'src' => '<f:last value="{value}" />',
'expectation' => 1,
];
yield 'multiple items' => [
'arguments' => [
'value' => ['first', 'second', 'third'],
],
'src' => '<f:last value="{value}" />',
'expectation' => 'third',
];
yield 'value inline as iterable' => [
'arguments' => [
'value' => new IterableExample(['first', 'second', 'third']),
],
'src' => '{value -> f:last()}',
'expectation' => 'third',
];
}

#[DataProvider('renderValidDataProvider')]
#[Test]
public function renderValid(array $arguments, string $src, mixed $expectation): void
{
$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
self::assertSame($expectation, $view->render());

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
self::assertSame($expectation, $view->render());
}

public static function renderInvalidDataProvider(): iterable
{
yield 'invalid string content' => [
'arguments' => [
],
'src' => '<f:last>SOME TEXT</f:last>',
];
yield 'invalid string inline' => [
'arguments' => [
'value' => 'string',
],
'src' => '{value -> f:last()}',
];
yield 'arrayaccess inline' => [
'arguments' => [
'value' => new ArrayAccessExample(['foo' => 'bar']),
],
'src' => '{value -> f:last()}',
];
}

#[DataProvider('renderInvalidDataProvider')]
#[Test]
public function renderInvalid(array $arguments, string $src): void
{
$this->expectException(\InvalidArgumentException::class);
$this->expectExceptionCode(1712221620);

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
$view->render();

$view = new TemplateView();
$view->getRenderingContext()->setCache(self::$cache);
$view->getRenderingContext()->getTemplatePaths()->setTemplateSource($src);
$view->assignMultiple($arguments);
$view->render();
}
}

0 comments on commit 392c7d5

Please sign in to comment.