Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
6 / 6
CRAP
100.00% covered (success)
100.00%
1 / 1
Dashboard_Section
100.00% covered (success)
100.00%
51 / 51
100.00% covered (success)
100.00%
6 / 6
24
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 derive_slug
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_available
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 get_default_layout
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 to_array
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
1
 set_props
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
15
1<?php
2/**
3 * Dashboard Sections API: Dashboard_Section class.
4 *
5 * @package automattic/jetpack-premium-analytics
6 */
7
8namespace Automattic\Jetpack\PremiumAnalytics;
9
10/**
11 * Represents a dashboard section.
12 *
13 * A dashboard section is the server-owned model behind a top-level dashboard
14 * tab. It carries display metadata plus availability and default-layout
15 * callbacks that can be extended by consumers.
16 */
17final class Dashboard_Section {
18
19    /**
20     * Date-filter surface offering the rolling date-range picker (today, last 7
21     * days, a custom range, …) plus the comparison control. The default.
22     *
23     * @since 0.2.0
24     * @var string
25     */
26    const DATE_FILTER_RANGE = 'range';
27
28    /**
29     * Date-filter surface offering all time plus one entry per calendar year,
30     * for sections whose data is read as whole history rather than as a
31     * rolling window.
32     *
33     * @since 0.2.0
34     * @var string
35     */
36    const DATE_FILTER_YEAR = 'year';
37
38    /**
39     * Date-filter surfaces a section may declare.
40     *
41     * @since 0.2.0
42     * @var string[]
43     */
44    const DATE_FILTERS = array( self::DATE_FILTER_RANGE, self::DATE_FILTER_YEAR );
45
46    /**
47     * Dashboard identifier.
48     *
49     * @var string
50     */
51    public $dashboard_name;
52
53    /**
54     * Section identifier.
55     *
56     * @var string
57     */
58    public $id;
59
60    /**
61     * URL-facing section slug, derived from the identifier.
62     *
63     * @var string
64     */
65    public $slug;
66
67    /**
68     * Display label, naming the section's tab.
69     *
70     * @var string
71     */
72    public $label;
73
74    /**
75     * Section heading, deliberately distinct from the tab label: the tab reads
76     * `Traffic` where the heading reads `Site traffic`. Null falls back to the label.
77     *
78     * @since 0.3.0
79     * @var string|null
80     */
81    public $title = null;
82
83    /**
84     * Section description, shown as the page subtitle while this section is active.
85     *
86     * @since 0.3.0
87     * @var string|null
88     */
89    public $description = null;
90
91    /**
92     * Sort order.
93     *
94     * @var int
95     */
96    public $order = 10;
97
98    /**
99     * Which date filter the section's header offers, as one of self::DATE_FILTERS.
100     *
101     * @since 0.2.0
102     * @var string
103     */
104    public $date_filter = self::DATE_FILTER_RANGE;
105
106    /**
107     * Which optional controls the section's date filter offers.
108     *
109     * @since 0.3.0
110     * @var array
111     */
112    public $date_filter_options = array(
113        'with_date_comparison' => true,
114    );
115
116    /**
117     * Whether the section's data only reaches WordPress.com through the analytics
118     * full sync, so its numbers are incomplete until that sync has finished once.
119     *
120     * @since $$next-version$$
121     * @var bool
122     */
123    public $requires_sync = false;
124
125    /**
126     * Availability flag or callback.
127     *
128     * @var bool|callable
129     */
130    private $is_available = true;
131
132    /**
133     * Default layout array or callback.
134     *
135     * @var array|callable
136     */
137    private $default_layout = array();
138
139    /**
140     * Constructor.
141     *
142     * @param string $dashboard_name Dashboard identifier.
143     * @param string $id             Section identifier.
144     * @param array  $args           Optional. Section arguments.
145     */
146    public function __construct( $dashboard_name, $id, $args = array() ) {
147        $this->dashboard_name = $dashboard_name;
148        $this->id             = $id;
149        $this->slug           = self::derive_slug( $id );
150        $this->label          = $id;
151
152        $this->set_props( $args );
153    }
154
155    /**
156     * Derives the URL-facing slug from a namespaced section identifier.
157     *
158     * @param string $id Section identifier, e.g. `analytics/traffic`.
159     * @return string The segment after the namespace, e.g. `traffic`.
160     */
161    private static function derive_slug( $id ) {
162        $separator = strpos( (string) $id, '/' );
163
164        return false === $separator ? (string) $id : substr( $id, $separator + 1 );
165    }
166
167    /**
168     * Returns whether this section should be exposed.
169     *
170     * @return bool
171     */
172    public function is_available() {
173        if ( is_callable( $this->is_available ) ) {
174            return (bool) call_user_func( $this->is_available, $this );
175        }
176
177        return (bool) $this->is_available;
178    }
179
180    /**
181     * Returns the section's default widget layout.
182     *
183     * @return array Array of widget instances.
184     */
185    public function get_default_layout() {
186        $layout = is_callable( $this->default_layout )
187            ? call_user_func( $this->default_layout, $this )
188            : $this->default_layout;
189
190        return is_array( $layout ) ? array_values( $layout ) : array();
191    }
192
193    /**
194     * Returns the public REST representation.
195     *
196     * @return array
197     */
198    public function to_array() {
199        return array(
200            'id'                  => $this->id,
201            'slug'                => $this->slug,
202            'label'               => $this->label,
203            'title'               => $this->title,
204            'description'         => $this->description,
205            'order'               => (int) $this->order,
206            'date_filter'         => $this->date_filter,
207            'date_filter_options' => $this->date_filter_options,
208            'requires_sync'       => $this->requires_sync,
209            'default_layout'      => $this->get_default_layout(),
210        );
211    }
212
213    /**
214     * Hydrates section properties from the args array.
215     *
216     * @param array $args Section arguments.
217     * @return void
218     */
219    private function set_props( $args ) {
220        if ( ! is_array( $args ) ) {
221            return;
222        }
223
224        if ( isset( $args['label'] ) ) {
225            $this->label = (string) $args['label'];
226        }
227
228        // An empty string is a registrant saying "none", not a heading: kept as-is it
229        // would defeat the label fallback and render an `<h2>` with no accessible name.
230        if ( isset( $args['title'] ) ) {
231            $title       = (string) $args['title'];
232            $this->title = '' === $title ? null : $title;
233        }
234
235        if ( isset( $args['description'] ) ) {
236            $description       = (string) $args['description'];
237            $this->description = '' === $description ? null : $description;
238        }
239
240        if ( isset( $args['order'] ) ) {
241            $this->order = (int) $args['order'];
242        }
243
244        // An unrecognized surface keeps the default rather than reaching the
245        // dashboard, where the frontend has no filter to render for it.
246        if ( isset( $args['date_filter'] ) && in_array( $args['date_filter'], self::DATE_FILTERS, true ) ) {
247            $this->date_filter = (string) $args['date_filter'];
248        }
249
250        // Merged over the defaults so a partial array keeps the rest, and narrowed
251        // to the known options, which is all the dashboard renders.
252        if ( isset( $args['date_filter_options'] ) && is_array( $args['date_filter_options'] ) ) {
253            $options = array_merge( $this->date_filter_options, $args['date_filter_options'] );
254
255            $this->date_filter_options = array(
256                'with_date_comparison' => (bool) $options['with_date_comparison'],
257            );
258        }
259
260        if ( isset( $args['requires_sync'] ) ) {
261            $this->requires_sync = (bool) $args['requires_sync'];
262        }
263
264        if ( array_key_exists( 'is_available', $args ) ) {
265            $this->is_available = $args['is_available'];
266        }
267
268        if ( array_key_exists( 'default_layout', $args ) ) {
269            $this->default_layout = $args['default_layout'];
270        }
271    }
272}