" (the order) and in the wp_podsrel table. The IDs * travel as tokens; after a write, Pods' own API writes meta and wp_podsrel again. * * Structure: pods, groups and fields are posts of the types _pods_pod, _pods_group * and _pods_field. They travel as layouts unless switched off in the settings; a * field removed on staging moves to the trash on live, its data stays. */ class Pods { /** * Post types that hold the Pods structure. */ const STRUCTURE = array( '_pods_pod', '_pods_group', '_pods_field' ); /** * Meta keys of the structure that hold IDs of other structure posts. */ const STRUCTURE_REFS = array( 'group', 'parent', 'sister_id' ); /** * Option: '0' keeps the Pods structure out of transfers. */ const OPTION = 'wonderful_sync_pods_structure'; /** * Reference fields per pod (request cache). * * @var array */ private static $fields = array(); /** * Whether Pods runs on this site. */ public static function active(): bool { return function_exists( 'pods_api' ); } /** * Whether the Pods structure travels. */ public static function structure_enabled(): bool { return '0' !== (string) get_option( self::OPTION, '1' ); } /** * Relationship and file fields of the pod behind a post type or taxonomy. * * @param string $object 'post' or 'term'. * @param string $subtype Post type or taxonomy. * @return array Field name => what its IDs are. */ public static function ref_fields( string $object, string $subtype ): array { $cache = $object . ':' . $subtype; if ( isset( self::$fields[ $cache ] ) ) { return self::$fields[ $cache ]; } self::$fields[ $cache ] = array(); if ( ! self::active() || '' === $subtype || in_array( $subtype, self::STRUCTURE, true ) ) { return array(); } try { $pod = pods_api()->load_pod( array( 'name' => $subtype ), false ); } catch ( \Throwable $e ) { return array(); } $types = 'post' === $object ? array( 'post_type', 'media' ) : array( 'taxonomy' ); if ( empty( $pod ) || ! in_array( (string) ( $pod['type'] ?? '' ), $types, true ) || 'meta' !== (string) ( $pod['storage'] ?? 'meta' ) ) { return array(); } foreach ( (array) ( $pod['fields'] ?? array() ) as $name => $field ) { $kind = self::field_kind( $field ); if ( '' !== $kind ) { self::$fields[ $cache ][ (string) ( $field['name'] ?? $name ) ] = array( 'kind' => $kind, 'field' => $field, 'pod' => $pod, ); } } return self::$fields[ $cache ]; } /** * What the IDs of a field point to: 'post', 'term' or '' (no IDs, or users). * * @param array|\ArrayAccess $field Pods field. */ private static function field_kind( $field ): string { $type = (string) ( $field['type'] ?? '' ); if ( 'file' === $type ) { return 'post'; } if ( 'pick' !== $type ) { return ''; } $object = (string) ( $field['pick_object'] ?? '' ); if ( in_array( $object, array( 'post_type', 'media' ), true ) ) { return 'post'; } if ( in_array( $object, array( 'taxonomy', 'nav_menu' ), true ) ) { return 'term'; } // Pods of type post_type or taxonomy are named by "pod" with the pod in pick_val. if ( 'pod' === $object ) { try { $related = pods_api()->load_pod( array( 'name' => (string) ( $field['pick_val'] ?? '' ) ), false ); } catch ( \Throwable $e ) { return ''; } $related_type = (string) ( $related['type'] ?? '' ); if ( 'meta' === (string) ( $related['storage'] ?? 'meta' ) ) { return in_array( $related_type, array( 'post_type', 'media' ), true ) ? 'post' : ( 'taxonomy' === $related_type ? 'term' : '' ); } } return ''; } /** * Lets Pods write meta and wp_podsrel of the reference fields again from the IDs that * were just written ("_pods_" keeps their order). Pods' API only adds and * updates rows; relationships that are gone are deleted here, on both sides of a * two-way field. * * @param string $object 'post' or 'term'. * @param int $id Object ID. * @param string $subtype Post type or taxonomy. * @return string[] Fields whose relationship table could not be updated. */ public static function sync_relationships( string $object, int $id, string $subtype ): array { global $wpdb; $failed = array(); $table = $wpdb->prefix . 'podsrel'; foreach ( self::ref_fields( $object, $subtype ) as $name => $info ) { $ordered = get_metadata( $object, $id, '_pods_' . $name, true ); $ids = is_array( $ordered ) && $ordered ? $ordered : get_metadata( $object, $id, $name, false ); $ids = array_values( array_unique( array_filter( array_map( 'intval', (array) $ids ) ) ) ); $pod_id = (int) ( $info['pod']['id'] ?? 0 ); $fid = (int) ( $info['field']['id'] ?? 0 ); $rel = function_exists( 'pods_podsrel_enabled' ) ? pods_podsrel_enabled( $info['field'], 'save' ) : false; $old = $rel ? array_map( 'intval', (array) $wpdb->get_col( $wpdb->prepare( 'SELECT related_item_id FROM %i WHERE pod_id = %d AND field_id = %d AND item_id = %d', $table, $pod_id, $fid, $id ) ) ) : array(); // phpcs:ignore WordPress.DB.DirectDatabaseQuery -- Pods' own table, read right before it is rewritten. try { pods_api()->save_relationships( $id, $ids, $info['pod'], $info['field'] ); $gone = array_diff( $old, $ids ); if ( $gone ) { $gone = array_values( array_map( 'intval', $gone ) ); $marks = implode( ',', array_fill( 0, count( $gone ), '%d' ) ); $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE pod_id = %d AND field_id = %d AND item_id = %d AND related_item_id IN ({$marks})", array_merge( array( $table, $pod_id, $fid, $id ), $gone ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- one %d per ID. $sister = (int) ( $info['field']['sister_id'] ?? 0 ); if ( $sister ) { $wpdb->query( $wpdb->prepare( "DELETE FROM %i WHERE field_id = %d AND related_item_id = %d AND item_id IN ({$marks})", array_merge( array( $table, $sister, $id ), $gone ) ) ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery, WordPress.DB.PreparedSQL.InterpolatedNotPrepared, WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber -- one %d per ID. } } } catch ( \Throwable $e ) { $failed[] = $name; } } return $failed; } /** * Pods caches its structure; after a transfer it reads it again. */ public static function flush(): void { self::$fields = array(); if ( self::active() ) { try { pods_api()->cache_flush_pods( null, false ); // New pods may bring post types with their own permalinks: rebuilt on demand. delete_option( 'rewrite_rules' ); } catch ( \Throwable $e ) { return; } } } }