| 
 | 1 | +<?php  | 
 | 2 | + | 
 | 3 | +declare(strict_types=1);  | 
 | 4 | + | 
 | 5 | +/**  | 
 | 6 | + * This file is part of CodeIgniter 4 framework.  | 
 | 7 | + *  | 
 | 8 | + * (c) CodeIgniter Foundation <[email protected]>  | 
 | 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 | +     * @var list<string>|null  | 
 | 59 | +     */  | 
 | 60 | +    private ?array $fields = null;  | 
 | 61 | + | 
 | 62 | +    /**  | 
 | 63 | +     * @var list<string>|null  | 
 | 64 | +     */  | 
 | 65 | +    private ?array $includes = null;  | 
 | 66 | + | 
 | 67 | +    protected mixed $resource = null;  | 
 | 68 | + | 
 | 69 | +    public function __construct(  | 
 | 70 | +        private ?IncomingRequest $request = null,  | 
 | 71 | +    ) {  | 
 | 72 | +        $this->request = $request ?? request();  | 
 | 73 | + | 
 | 74 | +        $fields       = $this->request->getGet('fields');  | 
 | 75 | +        $this->fields = is_string($fields)  | 
 | 76 | +            ? array_map('trim', explode(',', $fields))  | 
 | 77 | +            : $fields;  | 
 | 78 | + | 
 | 79 | +        $includes       = $this->request->getGet('include');  | 
 | 80 | +        $this->includes = is_string($includes)  | 
 | 81 | +            ? array_map('trim', explode(',', $includes))  | 
 | 82 | +            : $includes;  | 
 | 83 | +    }  | 
 | 84 | + | 
 | 85 | +    /**  | 
 | 86 | +     * Converts the resource to an array representation.  | 
 | 87 | +     * This is overridden by child classes to define the  | 
 | 88 | +     * API-safe resource representation.  | 
 | 89 | +     *  | 
 | 90 | +     * @param mixed $resource The resource being transformed  | 
 | 91 | +     */  | 
 | 92 | +    abstract public function toArray(mixed $resource): array;  | 
 | 93 | + | 
 | 94 | +    /**  | 
 | 95 | +     * Transforms the given resource into an array using  | 
 | 96 | +     * the $this->toArray().  | 
 | 97 | +     */  | 
 | 98 | +    public function transform(array|object|null $resource = null): array  | 
 | 99 | +    {  | 
 | 100 | +        // Store the resource so include methods can access it  | 
 | 101 | +        $this->resource = $resource;  | 
 | 102 | + | 
 | 103 | +        if ($resource === null) {  | 
 | 104 | +            $data = $this->toArray(null);  | 
 | 105 | +        } elseif (is_object($resource) && method_exists($resource, 'toArray')) {  | 
 | 106 | +            $data = $this->toArray($resource->toArray());  | 
 | 107 | +        } else {  | 
 | 108 | +            $data = $this->toArray((array) $resource);  | 
 | 109 | +        }  | 
 | 110 | + | 
 | 111 | +        $data = $this->limitFields($data);  | 
 | 112 | + | 
 | 113 | +        return $this->insertIncludes($data);  | 
 | 114 | +    }  | 
 | 115 | + | 
 | 116 | +    /**  | 
 | 117 | +     * Transforms a collection of resources using $this->transform() on each item.  | 
 | 118 | +     *  | 
 | 119 | +     * If the request's 'fields' query variable is set, only those fields will be included  | 
 | 120 | +     * in the transformed output.  | 
 | 121 | +     */  | 
 | 122 | +    public function transformMany(array $resources): array  | 
 | 123 | +    {  | 
 | 124 | +        return array_map(fn ($resource): array => $this->transform($resource), $resources);  | 
 | 125 | +    }  | 
 | 126 | + | 
 | 127 | +    /**  | 
 | 128 | +     * Define which fields can be requested via the 'fields' query parameter.  | 
 | 129 | +     * Override in child classes to restrict available fields.  | 
 | 130 | +     * Return null to allow all fields from toArray().  | 
 | 131 | +     *  | 
 | 132 | +     * @return list<string>|null  | 
 | 133 | +     */  | 
 | 134 | +    protected function getAllowedFields(): ?array  | 
 | 135 | +    {  | 
 | 136 | +        return null;  | 
 | 137 | +    }  | 
 | 138 | + | 
 | 139 | +    /**  | 
 | 140 | +     * Define which related resources can be included via the 'include' query parameter.  | 
 | 141 | +     * Override in child classes to restrict available includes.  | 
 | 142 | +     * Return null to allow all includes that have corresponding methods.  | 
 | 143 | +     * Return an empty array to disable all includes.  | 
 | 144 | +     *  | 
 | 145 | +     * @return list<string>|null  | 
 | 146 | +     */  | 
 | 147 | +    protected function getAllowedIncludes(): ?array  | 
 | 148 | +    {  | 
 | 149 | +        return null;  | 
 | 150 | +    }  | 
 | 151 | + | 
 | 152 | +    /**  | 
 | 153 | +     * Limits the given data array to only the fields specified  | 
 | 154 | +     *  | 
 | 155 | +     * @param array<string, mixed> $data  | 
 | 156 | +     *  | 
 | 157 | +     * @return array<string, mixed>  | 
 | 158 | +     *  | 
 | 159 | +     * @throws InvalidArgumentException  | 
 | 160 | +     */  | 
 | 161 | +    private function limitFields(array $data): array  | 
 | 162 | +    {  | 
 | 163 | +        if ($this->fields === null || $this->fields === []) {  | 
 | 164 | +            return $data;  | 
 | 165 | +        }  | 
 | 166 | + | 
 | 167 | +        $allowedFields = $this->getAllowedFields();  | 
 | 168 | + | 
 | 169 | +        // If whitelist is defined, validate against it  | 
 | 170 | +        if ($allowedFields !== null) {  | 
 | 171 | +            $invalidFields = array_diff($this->fields, $allowedFields);  | 
 | 172 | + | 
 | 173 | +            if ($invalidFields !== []) {  | 
 | 174 | +                throw ApiException::forInvalidFields(implode(', ', $invalidFields));  | 
 | 175 | +            }  | 
 | 176 | +        }  | 
 | 177 | + | 
 | 178 | +        return array_intersect_key($data, array_flip($this->fields));  | 
 | 179 | +    }  | 
 | 180 | + | 
 | 181 | +    /**  | 
 | 182 | +     * Checks the request for 'include' query variable, and if present,  | 
 | 183 | +     * calls the corresponding include{Resource} methods to add related data.  | 
 | 184 | +     *  | 
 | 185 | +     * @param array<string, mixed> $data  | 
 | 186 | +     *  | 
 | 187 | +     * @return array<string, mixed>  | 
 | 188 | +     */  | 
 | 189 | +    private function insertIncludes(array $data): array  | 
 | 190 | +    {  | 
 | 191 | +        if ($this->includes === null) {  | 
 | 192 | +            return $data;  | 
 | 193 | +        }  | 
 | 194 | + | 
 | 195 | +        $allowedIncludes = $this->getAllowedIncludes();  | 
 | 196 | + | 
 | 197 | +        if ($allowedIncludes === []) {  | 
 | 198 | +            return $data; // No includes allowed  | 
 | 199 | +        }  | 
 | 200 | + | 
 | 201 | +        // If whitelist is defined, filter the requested includes  | 
 | 202 | +        if ($allowedIncludes !== null) {  | 
 | 203 | +            $invalidIncludes = array_diff($this->includes, $allowedIncludes);  | 
 | 204 | + | 
 | 205 | +            if ($invalidIncludes !== []) {  | 
 | 206 | +                throw ApiException::forInvalidIncludes(implode(', ', $invalidIncludes));  | 
 | 207 | +            }  | 
 | 208 | +        }  | 
 | 209 | + | 
 | 210 | +        foreach ($this->includes as $include) {  | 
 | 211 | +            $method = 'include' . ucfirst($include);  | 
 | 212 | +            if (method_exists($this, $method)) {  | 
 | 213 | +                $data[$include] = $this->{$method}();  | 
 | 214 | +            } else {  | 
 | 215 | +                throw ApiException::forMissingInclude($include);  | 
 | 216 | +            }  | 
 | 217 | +        }  | 
 | 218 | + | 
 | 219 | +        return $data;  | 
 | 220 | +    }  | 
 | 221 | +}  | 
0 commit comments