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

codeigniter4 / CodeIgniter4 / 27303624104

10 Jun 2026 08:17PM UTC coverage: 88.338% (+0.1%) from 88.233%
27303624104

Pull #10298

github

web-flow
Merge de7470fb4 into 1bee8837c
Pull Request #10298: fix: resolve TOCTOU symlink vulnerability in FileHandler gc and destroy

29 of 33 new or added lines in 3 files covered. (87.88%)

4 existing lines in 2 files now uncovered.

22171 of 25098 relevant lines covered (88.34%)

210.92 hits per line

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

93.62
/system/API/BaseTransformer.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\API;
15

16
use CodeIgniter\HTTP\IncomingRequest;
17
use InvalidArgumentException;
18

19
/**
20
 * Base class for transforming resources into arrays.
21
 * Fulfills common functionality of the TransformerInterface,
22
 * and provides helper methods for conditional inclusion/exclusion of values.
23
 *
24
 * Supports the following query variables from the request:
25
 * - fields: Comma-separated list of fields to include in the response
26
 *      (e.g., ?fields=id,name,email)
27
 *      If not provided, all fields from toArray() are included.
28
 * - include: Comma-separated list of related resources to include
29
 *      (e.g., ?include=posts,comments)
30
 *      This looks for methods named `include{Resource}()` on the transformer,
31
 *      and calls them to get the related data, which are added as a new key to the output.
32
 *
33
 * Example:
34
 *
35
 * class UserTransformer extends BaseTransformer
36
 * {
37
 *    public function toArray(mixed $resource): array
38
 *    {
39
 *      return [
40
 *          'id' => $resource['id'],
41
 *          'name' => $resource['name'],
42
 *          'email' => $resource['email'],
43
 *          'created_at' => $resource['created_at'],
44
 *          'updated_at' => $resource['updated_at'],
45
 *      ];
46
 *    }
47
 *
48
 *   protected function includePosts(): array
49
 *   {
50
 *       $posts = model('PostModel')->where('user_id', $this->resource['id'])->findAll();
51
 *       return (new PostTransformer())->transformMany($posts);
52
 *   }
53
 * }
54
 */
