Skip to content

Commit 167d6ea

Browse files
committed
fix view fragments
1 parent 2a8fb60 commit 167d6ea

21 files changed

+376
-113
lines changed

README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ It also provides some additional help with **handling errors** and **Debug Toolb
1616

1717
composer require michalsn/codeigniter-htmx
1818

19-
Remember - you still need to include the `htmx` javascript library inside the `head` tag.
19+
> [!NOTE]
20+
> Remember - you still need to include the `htmx` javascript library inside the `head` tag.
2021
2122
## Docs
2223

phpstan.neon.dist

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ parameters:
1212
ignoreErrors:
1313
- '#Variable \$testString might not be defined.#'
1414
- '#Variable \$this might not be defined.#'
15+
- '#Call to an undefined method CodeIgniter\\View\\RendererInterface::renderFragments\(\).#'
16+
- '#Call to an undefined method CodeIgniter\\View\\RendererInterface::parseFragments\(\).#'
1517
universalObjectCratesClasses:
1618
- CodeIgniter\Entity
1719
- CodeIgniter\Entity\Entity

rector.php

+1-3
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@
3131
use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector;
3232
use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector;
3333
use Rector\Php55\Rector\String_\StringClassNameToClassConstantRector;
34-
use Rector\Php73\Rector\FuncCall\JsonThrowOnErrorRector;
3534
use Rector\Php73\Rector\FuncCall\StringifyStrNeedlesRector;
3635
use Rector\PHPUnit\AnnotationsToAttributes\Rector\Class_\AnnotationWithValueToAttributeRector;
3736
use Rector\PHPUnit\CodeQuality\Rector\Class_\YieldDataProviderRector;
@@ -48,7 +47,7 @@
4847
return static function (RectorConfig $rectorConfig): void {
4948
$rectorConfig->sets([
5049
SetList::DEAD_CODE,
51-
LevelSetList::UP_TO_PHP_74,
50+
LevelSetList::UP_TO_PHP_80,
5251
PHPUnitSetList::PHPUNIT_CODE_QUALITY,
5352
PHPUnitSetList::PHPUNIT_100,
5453
]);
@@ -91,7 +90,6 @@
9190
$rectorConfig->skip([
9291
__DIR__ . '/src/Views',
9392

94-
JsonThrowOnErrorRector::class,
9593
StringifyStrNeedlesRector::class,
9694
YieldDataProviderRector::class,
9795

src/Common.php

+1-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,6 @@ function view_fragment(string $name, array|string $fragments, array $data = [],
3232
? array_map('trim', explode(',', $fragments))
3333
: $fragments;
3434

35-
return $renderer->setData($data, 'raw')->render($name, $options, $saveData);
35+
return $renderer->setData($data, 'raw')->renderFragments($name, $options, $saveData);
3636
}
3737
}

src/View/View.php

+121-87
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
use CodeIgniter\Debug\Toolbar\Collectors\Views;
66
use CodeIgniter\Filters\DebugToolbar;
77
use CodeIgniter\View\Exceptions\ViewException;
8+
use CodeIgniter\View\RendererInterface;
89
use CodeIgniter\View\View as BaseView;
910
use Config\Toolbar;
1011
use RuntimeException;
@@ -15,9 +16,9 @@
1516
class View extends BaseView
1617
{
1718
/**
18-
* Holds the sections and their data.
19+
* Show fragments tags or not.
1920
*/
20-
protected array $fragments = [];
21+
protected bool $showFragments = false;
2122

2223
/**
2324
* The name of the current section being rendered,
@@ -27,6 +28,116 @@ class View extends BaseView
2728
*/
2829
protected array $fragmentStack = [];
2930

31+
/**
32+
* Starts holds content for a fragment within the layout.
33+
*
34+
* @param string $name Fragment name
35+
*/
36+
public function fragment(string $name): void
37+
{
38+
$this->fragmentStack[] = $name;
39+
40+
if ($this->showFragments) {
41+
echo sprintf('@[[fragmentStart="%s"]]', $name);
42+
}
43+
}
44+
45+
/**
46+
* Captures the last fragment
47+
*
48+
* @throws RuntimeException
49+
*/
50+
public function endFragment(): void
51+
{
52+
if ($this->fragmentStack === []) {
53+
ob_end_clean();
54+
55+
throw new RuntimeException('View themes, no current fragment.');
56+
}
57+
58+
$name = array_pop($this->fragmentStack);
59+
60+
if ($this->showFragments) {
61+
echo sprintf('@[[fragmentEnd="%s"]]', $name);
62+
}
63+
}
64+
65+
/**
66+
* Whether we should display fragments tags or not.
67+
*/
68+
protected function showFragments(bool $display = true): RendererInterface
69+
{
70+
$this->showFragments = $display;
71+
72+
return $this;
73+
}
74+
75+
/**
76+
* Render fragments.
77+
*/
78+
public function renderFragments(string $name, ?array $options = null, ?bool $saveData = null): string
79+
{
80+
$fragments = $options['fragments'] ?? [];
81+
$output = $this->showFragments()->render($name, $options, $saveData);
82+
83+
if ($fragments === []) {
84+
return preg_replace('/@\[\[fragmentStart="[^"]+"\]\]|@\[\[fragmentEnd="[^"]+"\]\]/', '', $output);
85+
}
86+
87+
$result = $this->showFragments(false)->parseFragments($output, $fragments);
88+
$output = '';
89+
90+
foreach ($result as $contents) {
91+
$output .= implode('', $contents);
92+
}
93+
94+
return $output;
95+
}
96+
97+
/**
98+
* Parse output to retrieve fragments.
99+
*/
100+
protected function parseFragments(string $output, array $fragments): array
101+
{
102+
$results = [];
103+
$stack = [];
104+
105+
// Match all fragment start and end tags at once
106+
preg_match_all('/@\[\[fragmentStart="([^"]+)"\]\]|@\[\[fragmentEnd="([^"]+)"\]\]/', $output, $matches, PREG_OFFSET_CAPTURE);
107+
108+
// Return empty array if no matches
109+
if (count($matches[0]) === 0) {
110+
return $results;
111+
}
112+
113+
foreach ($matches[0] as $index => $match) {
114+
$pos = $match[1];
115+
$isStart = isset($matches[1][$index]) && $matches[1][$index][0] !== '';
116+
$name = $isStart ? $matches[1][$index][0] : (isset($matches[2][$index]) ? $matches[2][$index][0] : '');
117+
118+
if ($isStart) {
119+
$stack[] = ['name' => $name, 'start' => $pos];
120+
} elseif ($stack !== [] && end($stack)['name'] === $name) {
121+
$info = array_pop($stack);
122+
123+
// Calculate the position of the fragment content
124+
$fragmentStart = $info['start'] + strlen($matches[0][array_search($info['name'], array_column($matches[1], 0), true)][0]);
125+
$fragmentEnd = $pos;
126+
127+
// Extract the content between the tags
128+
$content = substr($output, $fragmentStart, $fragmentEnd - $fragmentStart);
129+
// Clean the fragment content by removing the tags
130+
$content = preg_replace('/@\[\[fragmentStart="[^"]+"\]\]|@\[\[fragmentEnd="[^"]+"\]\]/', '', $content);
131+
132+
if (in_array($info['name'], $fragments, true)) {
133+
$results[$info['name']][] = $content;
134+
}
135+
}
136+
}
137+
138+
return $results;
139+
}
140+
30141
/**
31142
* Builds the output based upon a file name and any
32143
* data that has already been set.
@@ -35,13 +146,13 @@ class View extends BaseView
35146
* - cache Number of seconds to cache for
36147
* - cache_name Name to use for cache
37148
*
38-
* @param string $view File name of the view source
39-
* @param array|null $options Reserved for 3rd-party uses since
40-
* it might be needed to pass additional info
41-
* to other template engines.
42-
* @param bool|null $saveData If true, saves data for subsequent calls,
43-
* if false, cleans the data after displaying,
44-
* if null, uses the config setting.
149+
* @param string $view File name of the view source
150+
* @param array<string, mixed>|null $options Reserved for 3rd-party uses since
151+
* it might be needed to pass additional info
152+
* to other template engines.
153+
* @param bool|null $saveData If true, saves data for subsequent calls,
154+
* if false, cleans the data after displaying,
155+
* if null, uses the config setting.
45156
*/
46157
public function render(string $view, ?array $options = null, ?bool $saveData = null): string
47158
{
@@ -58,7 +169,7 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n
58169

59170
// Was it cached?
60171
if (isset($this->renderVars['options']['cache'])) {
61-
$cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']);
172+
$cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']) . (empty($this->renderVars['options']['fragments']) ? '' : implode('', $this->renderVars['options']['fragments']));
62173
$cacheName = str_replace(['\\', '/'], '', $cacheName);
63174

64175
$this->renderVars['cacheName'] = $cacheName;
@@ -109,13 +220,6 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n
109220
$output = $this->render($layoutView, $options, $saveData);
110221
// Get back current vars
111222
$this->renderVars = $renderVars;
112-
} elseif (! empty($this->renderVars['options']['fragments']) && $this->fragmentStack === []) {
113-
$output = '';
114-
115-
foreach ($this->renderVars['options']['fragments'] as $fragmentName) {
116-
$output .= $this->renderFragment($fragmentName);
117-
unset($this->fragments[$fragmentName]);
118-
}
119223
}
120224

121225
$output = $this->decorateOutput($output);
@@ -148,74 +252,4 @@ public function render(string $view, ?array $options = null, ?bool $saveData = n
148252

149253
return $output;
150254
}
151-
152-
/**
153-
* Starts holds content for a fragment within the layout.
154-
*
155-
* @param string $name Fragment name
156-
*
157-
* @return void
158-
*/
159-
public function fragment(string $name)
160-
{
161-
$this->fragmentStack[] = $name;
162-
163-
ob_start();
164-
}
165-
166-
/**
167-
* Captures the last fragment
168-
*
169-
* @throws RuntimeException
170-
*/
171-
public function endFragment()
172-
{
173-
$contents = ob_get_clean();
174-
175-
if ($this->fragmentStack === []) {
176-
throw new RuntimeException('View themes, no current fragment.');
177-
}
178-
179-
$fragmentName = array_pop($this->fragmentStack);
180-
181-
// Ensure an array exists, so we can store multiple entries for this.
182-
if (! array_key_exists($fragmentName, $this->fragments)) {
183-
$this->fragments[$fragmentName] = [];
184-
}
185-
186-
$this->fragments[$fragmentName][] = $contents;
187-
188-
echo $contents;
189-
}
190-
191-
/**
192-
* Renders a fragment's contents.
193-
*/
194-
protected function renderFragment(string $fragmentName)
195-
{
196-
if (! isset($this->fragments[$fragmentName])) {
197-
return '';
198-
}
199-
200-
foreach ($this->fragments[$fragmentName] as $contents) {
201-
return $contents;
202-
}
203-
}
204-
205-
/**
206-
* Used within layout views to include additional views.
207-
*
208-
* @param bool $saveData
209-
*/
210-
public function include(string $view, ?array $options = null, $saveData = true): string
211-
{
212-
if ($this->fragmentStack !== [] && ! empty($this->renderVars['options']['fragments'])) {
213-
$options['fragments'] = $this->renderVars['options']['fragments'];
214-
echo $this->render($view, $options, $saveData);
215-
216-
return '';
217-
}
218-
219-
return $this->render($view, $options, $saveData);
220-
}
221255
}

tests/CommonTest.php

+66
Original file line numberDiff line numberDiff line change
@@ -87,4 +87,70 @@ public function testViewFragmentFromLayout(): void
8787

8888
$this->assertSame($expected, view_fragment('with_fragment', 'sample0', $data));
8989
}
90+
91+
public function testComplexLayout(): void
92+
{
93+
$data = ['foo' => 'FOO'];
94+
$result = view_fragment('complex/view1', ['fragment1'], $data + ['bar' => 'BAR'])
95+
. view_fragment('complex/view2', ['fragment2'], $data + ['baz' => 'BAZ'])
96+
. view_fragment('complex/view3', ['fragment3'], $data + ['too' => 'TOO'])
97+
. view_fragment('complex/include', ['include'], $data + ['inc' => 'INC']);
98+
99+
$expected = <<<'EOD'
100+
<div>
101+
<b>Fragment 2</b><br>
102+
<b>foo: </b> YES<br>
103+
<b>bar: </b> YES<br>
104+
<b>baz: </b> YES<br>
105+
<b>too: </b> NO<br>
106+
<b>inc: </b> NO<br>
107+
</div>
108+
<div>
109+
<b>Fragment 3</b><br>
110+
<b>foo: </b> YES<br>
111+
<b>bar: </b> YES<br>
112+
<b>baz: </b> YES<br>
113+
<b>too: </b> YES<br>
114+
<b>inc: </b> NO<br>
115+
</div>
116+
<div>
117+
<b>Include</b><br>
118+
<b>foo: </b> YES<br>
119+
<b>bar: </b> YES<br>
120+
<b>baz: </b> YES<br>
121+
<b>too: </b> YES<br>
122+
<b>inc: </b> YES<br>
123+
</div>
124+
125+
EOD;
126+
127+
$this->assertSame($expected, $result);
128+
}
129+
130+
public function testManySameNameFragments()
131+
{
132+
$result = view_fragment('many/view1', ['fragment1']);
133+
134+
$expected = <<<'EOD'
135+
<b>Fragment 1 (1)</b><br>
136+
<b>Fragment 1 (3)</b><br>
137+
138+
EOD;
139+
140+
$this->assertSame($expected, $result);
141+
}
142+
143+
public function testHugeView(): void
144+
{
145+
$result = view_fragment('huge/view', ['fragment_one']);
146+
147+
$expected = <<<'EOD'
148+
Fragment one (from "huge/view")
149+
Fragment one (from "huge/include")
150+
Fragment one (from "huge/layout")
151+
152+
EOD;
153+
154+
$this->assertSame($expected, $result);
155+
}
90156
}

0 commit comments

Comments
 (0)