• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

michalsn / codeigniter-htmx / 13707691824

06 Mar 2025 08:36PM UTC coverage: 74.783%. First build
13707691824

Pull #83

github

web-flow
Merge 7f79fbae7 into 2ec7f6426
Pull Request #83: chore: update workflow to support PHP 8.4

17 of 22 new or added lines in 6 files covered. (77.27%)

258 of 345 relevant lines covered (74.78%)

7.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

97.98
/src/View/View.php
1
<?php
2

3
namespace Michalsn\CodeIgniterHtmx\View;
4

5
use CodeIgniter\Debug\Toolbar\Collectors\Views;
6
use CodeIgniter\Filters\DebugToolbar;
7
use CodeIgniter\View\Exceptions\ViewException;
8
use CodeIgniter\View\RendererInterface;
9
use CodeIgniter\View\View as BaseView;
10
use Config\Toolbar;
11
use RuntimeException;
12

13
/**
14
 * Class View
15
 */
16
class View extends BaseView
17
{
18
    /**
19
     * Show fragments tags or not.
20
     */
21
    protected bool $showFragments = false;
22

23
    /**
24
     * The name of the current section being rendered,
25
     * if any.
26
     *
27
     * @var list<string>
28
     */
29
    protected array $fragmentStack = [];
30

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;
22✔
39

40
        if ($this->showFragments) {
22✔
41
            echo sprintf('@[[fragmentStart="%s"]]', $name);
22✔
42
        }
43
    }
44

45
    /**
46
     * Captures the last fragment
47
     *
48
     * @throws RuntimeException
49
     */
50
    public function endFragment(): void
51
    {
52
        if ($this->fragmentStack === []) {
23✔
53
            ob_end_clean();
1✔
54

55
            throw new RuntimeException('View themes, no current fragment.');
1✔
56
        }
57

58
        $name = array_pop($this->fragmentStack);
22✔
59

60
        if ($this->showFragments) {
22✔
61
            echo sprintf('@[[fragmentEnd="%s"]]', $name);
22✔
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;
25✔
71

72
        return $this;
25✔
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'] ?? [];
25✔
81
        $output    = $this->showFragments()->render($name, $options, $saveData);
25✔
82

83
        if ($fragments === []) {
23✔
84
            return preg_replace('/@\[\[fragmentStart="[^"]+"\]\]|@\[\[fragmentEnd="[^"]+"\]\]/', '', $output);
4✔
85
        }
86

87
        $result = $this->showFragments(false)->parseFragments($output, $fragments);
19✔
88
        $output = '';
19✔
89

90
        foreach ($result as $contents) {
19✔
91
            $output .= implode('', $contents);
17✔
92
        }
93

94
        return $output;
19✔
95
    }
96

97
    /**
98
     * Parse output to retrieve fragments.
99
     */
100
    protected function parseFragments(string $output, array $fragments): array
101
    {
102
        $results = [];
20✔
103
        $stack   = [];
20✔
104

105
        // Match all fragment start and end tags at once
106
        preg_match_all('/@\[\[fragmentStart="([^"]+)"\]\]|@\[\[fragmentEnd="([^"]+)"\]\]/', $output, $matches, PREG_OFFSET_CAPTURE);
20✔
107

108
        // Return empty array if no matches
109
        if ($matches[0] === []) {
20✔
110
            return $results;
1✔
111
        }
112

113
        foreach ($matches[0] as $index => $match) {
19✔
114
            $pos     = $match[1];
19✔
115
            $isStart = isset($matches[1][$index]) && $matches[1][$index][0] !== '';
19✔
116
            $name    = $isStart ? $matches[1][$index][0] : (isset($matches[2][$index]) ? $matches[2][$index][0] : '');
19✔
117

118
            if ($isStart) {
19✔
119
                $stack[] = ['name' => $name, 'start' => $pos];
19✔
120
            } elseif ($stack !== [] && end($stack)['name'] === $name) {
19✔
121
                $info = array_pop($stack);
19✔
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]);
19✔
125
                $fragmentEnd   = $pos;
19✔
126

127
                // Extract the content between the tags
128
                $content = substr($output, $fragmentStart, $fragmentEnd - $fragmentStart);
19✔
129
                // Clean the fragment content by removing the tags
130
                $content = preg_replace('/@\[\[fragmentStart="[^"]+"\]\]|@\[\[fragmentEnd="[^"]+"\]\]/', '', $content);
19✔
131

132
                if (in_array($info['name'], $fragments, true)) {
19✔
133
                    $results[$info['name']][] = $content;
17✔
134
                }
135
            }
136
        }
137

138
        return $results;
19✔
139
    }
140

141
    /**
142
     * Builds the output based upon a file name and any
143
     * data that has already been set.
144
     *
145
     * Valid $options:
146
     *  - cache      Number of seconds to cache for
147
     *  - cache_name Name to use for cache
148
     *
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.
156
     */
157
    public function render(string $view, ?array $options = null, ?bool $saveData = null): string