55
abstract class BaseTransformer implements TransformerInterface
56
{
57
    /**
58
     * Nesting depth of the transformation currently in progress (0 = root).
59
     */
60
    private static int $depth = 0;
61

62
    /**
63
     * @var list<string>|null
64
     */
65
    private ?array $fields = null;
66

67
    /**
68
     * @var list<string>|null
69
     */
70
    private ?array $includes = null;
71

72
    protected mixed $resource = null;
73

74
    public function __construct(
75
        private ?IncomingRequest $request = null,
76
    ) {
77
        // An explicitly provided request is always honored - its scope is
78
        // intentional. Only the implicit global fallback is suppressed for
79
        // nested transformers, which is the actual leak vector.
80
        $explicitRequest = $request instanceof IncomingRequest;
38✔
81

82
        $this->request = $request ?? request();
38✔
83

84
        if ($explicitRequest || self::$depth === 0) {
38✔
85
            $fields       = $this->request->getGet('fields');
38✔
86
            $this->fields = is_string($fields)
38✔
87
                ? array_map(trim(...), explode(',', $fields))
14✔
88
                : $fields;
25✔
89

90
            $includes       = $this->request->getGet('include');
38✔
91
            $this->includes = is_string($includes)
38✔
92
                ? array_map(trim(...), explode(',', $includes))
17✔
93
                : $includes;
23✔
94
        }
95
    }
96

97
    /**
98
     * Converts the resource to an array representation.
99
     * This is overridden by child classes to define the
100
     * API-safe resource representation.
101
     *
102
     * @param mixed $resource The resource being transformed
103
     */
104
    abstract public function toArray(mixed $resource): array;
105

106
    /**
107
     * Transforms the given resource into an array using
108
     * the $this->toArray().
109
     */
110
    public function transform(array|object|null $resource = null): array
111
    {
112
        // Store the resource so include methods can access it
113
        $this->resource = $resource;
36✔
114

115
        if ($resource === null) {
36✔
116
            $data = $this->toArray(null);
3✔
117
        } elseif (is_object($resource) && method_exists($resource, 'toArray')) {
33✔
118
            $data = $this->toArray($resource->toArray());
1✔
119
        } else {
120
            $data = $this->toArray((array) $resource);
32✔
121
        }
122

123
        $data = $this->limitFields($data);
36✔
124

125
        return $this->insertIncludes($data);
35✔
126
    }
127

128
    /**
129
     * Transforms a collection of resources using $this->transform() on each item.
130
     *
131
     * If the request's 'fields' query variable is set, only those fields will be included
132
     * in the transformed output.
133
     */
134
    public function transformMany(array $resources): array
135
    {
136
        return array_map($this->transform(...), $resources);
9✔
137
    }
138

139
    /**
140
     * Define which fields can be requested via the 'fields' query parameter.
141
     * Override in child classes to restrict available fields.
142
     * Return null to allow all fields from toArray().
143
     *
144
     * @return list<string>|null
145
     */
146
    protected function getAllowedFields(): ?array
147
    {
148
        return null;
12✔
149
    }
150

151
    /**
152
     * Define which related resources can be included via the 'include' query parameter.
153
     * Override in child classes to restrict available includes.
154
     * Return null to allow all includes that have corresponding methods.
155
     * Return an empty array to disable all includes.
156
     *
157
     * @return list<string>|null
158
     */
159
    protected function getAllowedIncludes(): ?array
160
    {
161
        return null;
16✔
162
    }
163

164
    /**
165
     * Limits the given data array to only the fields specified
166
     *
167
     * @param array<string, mixed> $data
168
     *
169
     * @return array<string, mixed>
170
     *
171
     * @throws InvalidArgumentException
172
     */
173
    private function limitFields(array $data): array
174
    {
175
        if ($this->fields === null || $this->fields === []) {
36✔
176
            return $data;
27✔
177
        }
178

179
        $allowedFields = $this->getAllowedFields();
14✔
180

181
        // If whitelist is defined, validate against it
182
        if ($allowedFields !== null) {
14✔
183
            $invalidFields = array_diff($this->fields, $allowedFields);
2✔
184

185
            if ($invalidFields !== []) {
2✔
186
                throw ApiException::forInvalidFields(implode(', ', $invalidFields));
1✔
187
            }
188
        }
189

190
        return array_intersect_key($data, array_flip($this->fields));
13✔
191
    }
192

193
    /**
194
     * Checks the request for 'include' query variable, and if present,
195
     * calls the corresponding include{Resource} methods to add related data.
196
     *
197
     * @param array<string, mixed> $data
198
     *
199
     * @return array<string, mixed>
200
     */
201
    private function insertIncludes(array $data): array
202
    {
203
        if ($this->includes === null) {
35✔
204
            return $data;
25✔
205
        }
206

207
        $allowedIncludes = $this->getAllowedIncludes();
17✔
208

209
        if ($allowedIncludes === []) {
17✔
210
            return $data; // No includes allowed
1✔
211
        }
212

213
        // If whitelist is defined, filter the requested includes
214
        if ($allowedIncludes !== null) {
16✔
UNCOV
215
            $invalidIncludes = array_diff($this->includes, $allowedIncludes);
×
216

UNCOV
217
            if ($invalidIncludes !== []) {
×
UNCOV
218
                throw ApiException::forInvalidIncludes(implode(', ', $invalidIncludes));
×
219
            }
220
        }
221

222
        self::$depth++;
16✔
223

224
        try {
225
            foreach ($this->includes as $include) {
16✔
226
                $method = 'include' . ucfirst($include);
16✔
227
                if (method_exists($this, $method)) {
16✔
228
                    $data[$include] = $this->{$method}();
13✔
229
                } else {
230
                    throw ApiException::forMissingInclude($include);
5✔
231
                }
232
            }
233
        } finally {
234
            self::$depth--;
16✔
235
        }
236

237
        return $data;
11✔
238
    }
239
}
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