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

codeigniter4 / CodeIgniter4 / 14430292866

13 Apr 2025 02:16PM UTC coverage: 84.395% (-0.1%) from 84.525%
14430292866

push

github

paulbalandan
Merge branch 'develop' into 4.7

23 of 29 new or added lines in 12 files covered. (79.31%)

28 existing lines in 1 file now uncovered.

20816 of 24665 relevant lines covered (84.39%)

191.36 hits per line

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

76.83
/system/Test/FilterTestTrait.php
1
<?php
2

3
declare(strict_types=1);
4

5
/**
6
 * This file is part of CodeIgniter 4 framework.
7
 *
8
 * (c) CodeIgniter Foundation <admin@codeigniter.com>
9
 *
10
 * For the full copyright and license information, please view
11
 * the LICENSE file that was distributed with this source code.
12
 */
13

14
namespace CodeIgniter\Test;
15

16
use Closure;
17
use CodeIgniter\Exceptions\InvalidArgumentException;
18
use CodeIgniter\Exceptions\RuntimeException;
19
use CodeIgniter\Filters\Exceptions\FilterException;
20
use CodeIgniter\Filters\FilterInterface;
21
use CodeIgniter\Filters\Filters;
22
use CodeIgniter\HTTP\RequestInterface;
23
use CodeIgniter\HTTP\ResponseInterface;
24
use CodeIgniter\Router\RouteCollection;
25
use Config\Filters as FiltersConfig;
26

27
/**
28
 * Filter Test Trait
29
 *
30
 * Provides functionality for testing
31
 * filters and their route associations.
32
 *
33
 * @mixin CIUnitTestCase
34
 */
