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

timber / timber / 22903047261

10 Mar 2026 12:44PM UTC coverage: 89.661%. First build
22903047261

Pull #3210

travis-ci

web-flow
Merge 1958a1880 into 9a7d41124
Pull Request #3210: feat: Add ACF option field retrieval methods with transformations

11 of 13 new or added lines in 1 file covered. (84.62%)

4605 of 5136 relevant lines covered (89.66%)

63.8 hits per line

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

79.61
/src/Integration/AcfIntegration.php
1
<?php
2

3
/**
4
 * Integration with Advanced Custom Fields (ACF)
5
 *
6
 * @package Timber
7
 */
8

9
namespace Timber\Integration;
10

11
use ACF;
12
use acf_field;
13
use DateTimeImmutable;
14
use Timber\Post;
15
use Timber\Term;
16
use Timber\Timber;
17
use Timber\User;
18

19
/**
20
 * Class used to handle integration with Advanced Custom Fields
21
 */
22
class AcfIntegration implements IntegrationInterface
23
{
24
    public function should_init(): bool
×
25
    {
26
        return \class_exists(ACF::class);
×
27
    }
28

29
    public function init(): void
×
30
    {
31
        \add_filter('timber/post/pre_meta', [self::class, 'post_get_meta_field'], 10, 5);
×
32
        \add_filter('timber/post/meta_object_field', [self::class, 'post_meta_object'], 10, 3);
×
33
        \add_filter('timber/term/pre_meta', [self::class, 'term_get_meta_field'], 10, 5);
×
34
        \add_filter('timber/user/pre_meta', [self::class, 'user_get_meta_field'], 10, 5);
×
35

36
        /**
37
         * Allowed a user to set a meta value
38
         *
39
         * @deprecated 2.0.0 with no replacement
40
         */
41
        \add_filter('timber/term/meta/set', [self::class, 'term_set_meta'], 10, 4);
×
42
    }
43

44
    /**
45
     * Gets meta value for a post through ACF’s API.
46
     *
47
     * @param string       $value      The field value. Default null.
48
     * @param int          $post_id    The post ID.
49
     * @param string       $field_name The name of the meta field to get the value for.
50
     * @param Post $post       The post object.
51
     * @param array        $args       An array of arguments.
52
     * @return mixed|false
53
     */
54
    public static function post_get_meta_field($value, $post_id, $field_name, $post, $args)
20✔
55
    {
56
        return self::get_meta($value, $post_id, $field_name, $args);
20✔
57
    }
58

59
    public static function post_meta_object($value, $post_id, $field_name)
1✔
60
    {
61
        return \get_field_object($field_name, $post_id);
1✔
62
    }
63

64
    /**
65
     * Gets meta value for a term through ACF’s API.
66
     *
67
     * @param string       $value      The field value. Default null.
68
     * @param int          $term_id    The term ID.
69
     * @param string       $field_name The name of the meta field to get the value for.
70
     * @param Term $term       The term object.
71
     * @param array        $args       An array of arguments.
72
     * @return mixed|false
73
     */
74
    public static function term_get_meta_field($value, $term_id, $field_name, $term, $args)
5✔
75
    {
76
        return self::get_meta($value, $term->taxonomy . '_' . $term_id, $field_name, $args);
5✔
77
    }
78

79
    /**
80
     * @deprecated 2.0.0, with no replacement
81
     *
82
     * @return mixed
83
     */
84
    public static function term_set_meta($value, $field, $term_id, $term)
×
85
    {
86
        $searcher = $term->taxonomy . '_' . $term->ID;
×
87
        \update_field($field, $value, $searcher);
×
88
        return $value;
×
89
    }
90

91
    /**
92
     * Gets meta value for a user through ACF’s API.
93
     *
94
     * @param string       $value      The field value. Default null.
95
     * @param int          $user_id    The user ID.
96
     * @param string       $field_name The name of the meta field to get the value for.
97
     * @param User $user       The user object.
98
     * @param array        $args       An array of arguments.
99
     * @return mixed|false
100
     */
101
    public static function user_get_meta_field($value, $user_id, $field_name, $user, $args)
1✔
102
    {
103
        return self::get_meta($value, 'user_' . $user_id, $field_name, $args);
1✔
104
    }
105

106
    /**
107
     * Transform ACF file field
108
     *
109
     * @param string $value
110
     * @param int    $id
111
     * @param array  $field
112
     */
113
    public static function transform_file($value, $id, $field)
1✔
114
    {
115
        if (empty($value)) {
1✔
116
            return false;
1✔
117
        }
118
        return Timber::get_attachment($value);
×
119
    }
120

121
    /**
122
     * Transform ACF image field
123
     *
124
     * @param string $value
125
     * @param int    $id
126
     * @param array  $field
127
     */
128
    public static function transform_image($value, $id, $field)
3✔
129
    {
130
        if (empty($value)) {
3✔
131
            return false;
1✔
132
        }
133
        return Timber::get_image($value);
2✔
134
    }
135

136
    /**
137
     * Transform ACF gallery field
138
     *
139
     * @param array $value
140
     * @param int   $id
141
     * @param array $field
142
     */
143
    public static function transform_gallery($value, $id, $field)
1✔
144
    {
145
        if (empty($value)) {
1✔
146
            return false;
×
147
        }
148
        return Timber::get_posts($value);
1✔
149
    }
150

151
    /**
152
     * Transform ACF date picker field
153
     *
154
     * @param string $value
155
     * @param int    $id
156
     * @param array  $field
157
     */
158
    public static function transform_date_picker($value, $id, $field)
2✔
159
    {
160
        if (!$value) {
2✔
161
            return $value;
×
162
        }
163
        return new DateTimeImmutable(\acf_format_date($value, 'Y-m-d H:i:s'), \wp_timezone());
2✔
164
    }
165

166
    /**
167
     * Transform ACF post object field
168
     *
169
     * @param string $value
170
     * @param int    $id
171
     * @param array  $field
172
     */
173
    public static function transform_post_object($value, $id, $field)
2✔
174
    {
175
        if (empty($value)) {
2✔
176
            return false;
×
177
        }
178
        if (!$field['multiple']) {
2✔
179
            return Timber::get_post($value);
1✔
180
        }
181
        return Timber::get_posts($value);
1✔
182
    }
183

184
    /**
185
     * Transform ACF relationship field
186
     *
187
     * @param string $value
188
     * @param int    $id
189
     * @param array  $field
190
     */
191
    public static function transform_relationship($value, $id, $field)
1✔
192
    {
193
        if (empty($value)) {
1✔
194
            return false;
×
195
        }
196
        return Timber::get_posts($value);
1✔
197
    }
198

199
    /**
200
     * Transform ACF taxonomy field
201
     *
202
     * @param string $value
203
     * @param int    $id
204
     * @param array  $field
205
     */
206
    public static function transform_taxonomy($value, $id, $field)
2✔
207
    {
208
        if (empty($value)) {
2✔
209
            return false;
×
210
        }
211
        if ($field['field_type'] === 'select' || $field['field_type'] === 'radio') {
2✔
212
            return Timber::get_term((int) $value);
1✔
213
        }
214
        return Timber::get_terms((array) $value);
1✔
215
    }
216

217
    /**
218
     * Transform ACF user field
219
     *
220
     * @param string $value
221
     * @param int    $id
222
     * @param array  $field
223
     */
224
    public static function transform_user($value, $id, $field)
2✔
225
    {
226
        if (empty($value)) {
2✔
227
            return false;
×
228
        }
229
        if (!$field['multiple']) {
2✔
230
            return Timber::get_user((int) $value);
1✔
231
        }
232
        return Timber::get_users((array) $value);
1✔
233
    }
234

235
    /**
236
     * Gets a single ACF option field with field transformations applied.
237
     *
238
     * Retrieves a field stored on an ACF option page and automatically applies Timber's field
239
     * transformations, converting raw values into Timber objects (e.g. Image, Post, Term).
240
     *
241
     * Example usage:
242
     *
243
     * ```php
244
     * $gallery = Timber\Integration\AcfIntegration::get_option('my_gallery_field');
245
     * ```
246
     *
247
     * @param string $field_name The name of the option field to retrieve.
248
     * @return mixed The transformed field value.
249
     */
250
    public static function get_option(string $field_name): mixed
1✔
251
    {
252
        return self::with_timber_transforms(static fn() => \get_field($field_name, 'options', true));
1✔
253
    }
254

255
    /**
256
     * Gets all ACF option fields with field transformations applied.
257
     *
258
     * Retrieves all fields stored on ACF option pages and automatically applies Timber's field
259
     * transformations, converting raw values into Timber objects (e.g. Image, Post, Term).
260
     *
261
     * Example usage:
262
     *
263
     * ```php
264
     * $options = Timber\Integration\AcfIntegration::get_options();
265
     * ```
266
     *
267
     * @return array An associative array of all transformed option fields.
268
     */
NEW
269
    public static function get_options(): array
×
270
    {
NEW
271
        return self::with_timber_transforms(static fn() => \get_fields('options') ?: []);
×
272
    }
273

274
    /**
275
     * Gets meta value through ACF's API.
276
     *
277
     * @param string     $value
278
     * @param int|string $id
279
     * @param string     $field_name
280
     * @param array      $args
281
     * @return mixed|false
282
     */
283
    private static function get_meta($value, $id, $field_name, $args)
26✔
284
    {
285
        $args = \wp_parse_args($args, [
26✔
286
            'format_value' => true,
26✔
287
            'transform_value' => false,
26✔
288
        ]);
26✔
289

290
        if (!$args['transform_value']) {
26✔
291
            return \get_field($field_name, $id, $args['format_value']);
14✔
292
        }
293

294
        return self::with_timber_transforms(static fn() => \get_field($field_name, $id, true));
12✔
295
    }
296

297
    /**
298
     * Temporarily replaces ACF's format_value filters with Timber's transform filters, executes a
299
     * callback, then restores the original ACF filters.
300
     *
301
     * We use acf_get_field_type() instead of acf()->fields->get_field_type(), because of some
302
     * function stub issues in the php-stubs/acf-pro-stubs package.
303
     *
304
     * @see https://github.com/timber/timber/pull/2630
305
     *
306
     * @param callable $callback The callback to execute with Timber's transforms active.
307
     * @return mixed The result of the callback.
308
     */
309
    private static function with_timber_transforms(callable $callback): mixed
13✔
310
    {
311
        $field_types = \array_filter([
13✔
312
            'file' => \acf_get_field_type('file'),
13✔
313
            'image' => \acf_get_field_type('image'),
13✔
314
            'gallery' => \acf_get_field_type('gallery'),
13✔
315
            'date_picker' => \acf_get_field_type('date_picker'),
13✔
316
            'date_time_picker' => \acf_get_field_type('date_time_picker'),
13✔
317
            'post_object' => \acf_get_field_type('post_object'),
13✔
318
            'relationship' => \acf_get_field_type('relationship'),
13✔
319
            'taxonomy' => \acf_get_field_type('taxonomy'),
13✔
320
            'user' => \acf_get_field_type('user'),
13✔
321
        ], static fn($field_type): bool => $field_type instanceof acf_field);
13✔
322

323
        $timber_transforms = [
13✔
324
            'file' => [self::class, 'transform_file'],
13✔
325
            'image' => [self::class, 'transform_image'],
13✔
326
            'gallery' => [self::class, 'transform_gallery'],
13✔
327
            'date_picker' => [self::class, 'transform_date_picker'],
13✔
328
            'date_time_picker' => [self::class, 'transform_date_picker'],
13✔
329
            'post_object' => [self::class, 'transform_post_object'],
13✔
330
            'relationship' => [self::class, 'transform_relationship'],
13✔
331
            'taxonomy' => [self::class, 'transform_taxonomy'],
13✔
332
            'user' => [self::class, 'transform_user'],
13✔
333
        ];
13✔
334

335
        // Remove ACF's format_value filters for known field types (only if the field class exists).
336
        foreach ($field_types as $type => $field_type) {
13✔
337
            \remove_filter("acf/format_value/type={$type}", [$field_type, 'format_value']);
13✔
338
        }
339

340
        // Always add Timber's transform filters, even for field types without a registered ACF
341
        // field class (e.g. gallery in ACF Free where it is a PRO-only field type).
342
        foreach ($timber_transforms as $type => $callback_fn) {
13✔
343
            \add_filter("acf/format_value/type={$type}", $callback_fn, 10, 3);
13✔
344
        }
345

346
        $result = $callback();
13✔
347

348
        // Remove Timber's transform filters and restore ACF's format_value filters.
349
        foreach ($timber_transforms as $type => $callback_fn) {
13✔
350
            \remove_filter("acf/format_value/type={$type}", $callback_fn);
13✔
351
        }
352
        foreach ($field_types as $type => $field_type) {
13✔
353
            \add_filter("acf/format_value/type={$type}", [$field_type, 'format_value'], 10, 3);
13✔
354
        }
355

356
        return $result;
13✔
357
    }
358
}
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