Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
0.00% covered (danger)
0.00%
0 / 121
0.00% covered (danger)
0.00%
0 / 6
CRAP
n/a
0 / 0
wpcomsh_apply_headstart_terms
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
56
wpcomsh_headstart_log
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
wpcomsh_apply_headstart_custom_term_assignments
0.00% covered (danger)
0.00%
0 / 12
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_apply_headstart_custom_term_assignment
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
20
wpcomsh_apply_headstart_custom_terms
0.00% covered (danger)
0.00%
0 / 40
0.00% covered (danger)
0.00%
0 / 1
132
wpcomsh_apply_headstart_custom_term_meta
0.00% covered (danger)
0.00%
0 / 22
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2/**
3 * Functions to read WooCommerce terms and taxonomy information from
4 * a theme's headstart annotation, then apply them to the site.
5 * To be used after WooCommerce is installed.
6 *
7 * @package wpcomsh
8 */
9
10/**
11 * Main function for applying all term and taxonomy information from
12 * the current theme's headstart annotation.
13 *
14 * @return array $results An associative array. Key 'missing_taxonomies' has an array value: if there were terms in the annotation that could not be added because a taxonomy was missing on the site, the list of missing taxonomies. (An array of strings).
15 */
16function wpcomsh_apply_headstart_terms() {
17    $theme_to_headstart = get_option( 'woocommerce_should_run_headstart_for_theme', false );
18    $early_return_data  = array( 'missing_taxonomies' => array() );
19
20    if ( false === $theme_to_headstart ) {
21        return $early_return_data;
22    }
23
24    $theme      = wp_get_theme();
25    $theme_name = $theme->get_stylesheet();
26
27    if ( $theme_name !== $theme_to_headstart ) {
28        wpcomsh_headstart_log( "wpcomsh_apply_headstart_terms: Theme slugs don't match. Theme to Headstart: [$theme_to_headstart], Theme Slug: [$theme_name]." );
29
30        // Delete the option; the theme has changed.
31        delete_option( 'woocommerce_should_run_headstart_for_theme' );
32        return $early_return_data;
33    }
34
35    $locale          = get_locale();
36    $fallback_locale = 'en';
37
38    $annotation         = wpcomsh_headstart_get_annotation( $theme_name, $locale, $fallback_locale );
39    $missing_taxonomies = array();
40
41    if ( false === $annotation ) {
42        wpcomsh_headstart_log( "wpcomsh_apply_headstart_terms: Could not find the headstart annotation for theme [$theme_name]. locale=[$locale] fallback_locale=[$fallback_locale]" );
43
44        // Delete the option; we don't have a Headstart annotation for this theme.
45        delete_option( 'woocommerce_should_run_headstart_for_theme' );
46        return $missing_taxonomies;
47    }
48
49    if ( ! empty( $annotation['custom_terms_by_taxonomy'] ) ) {
50        wpcomsh_headstart_log( "wpcomsh_apply_headstart_terms: Found custom_terms_by_taxonomy for [$theme], applying." );
51        $custom_terms_return = wpcomsh_apply_headstart_custom_terms( $annotation['custom_terms_by_taxonomy'] );
52        $term_id_map         = $custom_terms_return['term_id_map'];
53        $missing_taxonomies  = $custom_terms_return['missing_taxonomies'];
54        wpcomsh_headstart_log( compact( 'term_id_map', 'missing_taxonomies' ) );
55        if ( ! empty( $annotation['custom_term_meta'] ) ) {
56            wpcomsh_headstart_log( "wpcomsh_apply_headstart_terms: Found custom_terms_meta for [$theme], applying." );
57            wpcomsh_apply_headstart_custom_term_meta( $annotation['custom_term_meta'], $term_id_map );
58        }
59        if ( ! empty( $annotation['custom_term_assignments'] ) ) {
60            wpcomsh_headstart_log( "wpcomsh_apply_headstart_terms: Found custom_term_assignments for [$theme], applying." );
61            wpcomsh_apply_headstart_custom_term_assignments( $annotation['custom_term_assignments'] );
62        }
63    } else {
64        wpcomsh_headstart_log( "wpcomsh_apply_headstart_terms: Found an annotation for [$theme], but not taking action since it did not contain custom_terms_by_taxonomy." );
65    }
66
67    delete_option( 'woocommerce_should_run_headstart_for_theme' );
68    return array( 'missing_taxonomies' => $missing_taxonomies );
69}
70
71/**
72 * Logging wrapper.
73 *
74 * @param mixed $input Anything you want to log.
75 */
76function wpcomsh_headstart_log( $input ) {
77    if ( is_wp_error( $input ) ) {
78        $input = $input->get_error_message();
79    } elseif ( ! is_string( $input ) ) {
80        // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
81        $input = print_r( $input, true );
82    }
83    // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
84    error_log( 'WPComSH: headstart: ' . $input );
85}
86
87/**
88 * Given a list of custom term assignments for several posts from a headstart annotation,
89 * try to locate posts on the live site matching the "old ids" in the assignment, then
90 * apply the terms listed.
91 *
92 * Old vs New:
93 * "old" post id: 1379   - This represents the "Red Shirt" post's ID in the annotation.
94 * "new" post id: 203482 - This represents the "Red Shirt" post's ID on the current site.
95 *
96 * @param array $custom_term_assignments An array where keys are "old" post IDs and the values
97 * are arrays. The inner arrays' keys are taxonomy names and values are arrays of term names.
98 * Example:
99 *  "81": {
100 *    "product_cat": [ "Men", "Women" ],
101 *    "product_tag": [ "New Arrivals" ],
102 *  },
103 *  "80": {
104 *    "product_cat": [ "Men", "Women" ],
105 *    "product_tag": [ "New Arrivals" ],
106 *  }.
107 **/
108function wpcomsh_apply_headstart_custom_term_assignments( $custom_term_assignments ) {
109    $filter   = array(
110        'fields'      => 'ids',
111        'nopaging'    => true,
112        'post_status' => 'publish',
113        'post_type'   => get_post_types(),
114    );
115    $post_ids = get_posts( $filter );
116
117    foreach ( $post_ids as $post_id ) {
118        $old_id = get_post_meta( $post_id, '_hs_old_id', true );
119        if ( empty( $old_id ) || empty( $custom_term_assignments[ $old_id ] ) ) {
120            continue;
121        }
122        wpcomsh_apply_headstart_custom_term_assignment( $post_id, $custom_term_assignments[ $old_id ] );
123    }
124}
125
126/**
127 * Given a single post id, and a list of custom term assignments for
128 * up to several taxonomies, add those terms assignments to the post.
129 *
130 * @param int   $post_id A single post id.
131 * @param array $assignments An array where keys are taxonomy names and values are arrays of term names.
132 *  Example: {
133 *   "product_cat": [ "Men", "Women" ],
134 *   "product_tag": [ "New Arrivals" ],
135 *  }.
136 */
137function wpcomsh_apply_headstart_custom_term_assignment( $post_id, $assignments ) {
138    $append = true; // Don't ever delete terms
139    foreach ( $assignments as $taxonomy => $terms ) {
140        if ( empty( $terms ) ) {
141            continue;
142        }
143        $result = wp_set_object_terms( $post_id, $terms, $taxonomy, $append );
144        if ( is_wp_error( $result ) ) {
145            $error_message = "Failed to set terms for post $post_id in taxonomy $taxonomy due to error: " . $result->get_error_message();
146            wpcomsh_headstart_log( $error_message );
147        }
148    }
149}
150
151/**
152 * Given a list of custom terms for several taxonomies,
153 * check if each of those terms exists and if not, add it to
154 * the site.
155 *
156 * A term is one value of a taxonomy. For example, the "category" taxonomy might
157 * have the "Shirts" and "Shoes" terms.
158 *
159 * @param array $custom_terms_by_taxonomy An array where the keys are the taxonomy names
160 *     and the values are arrays of arrays (the same as what's returned by get_terms( $taxonomy )).
161 *  Example: {
162 *  "product_cat": [
163 *    { "term_id": 1379, "name": "Men", "slug": "men", "term_group": 0, "term_taxonomy_id": 25,
164 *      "taxonomy": "product_cat", "description": "", "parent": 0, "count": 4, "filter": "raw" },
165 *    { "term_id": 1380, "name": "Women", "slug": "women", "term_group": 0, "term_taxonomy_id": 26,
166 *      "taxonomy": "product_cat", "description": "", "parent": 0, "count": 4, "filter": "raw" }
167 *  ],
168 *  "product_tag": [
169 *    { "term_id": 1381, "name": "New Arrivals", "slug": "new-arrivals", "term_group": 0,
170 *      "term_taxonomy_id": 27, "taxonomy": "product_tag", "description": "", "parent": 0,
171 *      "count": 4, "filter": "raw" }
172 *  ]
173 *  }.
174 * @return array $term_id_map A mapping of "old" term_ids to "new" term_ids for any terms that were inserted or found.
175 */
176function wpcomsh_apply_headstart_custom_terms( $custom_terms_by_taxonomy ) {
177    $term_id_map        = array();
178    $missing_taxonomies = array();
179
180    foreach ( $custom_terms_by_taxonomy as $taxonomy => $terms ) {
181        if ( ! taxonomy_exists( $taxonomy ) ) {
182            wpcomsh_headstart_log( "Skipping taxonomy [$taxonomy] because it does not exist." );
183            $missing_taxonomies[] = $taxonomy;
184            continue;
185        }
186
187        // First pass: Move terms into $terms_by_parent
188        $terms_by_parent = array();
189        foreach ( $terms as $term ) {
190            $parent_id = $term['parent'];
191            if ( empty( $parent_id ) ) {
192                $parent_id = 0;
193            }
194
195            if ( empty( $terms_by_parent[ $parent_id ] ) ) {
196                $terms_by_parent[ $parent_id ] = array( $term );
197            } else {
198                $terms_by_parent[ $parent_id ][] = $term;
199            }
200        }
201
202        // Top level terms are 'addable': put those into a queue.
203        // As we insert terms, any terms that depend on those will be added to the queue.
204        $addable_terms = $terms_by_parent['0'];
205        while ( ! empty( $addable_terms ) ) {
206            $term = array_shift( $addable_terms );
207
208            $old_term_id = $term['term_id'];
209            $new_term_id = null;
210
211            $term_info = term_exists( $term['slug'], $taxonomy );
212            if ( $term_info ) {
213                // Already exists
214                $new_term_id = $term_info['term_id'];
215            } else {
216                // Doesn't exist, will add
217                $args   = array(
218                    'slug'        => $term['slug'],
219                    'description' => $term['description'],
220                    'parent'      => $term_id_map[ $term['parent'] ] ?? 0,
221                );
222                $result = wp_insert_term( $term['name'], $taxonomy, $args );
223                if ( ! is_wp_error( $result ) ) {
224                    $new_term_id = $result['term_id'];
225                }
226            }
227
228            if ( ! empty( $new_term_id ) ) {
229                // Record the ID mapping
230                $term_id_map[ $old_term_id ] = $new_term_id;
231
232                // Mark any terms depending on me as "addable"
233                if ( ! empty( $terms_by_parent[ $old_term_id ] ) ) {
234                    $addable_terms = array_merge( $addable_terms, $terms_by_parent[ $old_term_id ] );
235                    unset( $terms_by_parent[ $old_term_id ] );
236                }
237            }
238        }
239    }
240    return array(
241        'term_id_map'        => $term_id_map,
242        'missing_taxonomies' => $missing_taxonomies,
243    );
244}
245
246/**
247 * Given an annotation's list of term_meta assignments, and a
248 * map of "old" term ids to "new" term ids, write the term_meta values
249 * in the assignment to the "new" term ids.
250 *
251 * Example:
252 * "old" term id: 1379   - This represents the "New Arrivals" category's ID in the annotation.
253 * "new" term id: 203482 - This represents the "New Arrivals" category's ID on the current site.
254 *
255 * @param array $custom_term_meta Term_meta assignments by "old" id.
256 *  Example: { "1379": { "order": [ 0 ], "another_meta_key": [ "another_meta_value" ] }, ... }.
257 * @param array $term_id_map Mapping of "old" term ids to "new" term ids.
258 *  Example: { "1379": "203482", "1380": "203483", ... }.
259 */
260function wpcomsh_apply_headstart_custom_term_meta( $custom_term_meta, $term_id_map ) {
261    foreach ( $custom_term_meta as $old_term_id => $metas ) {
262        if ( empty( $term_id_map[ $old_term_id ] ) ) {
263            continue;
264        }
265        $new_term_id = $term_id_map[ $old_term_id ];
266
267        // Find which term meta keys are already set on this term.
268        $existing_meta_keys = array();
269        $has_term_meta      = has_term_meta( $new_term_id );
270        if ( ! empty( $has_term_meta ) ) {
271            $existing_meta_keys = array_map(
272                function ( $row ) {
273                    return $row['meta_key'];
274                },
275                $has_term_meta
276            );
277        }
278
279        foreach ( $metas as $meta_key => $meta_value ) {
280            if ( is_array( $meta_value ) && count( $meta_value ) === 1 ) {
281                $meta_value = reset( $meta_value );
282            }
283
284            // Only write values if they do not already exist.
285            if ( in_array( $meta_key, $existing_meta_keys, true ) ) {
286                continue;
287            }
288
289            $result = add_term_meta( $new_term_id, $meta_key, $meta_value );
290            if ( is_wp_error( $result ) ) {
291                $error_message = "Failed to add term_meta for term $new_term_id (old term id: $old_term_id) and meta key $meta_key due to error: " . $result->get_error_message();
292                wpcomsh_headstart_log( $error_message );
293            }
294        }
295    }
296}
297
298add_action( 'woocommerce_installed', 'wpcomsh_apply_headstart_terms' );