35
trait FilterTestTrait
36
{
37
    /**
38
     * Have the one-time classes been instantiated?
39
     *
40
     * @var bool
41
     */
42
    private $doneFilterSetUp = false;
43

44
    /**
45
     * The active IncomingRequest or CLIRequest
46
     *
47
     * @var RequestInterface
48
     */
49
    protected $request;
50

51
    /**
52
     * The active Response instance
53
     *
54
     * @var ResponseInterface
55
     */
56
    protected $response;
57

58
    /**
59
     * The Filters configuration to use.
60
     * Extracted for access to aliases
61
     * during Filters::discoverFilters().
62
     *
63
     * @var FiltersConfig|null
64
     */
65
    protected $filtersConfig;
66

67
    /**
68
     * The prepared Filters library.
69
     *
70
     * @var Filters|null
71
     */
72
    protected $filters;
73

74
    /**
75
     * The default App and discovered
76
     * routes to check for filters.
77
     *
78
     * @var RouteCollection|null
79
     */
80
    protected $collection;
81

82
    // --------------------------------------------------------------------
83
    // Staging
84
    // --------------------------------------------------------------------
85

86
    /**
87
     * Initializes dependencies once.
88
     */
89
    protected function setUpFilterTestTrait(): void
90
    {
91
        if ($this->doneFilterSetUp === true) {
12✔
92
            return;
×
93
        }
94

95
        // Create our own Request and Response so we can
96
        // use the same ones for Filters and FilterInterface
97
        // yet isolate them from outside influence
98
        $this->request ??= clone service('request');
12✔
99
        $this->response ??= clone service('response');
12✔
100

101
        // Create our config and Filters instance to reuse for performance
102
        $this->filtersConfig ??= config(FiltersConfig::class);
12✔
103
        $this->filters ??= new Filters($this->filtersConfig, $this->request, $this->response);
12✔
104

105
        if ($this->collection === null) {
12✔
106
            $this->collection = service('routes')->loadRoutes();
12✔
107
        }
108

109
        $this->doneFilterSetUp = true;
12✔
110
    }
111

112
    // --------------------------------------------------------------------
113
    // Utility
114
    // --------------------------------------------------------------------
115

116
    /**
117
     * Returns a callable method for a filter position
118
     * using the local HTTP instances.
119
     *
120
     * @param FilterInterface|string $filter   The filter instance, class, or alias
121
     * @param string                 $position "before" or "after"
122
     *
123
     * @phpstan-return Closure(list<string>|null=): mixed
124
     */
125
    protected function getFilterCaller($filter, string $position): Closure
126
    {
127
        if (! in_array($position, ['before', 'after'], true)) {
6✔
128
            throw new InvalidArgumentException('Invalid filter position passed: ' . $position);
1✔
129
        }
130

131
        if ($filter instanceof FilterInterface) {
5✔
132
            $filterInstances = [$filter];
1✔
133
        }
134

135
        if (is_string($filter)) {
5✔
136
            // Check for an alias (no namespace)
137
            if (! str_contains($filter, '\\')) {
4✔
138
                if (! isset($this->filtersConfig->aliases[$filter])) {
3✔
139
                    throw new RuntimeException("No filter found with alias '{$filter}'");
×
140
                }
141

142
                $filterClasses = (array) $this->filtersConfig->aliases[$filter];
3✔
143
            } else {
144
                // FQCN
145
                $filterClasses = [$filter];
1✔
146
            }
147

148
            $filterInstances = [];
4✔
149

150
            foreach ($filterClasses as $class) {
4✔
151
                // Get an instance
152
                $filter = new $class();
4✔
153

154
                if (! $filter instanceof FilterInterface) {
4✔
155
                    throw FilterException::forIncorrectInterface($filter::class);
×
156
                }
157

158
                $filterInstances[] = $filter;
4✔
159
            }
160
        }
161

162
        $request = clone $this->request;
5✔
163

164
        if ($position === 'before') {
5✔
165
            return static function (?array $params = null) use ($filterInstances, $request) {
5✔
166
                $result = null;
4✔
167

168
                foreach ($filterInstances as $filter) {
4✔
169
                    $result = $filter->before($request, $params);
4✔
170

171
                    // @TODO The following logic is in Filters class.
172
                    //       Should use Filters class.
173
                    if ($result instanceof RequestInterface) {
4✔
174
                        $request = $result;
4✔
175

176
                        continue;
4✔
177
                    }
178
                    if ($result instanceof ResponseInterface) {
×
179
                        return $result;
×
180
                    }
181
                    if (empty($result)) {
×
182
                        continue;
×
183
                    }
184
                }
185

186
                return $result;
4✔
187
            };
5✔
188
        }
189

190
        $response = clone $this->response;
×
191

192
        return static function (?array $params = null) use ($filterInstances, $request, $response) {
×
NEW
193
            $result = null;
×
194

195
            foreach ($filterInstances as $filter) {
×
196
                $result = $filter->after($request, $response, $params);
×
197

198
                // @TODO The following logic is in Filters class.
199
                //       Should use Filters class.
200
                if ($result instanceof ResponseInterface) {
×
201
                    $response = $result;
×
202

203
                    continue;
×
204
                }
205
            }
206

207
            return $result;
×
208
        };
×
209
    }
210

211
    /**
212
     * Gets an array of filter aliases enabled
213
     * for the given route at position.
214
     *
215
     * @param string $route    The route to test
216
     * @param string $position "before" or "after"
217
     *
218
     * @return list<string> The filter aliases
219
     */
220
    protected function getFiltersForRoute(string $route, string $position): array
221
    {
222
        if (! in_array($position, ['before', 'after'], true)) {
5✔
223
            throw new InvalidArgumentException('Invalid filter position passed:' . $position);
×
224
        }
225

226
        $this->filters->reset();
5✔
227

228
        $routeFilters = $this->collection->getFiltersForRoute($route);
5✔
229

230
        if ($routeFilters !== []) {
5✔
231
            $this->filters->enableFilters($routeFilters, $position);
×
232
        }
233

234
        $aliases = $this->filters->initialize($route)->getFilters();
5✔
235

236
        $this->filters->reset();
5✔
237

238
        return $aliases[$position];
5✔
239
    }
240

241
    // --------------------------------------------------------------------
242
    // Assertions
243
    // --------------------------------------------------------------------
244

245
    /**
246
     * Asserts that the given route at position uses
247
     * the filter (by its alias).
248
     *
249
     * @param string $route    The route to test
250
     * @param string $position "before" or "after"
251
     * @param string $alias    Alias for the anticipated filter
252
     */
253
    protected function assertFilter(string $route, string $position, string $alias): void
254
    {
255
        $filters = $this->getFiltersForRoute($route, $position);
1✔
256

257
        $this->assertContains(
1✔
258
            $alias,
1✔
259
            $filters,
1✔
260
            "Filter '{$alias}' does not apply to '{$route}'.",
1✔
261
        );
1✔
262
    }
263

264
    /**
265
     * Asserts that the given route at position does not
266
     * use the filter (by its alias).
267
     *
268
     * @param string $route    The route to test
269
     * @param string $position "before" or "after"
270
     * @param string $alias    Alias for the anticipated filter
271
     */
272
    protected function assertNotFilter(string $route, string $position, string $alias)
273
    {
274
        $filters = $this->getFiltersForRoute($route, $position);
1✔
275

276
        $this->assertNotContains(
1✔
277
            $alias,
1✔
278
            $filters,
1✔
279
            "Filter '{$alias}' applies to '{$route}' when it should not.",
1✔
280
        );
1✔
281
    }
282

283
    /**
284
     * Asserts that the given route at position has
285
     * at least one filter set.
286
     *
287
     * @param string $route    The route to test
288
     * @param string $position "before" or "after"
289
     */
290
    protected function assertHasFilters(string $route, string $position)
291
    {
292
        $filters = $this->getFiltersForRoute($route, $position);
1✔
293

294
        $this->assertNotEmpty(
1✔
295
            $filters,
1✔
296
            "No filters found for '{$route}' when at least one was expected.",
1✔
297
        );
1✔
298
    }
299

300
    /**
301
     * Asserts that the given route at position has
302
     * no filters set.
303
     *
304
     * @param string $route    The route to test
305
     * @param string $position "before" or "after"
306
     */
307
    protected function assertNotHasFilters(string $route, string $position)
308
    {
309
        $filters = $this->getFiltersForRoute($route, $position);
1✔
310

311
        $this->assertSame(
1✔
312
            [],
1✔
313
            $filters,
1✔
314
            "Found filters for '{$route}' when none were expected: " . implode(', ', $filters) . '.',
1✔
315
        );
1✔
316
    }
317
}
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

© 2026 Coveralls, Inc