Skip to content

Commit 63f8686

Browse files
Add ViewHelper to render styleguide component with fixture
1 parent cd1f0b7 commit 63f8686

File tree

1 file changed

+247
-0
lines changed

1 file changed

+247
-0
lines changed
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace Sitegeist\FluidStyleguide\ViewHelpers\Component;
5+
6+
use Sitegeist\FluidStyleguide\Domain\Model\Component;
7+
use Sitegeist\FluidStyleguide\Domain\Repository\ComponentRepository;
8+
use Sitegeist\FluidStyleguide\Exception\RequiredComponentArgumentException;
9+
use Sitegeist\FluidStyleguide\Service\StyleguideConfigurationManager;
10+
use SMS\FluidComponents\Fluid\ViewHelper\ComponentRenderer;
11+
use TYPO3\CMS\Core\Utility\GeneralUtility;
12+
use TYPO3\CMS\Extbase\Object\ObjectManager;
13+
use TYPO3Fluid\Fluid\Core\Rendering\RenderingContextInterface;
14+
use TYPO3Fluid\Fluid\Core\Variables\StandardVariableProvider;
15+
use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractViewHelper;
16+
use TYPO3Fluid\Fluid\Core\ViewHelper\Traits\CompileWithRenderStatic;
17+
18+
class RenderFixtureViewHelper extends AbstractViewHelper
19+
{
20+
use CompileWithRenderStatic;
21+
22+
/**
23+
* @var boolean
24+
*/
25+
protected $escapeOutput = false;
26+
27+
public function initializeArguments()
28+
{
29+
$this->registerArgument('component', 'string', 'Name of the component that should be rendered', true);
30+
$this->registerArgument('fixtureName', 'string', 'Name of the fixture that the component should be rendered with', false, 'default');
31+
$this->registerArgument('fixtureData', 'array', 'Additional dynamic fixture data that should be used');
32+
}
33+
34+
/**
35+
* Renders fluid example code for the specified component
36+
*
37+
* @param array $arguments
38+
* @param \Closure $renderChildrenClosure
39+
* @param RenderingContextInterface $renderingContext
40+
* @return string
41+
*/
42+
public static function renderStatic(
43+
array $arguments,
44+
\Closure $renderChildrenClosure,
45+
RenderingContextInterface $renderingContext
46+
): string {
47+
48+
$componentIdentifier = self::sanitizeComponentIdentifier($arguments['component'] ?? '');
49+
50+
$objectManager = GeneralUtility::makeInstance(ObjectManager::class);
51+
$componentRepository = $objectManager->get(ComponentRepository::class);
52+
53+
$component = $componentRepository->findWithFixturesByIdentifier($componentIdentifier);
54+
if (!$component) {
55+
return sprintf('Component %s not found', $componentIdentifier);
56+
}
57+
58+
if (!isset($arguments['fixtureName']) && !isset($arguments['fixtureData'])) {
59+
throw new \InvalidArgumentException(sprintf(
60+
'A fixture name or fixture data has to be specified to render the component %s.',
61+
$arguments['component']
62+
), 1566377563);
63+
}
64+
65+
$fixtureData = $arguments['fixtureData'] ?? [];
66+
67+
$fixtureName = self::sanitizeFixtureName($arguments['fixtureName'] ?? 'default');
68+
69+
if (isset($fixtureName)) {
70+
$componentFixture = $component->getFixture($fixtureName);
71+
if (!$componentFixture) {
72+
throw new \InvalidArgumentException(sprintf(
73+
'Invalid fixture name "%s" specified for component %s.',
74+
$fixtureName,
75+
$componentIdentifier
76+
), 1566377564);
77+
}
78+
79+
// Merge static fixture data with manually edited data
80+
$fixtureData = array_replace($componentFixture->getData(), $fixtureData);
81+
}
82+
83+
$renderingContext->getViewHelperResolver()->addNamespace('fsv', 'Sitegeist\FluidStyleguide\ViewHelpers');
84+
85+
// Parse fluid code in fixtures
86+
$fixtureData = self::renderFluidInExampleData($fixtureData, $renderingContext);
87+
88+
$styleguideConfigurationManager = $objectManager->get(StyleguideConfigurationManager::class);
89+
$componentContext = $styleguideConfigurationManager->getComponentContext();
90+
91+
$componentMarkup = self::renderComponent(
92+
$component,
93+
$fixtureData,
94+
$renderingContext
95+
);
96+
97+
$componentWithContext = self::applyComponentContext(
98+
$componentMarkup,
99+
$componentContext,
100+
$renderingContext,
101+
array_replace(
102+
$component->getDefaultValues(),
103+
$fixtureData
104+
)
105+
);
106+
107+
return $componentWithContext;
108+
}
109+
110+
/**
111+
* Make sure that the component identifier doesn't include any malicious characters
112+
*
113+
* @param string $componentIdentifier
114+
* @return string
115+
*/
116+
protected static function sanitizeComponentIdentifier(string $componentIdentifier): string
117+
{
118+
return trim(preg_replace('#[^a-z0-9_\\\\]#i', '', $componentIdentifier), '\\');
119+
}
120+
121+
/**
122+
* Make sure that the fixture name doesn't include any malicious characters
123+
*
124+
* @param string $fixtureName
125+
* @return string
126+
*/
127+
protected static function sanitizeFixtureName(string $fixtureName): string
128+
{
129+
return preg_replace('#[^a-z0-9_]#i', '', $fixtureName);
130+
}
131+
132+
/**
133+
* Calls a component with the supplied example data
134+
*
135+
* @param Component $component
136+
* @param array $data
137+
* @param RenderingContextInterface $renderingContext
138+
* @return string
139+
*/
140+
public static function renderComponent(
141+
Component $component,
142+
array $data,
143+
RenderingContextInterface $renderingContext
144+
): string {
145+
// Check if all required arguments were supplied to the component
146+
foreach ($component->getArguments() as $expectedArgument) {
147+
if ($expectedArgument->isRequired() && !isset($data[$expectedArgument->getName()])) {
148+
throw new RequiredComponentArgumentException(sprintf(
149+
'Required argument "%s" was not supplied for component %s.',
150+
$expectedArgument->getName(),
151+
$component->getName()->getIdentifier()
152+
), 1566636254);
153+
}
154+
}
155+
156+
return ComponentRenderer::renderComponent(
157+
$data,
158+
function () {
159+
return '';
160+
},
161+
$renderingContext,
162+
$component->getName()->getIdentifier()
163+
);
164+
}
165+
166+
/**
167+
* Renders inline fluid code in a fixture array that will be provided as example data to a component
168+
*
169+
* @param mixed $data
170+
* @param RenderingContextInterface $renderingContext
171+
* @return mixed
172+
*/
173+
public static function renderFluidInExampleData($data, RenderingContextInterface $renderingContext)
174+
{
175+
if (is_string($data)) {
176+
return $renderingContext->getTemplateParser()->parse($data)->render($renderingContext);
177+
} elseif (is_array($data)) {
178+
return array_map(function ($value) use ($renderingContext) {
179+
return self::renderFluidInExampleData($value, $renderingContext);
180+
}, $data);
181+
} else {
182+
return $data;
183+
}
184+
}
185+
186+
/**
187+
* Wraps component markup in the specified component context (HTML markup)
188+
* The component markup will replace all pipe characters (|) in the context string
189+
* Optionally, a renderingContext and template data can be provided, in which case
190+
* the context markup will be treated as fluid markup
191+
*
192+
* @param string $componentMarkup
193+
* @param string $context
194+
* @param RenderingContextInterface $renderingContext
195+
* @param array $data
196+
* @return string
197+
*/
198+
public static function applyComponentContext(
199+
string $componentMarkup,
200+
string $context,
201+
RenderingContextInterface $renderingContext = null,
202+
array $data = []
203+
): string {
204+
// Check if the context should be fetched from a file
205+
$context = self::checkObtainComponentContextFromFile($context);
206+
207+
if (isset($renderingContext)) {
208+
// Use unique value as component markup marker
209+
$marker = '###COMPONENT_MARKUP_' . mt_rand() . '###';
210+
$context = str_replace('|', $marker, $context);
211+
212+
// Parse fluid tags in context string
213+
$originalVariableContainer = $renderingContext->getVariableProvider();
214+
$renderingContext->setVariableProvider(new StandardVariableProvider($data));
215+
$context = $renderingContext->getTemplateParser()->parse($context)->render($renderingContext);
216+
$renderingContext->setVariableProvider($originalVariableContainer);
217+
218+
// Wrap component markup
219+
return str_replace($marker, $componentMarkup, $context);
220+
} else {
221+
return str_replace('|', $componentMarkup, $context);
222+
}
223+
}
224+
225+
/**
226+
* Checks if the provided component context is a file path and returns its contents;
227+
* falls back to the specified context string.
228+
*
229+
* @param string $context
230+
* @return string
231+
*/
232+
protected static function checkObtainComponentContextFromFile(string $context): string
233+
{
234+
// Probably not a file path
235+
if (strpos($context, '|') !== false) {
236+
return $context;
237+
}
238+
239+
// Check if the value is a valid file
240+
$path = GeneralUtility::getFileAbsFileName($context);
241+
if (!file_exists($path)) {
242+
return $context;
243+
}
244+
245+
return file_get_contents($path);
246+
}
247+
}

0 commit comments

Comments
 (0)