Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
56.86% covered (warning)
56.86%
29 / 51
40.00% covered (danger)
40.00%
4 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
WPCom_Themes_Service
56.86% covered (warning)
56.86%
29 / 51
40.00% covered (danger)
40.00%
4 / 10
40.20
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 filter_themes_api_result_recommended
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 filter_themes_api_result_search
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 without_wporg_themes
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
3.01
 map_and_filter_wpcom_themes
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 has_valid_theme_tier
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 get_theme
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 filter_themes_api_result_latest
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 filter_themes_api_result_block_themes
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 filter_themes_api_result_feature_filter
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * Class WPCom_Themes_Service.
4 * Component that interacts with the WordPress.com themes API and has the business logic to filter the partner themes.
5 *
6 * @package wpcom-themes
7 */
8
9/**
10 * WordPress.com themes service.
11 */
12class WPCom_Themes_Service {
13    /**
14     * Mapper to map WPCom themes to WPORG themes.
15     *
16     * @var WPCom_Themes_Mapper
17     */
18    private WPCom_Themes_Mapper $mapper;
19
20    /**
21     * WordPress.com themes API.
22     *
23     * @var WPCom_Themes_Api
24     */
25    private WPCom_Themes_Api $api;
26
27    /**
28     * Theme merge strategy collection.
29     *
30     * @var WPCom_Themes_Merger
31     */
32    private WPCom_Themes_Merger $merger;
33
34    /**
35     * Class constructor.
36     *
37     * @param WPCom_Themes_Api    $api    The WordPress.com themes API.
38     * @param WPCom_Themes_Mapper $mapper The mapper to map WPCom themes to WPORG themes.
39     * @param WPCom_Themes_Merger $merger The theme merge strategy collection.
40     */
41    public function __construct(
42        WPCom_Themes_Api $api,
43        WPCom_Themes_Mapper $mapper,
44        WPCom_Themes_Merger $merger
45    ) {
46        $this->api    = $api;
47        $this->mapper = $mapper;
48        $this->merger = $merger;
49    }
50
51    /**
52     * Filter the themes API result to include recommended WordPress.com themes.
53     *
54     * @param stdClass $wporg_theme_api_response The WP.org themes API result.
55     *
56     * @return stdClass The themes API result including wpcom themes.
57     */
58    public function filter_themes_api_result_recommended( stdClass $wporg_theme_api_response ): stdClass {
59        $wpcom_recommended_themes = $this->api->fetch_recommended_themes();
60        $wpcom_recommended_themes = $this->map_and_filter_wpcom_themes( $wpcom_recommended_themes );
61
62        return $this->merger->merge_by_wpcom_first( $wporg_theme_api_response, $wpcom_recommended_themes );
63    }
64
65    /**
66     * Filter the themes API result to include WordPress.com themes matching a search term.
67     *
68     * @param stdClass $wporg_theme_api_response The WP.org themes API result.
69     * @param string   $search                   Search term.
70     * @param bool     $prefer_wporg             Whether a theme WordPress.org also lists is left to it.
71     *
72     * @return stdClass The themes API result including wpcom themes.
73     */
74    public function filter_themes_api_result_search( stdClass $wporg_theme_api_response, string $search, bool $prefer_wporg = false ): stdClass {
75        $wpcom_search_themes = $this->api->search_themes( $search );
76
77        if ( $prefer_wporg ) {
78            $wpcom_search_themes = $this->without_wporg_themes( $wpcom_search_themes );
79        }
80
81        $wpcom_search_themes = $this->map_and_filter_wpcom_themes( $wpcom_search_themes );
82
83        return $this->merger->merge_by_wpcom_first( $wporg_theme_api_response, $wpcom_search_themes );
84    }
85
86    /**
87     * Leaves out the themes WordPress.org also lists, so its copy shows wherever it ranks them.
88     *
89     * @param array $wpcom_themes WPCom themes.
90     *
91     * @return array The rest, or all of them when WordPress.org could not be asked, as before.
92     */
93    protected function without_wporg_themes( array $wpcom_themes ): array {
94        // The lookup ships in jetpack-mu-wpcom, which an older copy may be loaded from.
95        $wporg = method_exists( $this->api, 'fetch_wporg_slugs' )
96            ? $this->api->fetch_wporg_slugs( array_column( $wpcom_themes, 'id' ) )
97            : null;
98
99        if ( null === $wporg ) {
100            return $wpcom_themes;
101        }
102
103        $wporg = array_flip( $wporg );
104
105        return array_values(
106            array_filter(
107                $wpcom_themes,
108                fn( $theme ) => ! isset( $wporg[ $theme->id ?? '' ] )
109            )
110        );
111    }
112
113    /**
114     * Get the WordPress.com themes that can be installed on Atomic sites without a subscription.
115     *
116     * @param array $wpcom_themes WPCom themes.
117     *
118     * @return array Filtered WPCom themes.
119     */
120    protected function map_and_filter_wpcom_themes( array $wpcom_themes ): array {
121        $themes = array();
122        foreach ( $wpcom_themes as $theme ) {
123            if ( $this->has_valid_theme_tier( $theme ) ) {
124                $themes[] = $this->mapper->map_wpcom_to_wporg( $theme );
125            }
126        }
127
128        return $themes;
129    }
130
131    /**
132     * Whether core's Add Themes tabs should offer a WPCom theme on this site.
133     *
134     * Partner themes need their own subscription rather than a plan, so these tabs leave them to
135     * the Marketplace tab, which lists them in order to sell them.
136     *
137     * @param stdClass $theme The theme to check.
138     *
139     * @return bool True if the theme has a valid tier, false otherwise.
140     */
141    protected function has_valid_theme_tier( stdClass $theme ): bool {
142        $tier = WPCom_Themes_Tiers::tier_of( $theme );
143
144        return 'partner' !== $tier && WPCom_Themes_Tiers::site_can_use( $tier );
145    }
146
147    /**
148     * Retrieves a WPCom theme by its slug.
149     *
150     * @param string $slug The theme slug.
151     *
152     * @return stdClass|null The theme object if found, null otherwise.
153     */
154    public function get_theme( string $slug ): ?stdClass {
155        $wpcom_theme = $this->api->fetch_theme( $slug );
156
157        if ( ! $wpcom_theme ) {
158            return null;
159        }
160
161        return $this->mapper->map_wpcom_to_wporg( $wpcom_theme );
162    }
163
164    /**
165     * Returns a list of themes that include WPCom themes sorted by release date.
166     *
167     * @param stdClass $wporg_theme_api_response The WP.org themes API result.
168     *
169     * @return stdClass The themes API result including wpcom themes.
170     */
171    public function filter_themes_api_result_latest( stdClass $wporg_theme_api_response ): stdClass {
172        $wpcom_themes = $this->api->fetch_all_non_delisted_themes();
173        $wpcom_themes = $this->map_and_filter_wpcom_themes( $wpcom_themes );
174
175        return $this->merger->merge_by_release_date( $wporg_theme_api_response, $wpcom_themes );
176    }
177
178    /**
179     * Returns a list of themes that include WPCom block themes sorted by release date.
180     *
181     * @param stdClass $wporg_theme_api_response The WP.org themes API result.
182     *
183     * @return stdClass The themes API result including wpcom themes.
184     */
185    public function filter_themes_api_result_block_themes( stdClass $wporg_theme_api_response ): stdClass {
186        $wpcom_themes = $this->api->fetch_all_non_delisted_themes();
187        $wpcom_themes = array_filter(
188            $wpcom_themes,
189            fn( $theme ) => $theme->block_theme
190        );
191        $wpcom_themes = $this->map_and_filter_wpcom_themes( $wpcom_themes );
192
193        return $this->merger->merge_by_release_date( $wporg_theme_api_response, $wpcom_themes );
194    }
195
196    /**
197     * Returns a list of themes that include WPCom themes filtered by tags.
198     *
199     * @param stdClass $wporg_theme_api_response The WP.org themes API result.
200     * @param array    $tags                     The tags to filter by.
201     *
202     * @return stdClass The themes API result including wpcom themes.
203     */
204    public function filter_themes_api_result_feature_filter( stdClass $wporg_theme_api_response, array $tags ): stdClass {
205        $wpcom_themes = $this->api->fetch_all_non_delisted_themes();
206        $wpcom_themes = $this->map_and_filter_wpcom_themes( $wpcom_themes );
207
208        $wpcom_themes = array_filter(
209            $wpcom_themes,
210            fn( $theme ) => (bool) array_intersect( $tags, $theme->tags )
211        );
212
213        return $this->merger->merge_by_wpcom_first( $wporg_theme_api_response, $wpcom_themes );
214    }
215}