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