158
    {
159
        $this->renderVars['start'] = microtime(true);
27✔
160

161
        // Store the results here so even if
162
        // multiple views are called in a view, it won't
163
        // clean it unless we mean it to.
164
        $saveData ??= $this->saveData;
27✔
165
        $fileExt                     = pathinfo($view, PATHINFO_EXTENSION);
27✔
166
        $realPath                    = empty($fileExt) ? $view . '.php' : $view; // allow Views as .html, .tpl, etc (from CI3)
27✔
167
        $this->renderVars['view']    = $realPath;
27✔
168
        $this->renderVars['options'] = $options ?? [];
27✔
169

170
        // Was it cached?
171
        if (isset($this->renderVars['options']['cache'])) {
27✔
172
            $cacheName = $this->renderVars['options']['cache_name'] ?? str_replace('.php', '', $this->renderVars['view']) . (empty($this->renderVars['options']['fragments']) ? '' : implode('', $this->renderVars['options']['fragments']));
1✔
173
            $cacheName = str_replace(['\\', '/'], '', $cacheName);
1✔
174

175
            $this->renderVars['cacheName'] = $cacheName;
1✔
176

177
            if ($output = cache($this->renderVars['cacheName'])) {
1✔
178
                $this->logPerformance($this->renderVars['start'], microtime(true), $this->renderVars['view']);
1✔
179

180
                return $output;
1✔
181
            }
182
        }
183

184
        $this->renderVars['file'] = $this->viewPath . $this->renderVars['view'];
27✔
185

186
        if (! is_file($this->renderVars['file'])) {
27✔
187
            $this->renderVars['file'] = $this->loader->locateFile($this->renderVars['view'], 'Views', empty($fileExt) ? 'php' : $fileExt);
1✔
188
        }
189

190
        // locateFile will return an empty string if the file cannot be found.
191
        if (empty($this->renderVars['file'])) {
27✔
192
            throw ViewException::forInvalidFile($this->renderVars['view']);
1✔
193
        }
194

195
        // Make our view data available to the view.
196
        $this->prepareTemplateData($saveData);
26✔
197

198
        // Save current vars
199
        $renderVars = $this->renderVars;
26✔
200

201
        $output = (function (): string {
26✔
202
            extract($this->tempData);
26✔
203
            ob_start();
26✔
204
            include $this->renderVars['file'];
26✔
205

206
            return ob_get_clean() ?: '';
25✔
207
        })();
26✔
208

209
        // Get back current vars
210
        $this->renderVars = $renderVars;
25✔
211

212
        // When using layouts, the data has already been stored
213
        // in $this->sections, and no other valid output
214
        // is allowed in $output so we'll overwrite it.
215
        if ($this->layout !== null && $this->sectionStack === []) {
25✔
216
            $layoutView   = $this->layout;
9✔
217
            $this->layout = null;
9✔
218
            // Save current vars
219
            $renderVars = $this->renderVars;
9✔
220
            $output     = $this->render($layoutView, $options, $saveData);
9✔
221
            // Get back current vars
222
            $this->renderVars = $renderVars;
9✔
223
        }
224

225
        $output = $this->decorateOutput($output);
25✔
226

227
        $this->logPerformance(
25✔
228
            $this->renderVars['start'],
25✔
229
            microtime(true),
25✔
230
            $this->renderVars['view'],
25✔
231
        );
25✔
232

233
        // Check if DebugToolbar is enabled.
234
        $filters              = service('filters');
25✔
235
        $requiredAfterFilters = $filters->getRequiredFilters('after')[0];
25✔
236
        if (in_array('toolbar', $requiredAfterFilters, true)) {
25✔
237
            $debugBarEnabled = true;
25✔
238
        } else {
NEW
239
            $afterFilters    = $filters->getFiltersClass()['after'];
×
NEW
240
            $debugBarEnabled = in_array(DebugToolbar::class, $afterFilters, true);
×
241
        }
242

243
        if (
244
            $this->debug && $debugBarEnabled
25✔
245
            && (! isset($options['debug']) || $options['debug'] === true)
25✔
246
            && empty($this->renderVars['options']['fragments'])
25✔
247
        ) {
248
            $toolbarCollectors = config(Toolbar::class)->collectors;
5✔
249

250
            if (in_array(Views::class, $toolbarCollectors, true)) {
5✔
251
                // Clean up our path names to make them a little cleaner
252
                $this->renderVars['file'] = clean_path($this->renderVars['file']);
5✔
253
                $this->renderVars['file'] = ++$this->viewsCount . ' ' . $this->renderVars['file'];
5✔
254

255
                $output = '<!-- DEBUG-VIEW START ' . $this->renderVars['file'] . ' -->' . PHP_EOL
5✔
256
                    . $output . PHP_EOL
5✔
257
                    . '<!-- DEBUG-VIEW ENDED ' . $this->renderVars['file'] . ' -->' . PHP_EOL;
5✔
258
            }
259
        }
260

261
        // Should we cache?
262
        if (isset($this->renderVars['options']['cache'])) {
25✔
263
            cache()->save($this->renderVars['cacheName'], $output, (int) $this->renderVars['options']['cache']);
1✔
264
        }
265

266
        $this->tempData = null;
25✔
267

268
        return $output;
25✔
269
    }
270
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc