PluginProbe ʕ •ᴥ•ʔ
JetFormBuilder — Dynamic Blocks Form Builder / 3.6.4.1
JetFormBuilder — Dynamic Blocks Form Builder v3.6.4.1
3.6.4.1 3.6.4 3.6.3.1 3.6.3 3.6.2.2 3.6.2.1 3.6.2 3.6.1.1 3.6.1 3.6.0.1 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.1.6 1.1.7 1.2.0 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.3.0 1.3.1 1.3.2 1.3.3 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.5.5 2.0.0 2.0.1 2.0.2 2.0.3 2.0.4 2.0.5 2.0.6 2.1.0 2.1.1 2.1.10 2.1.11 2.1.2 2.1.3 2.1.4 2.1.5 2.1.6 2.1.7 2.1.8 2.1.9 3.0.0 3.0.0.1 3.0.0.2 3.0.0.3 3.0.1 3.0.1.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.0.1 3.1.1 3.1.2 3.1.3 3.1.4 3.1.5 3.1.6 3.1.7 3.1.8 3.1.9 3.2.0 3.2.1 3.2.2 3.2.3 3.3.0 3.3.1 3.3.2 3.3.3 3.3.3.1 3.3.4 3.3.4.1 3.3.4.2 3.4.0 3.4.1 3.4.2 3.4.3 3.4.4 3.4.5 3.4.5.1 3.4.5.2 3.4.6 3.4.7 3.4.7.1 3.5.0 3.5.1 3.5.1.1 3.5.1.2 3.5.2 3.5.2.1 3.5.3 3.5.4 3.5.5 3.5.6 3.5.6.1 3.5.6.2 3.5.6.3 3.6.0
jetformbuilder / includes / generators / get-related-posts.php
jetformbuilder / includes / generators Last commit date
base-v2.php 3 months ago base.php 2 years ago get-from-db.php 3 weeks ago get-from-rest-api.php 3 months ago get-from-users.php 3 weeks ago get-related-posts.php 3 months ago legacy-parser.php 3 months ago num-range-manual.php 3 months ago num-range.php 3 months ago registry.php 3 weeks ago
get-related-posts.php
493 lines
1 <?php
2 /**
3 * Related Posts Generator.
4 *
5 * Generates a list of posts related to a value from another field.
6 * Supports meta field, post_parent, and taxonomy-based relations.
7 * Designed for cascading select scenarios (e.g. Project → Tasks).
8 *
9 * @package Jet_Form_Builder\Generators
10 */
11
12 namespace Jet_Form_Builder\Generators;
13
14 // If this file is called directly, abort.
15 if ( ! defined( 'WPINC' ) ) {
16 die;
17 }
18
19 /**
20 * Get_Related_Posts class.
21 *
22 * Generates options from posts related to the value of another field.
23 */
24 class Get_Related_Posts extends Base_V2 {
25
26 /**
27 * Returns generator ID.
28 *
29 * @return string
30 */
31 public function get_id() {
32 return 'get_related_posts';
33 }
34
35 /**
36 * Returns generator name.
37 *
38 * @return string
39 */
40 public function get_name() {
41 return __( 'Get related posts list', 'jet-form-builder' );
42 }
43
44 /**
45 * Returns structured settings schema.
46 *
47 * @return array
48 */
49 public function get_settings_schema(): array {
50 return array(
51 'post_type' => array(
52 'type' => 'string',
53 'default' => 'post',
54 'label' => __( 'Post Type', 'jet-form-builder' ),
55 'control' => 'select',
56 'options' => $this->get_post_types_options(),
57 'help' => __( 'The type of posts to show as options in this field.', 'jet-form-builder' ),
58 ),
59 'relation_type' => array(
60 'type' => 'string',
61 'default' => 'meta_field',
62 'label' => __( 'Filter By', 'jet-form-builder' ),
63 'control' => 'select',
64 'options' => array(
65 array( 'value' => 'meta_field', 'label' => __( 'Match by meta key', 'jet-form-builder' ) ),
66 array( 'value' => 'post_parent', 'label' => __( 'Match by WordPress parent post', 'jet-form-builder' ) ),
67 array( 'value' => 'taxonomy', 'label' => __( 'Match by taxonomy term', 'jet-form-builder' ) ),
68 ),
69 'help' => __( 'How the listed posts are connected to the value from the watched field.', 'jet-form-builder' ),
70 ),
71 'relation_key' => array(
72 'type' => 'string',
73 'default' => '',
74 'label' => __( 'Relation Key', 'jet-form-builder' ),
75 'control' => 'text',
76 'placeholder' => 'related_project_id',
77 'help' => __( 'Enter the custom field name for "Match by meta key" or the taxonomy slug for "Match by taxonomy term".', 'jet-form-builder' ),
78 'condition' => array(
79 'relation_type!' => 'post_parent',
80 ),
81 ),
82 'relation_key_value' => array(
83 'type' => 'string',
84 'default' => '',
85 'label' => __( 'Filter Value', 'jet-form-builder' ),
86 'control' => 'text',
87 'placeholder' => '42',
88 'help' => __( 'Static value used when the Trigger Field is empty. To use multiple values, enter them as a comma-separated list, for example: `12,34,56`.', 'jet-form-builder' ),
89 'condition' => array(
90 'relation_type!' => 'post_parent',
91 ),
92 ),
93 'parent_post_id' => array(
94 'type' => 'string',
95 'default' => '',
96 'label' => __( 'Parent Post ID', 'jet-form-builder' ),
97 'control' => 'text',
98 'help' => __( 'Static or dynamic parent post ID used to load child posts when auto-update is not active. Enter `current_post_id` to load child posts of the current post. To load child posts from several parents, enter comma-separated IDs, for example: `12,34,56`.', 'jet-form-builder' ),
99 'condition' => array(
100 'relation_type' => 'post_parent',
101 ),
102 ),
103 'value_field' => array(
104 'type' => 'string',
105 'default' => 'ID',
106 'label' => __( 'Option Value', 'jet-form-builder' ),
107 'control' => 'select',
108 'options' => array(
109 array( 'value' => 'ID', 'label' => 'Post ID' ),
110 array( 'value' => 'post_title', 'label' => 'Post Title' ),
111 array( 'value' => 'post_name', 'label' => 'Post Slug' ),
112 ),
113 ),
114 'label_field' => array(
115 'type' => 'string',
116 'default' => 'post_title',
117 'label' => __( 'Option Label', 'jet-form-builder' ),
118 'control' => 'select',
119 'options' => array(
120 array( 'value' => 'post_title', 'label' => 'Post Title' ),
121 array( 'value' => 'post_name', 'label' => 'Post Slug' ),
122 array( 'value' => 'post_excerpt', 'label' => 'Post Excerpt' ),
123 ),
124 ),
125 'posts_per_page' => array(
126 'type' => 'number',
127 'default' => -1,
128 'label' => __( 'Max Items', 'jet-form-builder' ),
129 'control' => 'number',
130 'min' => -1,
131 'help' => __( 'Maximum number of options to show. Set to -1 to show all.', 'jet-form-builder' ),
132 ),
133 'orderby' => array(
134 'type' => 'string',
135 'default' => 'title',
136 'label' => __( 'Order By', 'jet-form-builder' ),
137 'control' => 'select',
138 'options' => array(
139 array( 'value' => 'title', 'label' => 'Title' ),
140 array( 'value' => 'date', 'label' => 'Date' ),
141 array( 'value' => 'ID', 'label' => 'ID' ),
142 array( 'value' => 'menu_order', 'label' => 'Menu Order' ),
143 ),
144 ),
145 );
146 }
147
148 /**
149 * Get public post types as select options.
150 *
151 * @return array
152 */
153 private function get_post_types_options(): array {
154 $options = array();
155
156 foreach ( get_post_types( array(), 'objects' ) as $post_type ) {
157 $options[] = array(
158 'value' => $post_type->name,
159 'label' => $post_type->label,
160 );
161 }
162
163 return $options;
164 }
165
166 /**
167 * Whether this generator supports auto-update.
168 *
169 * @return bool
170 */
171 public function supports_auto_update(): bool {
172 return true;
173 }
174
175 /**
176 * Returns context field descriptions for auto-update.
177 *
178 * @return array
179 */
180 public function get_auto_update_context_fields(): array {
181 return array(
182 array(
183 'single' => true,
184 'description' => __( 'The Trigger Field value overrides the static filter above. If the Trigger Field is empty, the static filter is used when configured. Otherwise, this list will be empty.', 'jet-form-builder' ),
185 'example' => __( 'Select the field that controls this list. Single-value and multi-value trigger fields are supported.', 'jet-form-builder' ),
186 ),
187 );
188 }
189
190 /**
191 * Returns the auto-update value type supported by this generator.
192 *
193 * @return string
194 */
195 public function get_auto_update_value_type(): string {
196 return 'scalar_or_array';
197 }
198
199 /**
200 * Empty trigger may fall back to static filter settings when configured.
201 *
202 * @return string
203 */
204 public function get_auto_update_empty_context_policy(): string {
205 return 'fallback_to_static_if_configured';
206 }
207
208 /**
209 * Whether current related-posts settings provide a valid static fallback.
210 *
211 * @param array $settings Parsed generator settings.
212 *
213 * @return bool
214 */
215 public function has_auto_update_static_fallback( array $settings ): bool {
216 $relation_type = $settings['relation_type'] ?? 'meta_field';
217 $filter_value = $this->normalize_source_values( $this->get_static_source_value( $settings ) );
218
219 if ( 'post_parent' === $relation_type ) {
220 return ! empty( $filter_value );
221 }
222
223 $relation_key = trim( (string) ( $settings['relation_key'] ?? '' ) );
224
225 return '' !== $relation_key && ! empty( $filter_value );
226 }
227
228 /**
229 * Generate with context from dependent fields.
230 *
231 * @param array $settings Parsed settings.
232 * @param array $context ['field_name' => 'value'] from listened fields.
233 *
234 * @return array
235 */
236 public function generate_with_context( array $settings, array $context = array() ): array {
237 if ( empty( $context ) ) {
238 if ( $this->has_auto_update_static_fallback( $settings ) ) {
239 $settings['_related_value'] = $this->get_static_source_value( $settings );
240 return $this->generate( $settings );
241 }
242
243 return array();
244 }
245
246 // Take the first context value as the relation source.
247 $settings['_related_value'] = reset( $context );
248 $source_values = $this->normalize_source_values(
249 $settings['_related_value']
250 );
251
252 if ( empty( $source_values ) ) {
253 if ( $this->has_auto_update_static_fallback( $settings ) ) {
254 $settings['_related_value'] = $this->get_static_source_value( $settings );
255 return $this->generate( $settings );
256 }
257
258 return array();
259 }
260
261 return $this->generate( $settings );
262 }
263
264 /**
265 * Generates options from related posts.
266 *
267 * @param array $args Settings from schema + optional _related_value.
268 *
269 * @return array
270 */
271 public function generate( $args ) {
272 $post_type = sanitize_key( $args['post_type'] ?? 'post' );
273 $relation_type = $args['relation_type'] ?? 'meta_field';
274 $raw_source_value = $args['_related_value'] ?? (
275 'post_parent' === $relation_type
276 ? ( $args['parent_post_id'] ?? null )
277 : ( $args['relation_key_value'] ?? null )
278 );
279
280 // _related_value comes from auto-update context.
281 // Static fallback depends on the selected relation type.
282 $source_values = $this->normalize_source_values( $raw_source_value );
283 $source_value = $source_values[0] ?? null;
284
285 // If the selected relation cannot be resolved to a concrete source value,
286 // return nothing instead of falling back to an unfiltered posts query.
287 if ( empty( $source_values ) ) {
288 if ( 'post_parent' === $relation_type ) {
289 return array();
290 }
291
292 $relation_key = $args['relation_key'] ?? '';
293 if ( $relation_key ) {
294 return array();
295 }
296 }
297
298 $query_args = array(
299 'post_type' => $post_type,
300 'posts_per_page' => intval( $args['posts_per_page'] ?? -1 ),
301 'post_status' => 'publish',
302 'orderby' => $args['orderby'] ?? 'title',
303 'order' => 'ASC',
304 );
305
306 // Apply relation filter when a source value is present (direct call without context returns all posts).
307 if ( ! empty( $source_values ) ) {
308 switch ( $relation_type ) {
309 case 'meta_field':
310 $relation_key = $args['relation_key'] ?? '';
311 if ( $relation_key ) {
312 $query_args['meta_query'] = array(
313 array(
314 'key' => $relation_key,
315 'value' => count( $source_values ) > 1 ? $source_values : $source_value,
316 'compare' => count( $source_values ) > 1 ? 'IN' : '=',
317 ),
318 );
319 }
320 break;
321
322 case 'post_parent':
323 if ( null !== $raw_source_value && empty( $source_values ) ) {
324 return array();
325 }
326
327 $parent_ids = array_values(
328 array_filter(
329 array_map( 'intval', $source_values )
330 )
331 );
332
333 if ( empty( $parent_ids ) ) {
334 return array();
335 }
336
337 if ( 1 === count( $parent_ids ) ) {
338 $query_args['post_parent'] = $parent_ids[0];
339 } else {
340 $query_args['post_parent__in'] = $parent_ids;
341 }
342 break;
343
344 case 'taxonomy':
345 $taxonomy = $args['relation_key'] ?? 'category';
346 $term_ids = array_values(
347 array_filter(
348 array_map( 'intval', $source_values )
349 )
350 );
351
352 if ( empty( $term_ids ) ) {
353 return array();
354 }
355
356 $query_args['tax_query'] = array(
357 array(
358 'taxonomy' => $taxonomy,
359 'field' => 'term_id',
360 'terms' => 1 === count( $term_ids ) ? $term_ids[0] : $term_ids,
361 ),
362 );
363 break;
364 }
365 }
366
367 $posts = get_posts( $query_args );
368 $value_field = $args['value_field'] ?? 'ID';
369 $label_field = $args['label_field'] ?? 'post_title';
370 $result = array();
371
372 foreach ( $posts as $post ) {
373 $result[] = array(
374 'value' => strval( $post->$value_field ),
375 'label' => strval( $post->$label_field ),
376 );
377 }
378
379 return $result;
380 }
381
382 /**
383 * Resolve dynamic/static source value before applying filters.
384 *
385 * @param mixed $value Raw source value from settings or context.
386 *
387 * @return string|null
388 */
389 private function resolve_source_value( $value ): ?string {
390 if ( null === $value ) {
391 return null;
392 }
393
394 if ( is_array( $value ) ) {
395 $value = reset( $value );
396 }
397
398 $value = trim( (string) $value );
399
400 if ( '' === $value ) {
401 return '';
402 }
403
404 return $this->resolve_special_source_alias( $value );
405 }
406
407 /**
408 * Normalize source value to a list of sanitized scalar values.
409 *
410 * @param mixed $value Raw source value from settings or context.
411 *
412 * @return array
413 */
414 private function normalize_source_values( $value ): array {
415 if ( null === $value ) {
416 return array();
417 }
418
419 if ( is_array( $value ) ) {
420 $values = $value;
421 } elseif ( is_string( $value ) && false !== strpos( $value, ',' ) ) {
422 $values = array_map( 'trim', explode( ',', $value ) );
423 } else {
424 $values = array( $value );
425 }
426
427 $values = array_map(
428 function ( $item ) {
429 return $this->resolve_source_value( $item );
430 },
431 $values
432 );
433
434 $values = array_filter(
435 $values,
436 static function ( $item ) {
437 return null !== $item && '' !== $item;
438 }
439 );
440
441 return array_values( array_unique( $values ) );
442 }
443
444 /**
445 * Resolve simple aliases for current post context.
446 *
447 * @param string $value Raw or unresolved source value.
448 *
449 * @return string
450 */
451 private function resolve_special_source_alias( string $value ): string {
452 if ( 'current_post_id' !== $value ) {
453 return $value;
454 }
455
456 $current_post_id = $this->get_current_post_id();
457
458 return $current_post_id ? (string) $current_post_id : '';
459 }
460
461 /**
462 * Get current post ID for frontend render and AJAX fallback.
463 *
464 * @return int
465 */
466 private function get_current_post_id(): int {
467 global $post;
468
469 if ( ! empty( $post->ID ) ) {
470 return absint( $post->ID );
471 }
472
473 if ( wp_doing_ajax() ) {
474 return absint( url_to_postid( wp_get_referer() ) );
475 }
476
477 return 0;
478 }
479
480 /**
481 * Get static fallback source value from generator settings.
482 *
483 * @param array $settings Parsed generator settings.
484 *
485 * @return mixed
486 */
487 private function get_static_source_value( array $settings ) {
488 return 'post_parent' === ( $settings['relation_type'] ?? 'meta_field' )
489 ? ( $settings['parent_post_id'] ?? null )
490 : ( $settings['relation_key_value'] ?? null );
491 }
492 }
493