Busiest_Day_Of_Week_Module.php
5 months ago
Busiest_Time_Of_Day_Module.php
1 year ago
Line_Chart_Module.php
9 months ago
Map_Module.php
9 months ago
Module.php
5 months ago
New_Sessions_Module.php
1 year ago
Pie_Chart_Module.php
9 months ago
Quick_Stats_Module.php
9 months ago
Recent_Conversions_Module.php
5 months ago
Recent_Views_Module.php
5 months ago
Top_Ten_Module.php
9 months ago
Module.php
420 lines
| 1 | <?php |
| 2 | |
| 3 | namespace IAWP\Overview\Modules; |
| 4 | |
| 5 | use DateTime; |
| 6 | use IAWP\Date_Range\Relative_Date_Range; |
| 7 | use IAWP\Overview\Form_Field; |
| 8 | use IAWP\Overview\Form_Field_Option; |
| 9 | use IAWP\Overview\WP_Options_Storage; |
| 10 | use IAWP\Report; |
| 11 | use IAWP\Report_Finder; |
| 12 | use IAWP\Utils\Format; |
| 13 | use IAWP\Utils\Security; |
| 14 | use IAWP\Utils\Timezone; |
| 15 | use IAWPSCOPED\Illuminate\Support\Arr; |
| 16 | use IAWPSCOPED\Illuminate\Support\Collection; |
| 17 | /** @internal */ |
| 18 | abstract class Module |
| 19 | { |
| 20 | protected $attributes; |
| 21 | private $report; |
| 22 | private $form_fields; |
| 23 | private $validation_errors = null; |
| 24 | private $options_storage; |
| 25 | public function __construct(?array $attributes = []) |
| 26 | { |
| 27 | $this->report = $this->get_report($attributes); |
| 28 | $this->form_fields = $this->create_form_fields(); |
| 29 | $this->attributes = $this->prune_attributes($attributes); |
| 30 | $this->options_storage = new WP_Options_Storage('iawp_overview_modules'); |
| 31 | } |
| 32 | public abstract function module_type() : string; |
| 33 | public abstract function module_name() : string; |
| 34 | protected abstract function module_fields() : array; |
| 35 | public abstract function calculate_dataset(); |
| 36 | public function id() : ?string |
| 37 | { |
| 38 | return $this->attributes['id'] ?? null; |
| 39 | } |
| 40 | public function name() : ?string |
| 41 | { |
| 42 | return $this->attributes['name'] ?? null; |
| 43 | } |
| 44 | public function is_full_width() : bool |
| 45 | { |
| 46 | return $this->attributes['is_full_width'] ?? \false; |
| 47 | } |
| 48 | public function report() : ?Report |
| 49 | { |
| 50 | return $this->report; |
| 51 | } |
| 52 | public function save($skip_sync = \false) : bool |
| 53 | { |
| 54 | if (!$this->is_valid()) { |
| 55 | return \false; |
| 56 | } |
| 57 | $is_update = $this->options_storage->exists($this->id()); |
| 58 | // Generate an id |
| 59 | if (!$is_update) { |
| 60 | $this->attributes['id'] = $this->options_storage->generate_id(); |
| 61 | } |
| 62 | $this->options_storage->insert($this->attributes); |
| 63 | $this->report = $this->get_report($this->attributes); |
| 64 | if (!$skip_sync) { |
| 65 | $this->refresh(); |
| 66 | } |
| 67 | return \true; |
| 68 | } |
| 69 | public function update(array $attributes) |
| 70 | { |
| 71 | $pruned_attributes = $this->prune_attributes($attributes, \true); |
| 72 | $this->attributes = \array_merge($this->attributes, $pruned_attributes); |
| 73 | $this->report = $this->get_report($attributes); |
| 74 | $this->form_fields = $this->create_form_fields(); |
| 75 | } |
| 76 | public function delete() : bool |
| 77 | { |
| 78 | // Delete any cached datasets |
| 79 | \delete_option($this->option_name()); |
| 80 | // Delete the module configuration |
| 81 | return $this->options_storage->delete($this->id()); |
| 82 | } |
| 83 | public function is_valid() : bool |
| 84 | { |
| 85 | $validation_errors = []; |
| 86 | if (\is_string($this->attributes['name'] ?? null) && \strlen($this->attributes['name']) > 0) { |
| 87 | } else { |
| 88 | $validation_errors[] = ['name', 'Must be provided']; |
| 89 | } |
| 90 | // Check every form field |
| 91 | foreach ($this->form_fields as $form_field) { |
| 92 | $value = $this->attributes[$form_field->id()] ?? null; |
| 93 | if (!$form_field->is_a_supported_value($value)) { |
| 94 | $validation_errors[] = [$form_field->id(), "Invalid option provided"]; |
| 95 | } |
| 96 | } |
| 97 | if (\count($validation_errors) > 0) { |
| 98 | $this->validation_errors = $validation_errors; |
| 99 | return \false; |
| 100 | } else { |
| 101 | return \true; |
| 102 | } |
| 103 | } |
| 104 | public function validation_errors() : ?array |
| 105 | { |
| 106 | return $this->validation_errors; |
| 107 | } |
| 108 | public function form_fields() : array |
| 109 | { |
| 110 | return $this->form_fields; |
| 111 | } |
| 112 | public function get_module_html() : string |
| 113 | { |
| 114 | $dataset = $this->get_dataset(); |
| 115 | return \IAWPSCOPED\iawp_render("overview.modules.layout", ['module' => $this, 'dataset' => $dataset, 'is_loaded' => $dataset !== null, 'is_empty' => \is_array($dataset) && empty($dataset)]); |
| 116 | } |
| 117 | public function get_editor_html() : string |
| 118 | { |
| 119 | return \IAWPSCOPED\iawp_render('overview.module-editor', ['module' => $this]); |
| 120 | } |
| 121 | public function get_form_fields_html() : string |
| 122 | { |
| 123 | $html = ''; |
| 124 | foreach ($this->form_fields as $form_field) { |
| 125 | $type = $form_field->type(); |
| 126 | $html .= \IAWPSCOPED\iawp_render("overview.form-fields.{$type}", ['form_field' => $form_field, 'selected_value' => $this->attributes[$form_field->id()] ?? null]); |
| 127 | } |
| 128 | return $html; |
| 129 | } |
| 130 | public function get_report_details() : array |
| 131 | { |
| 132 | return Form_Field::get_report_details(); |
| 133 | } |
| 134 | /** |
| 135 | * Has this module already been saved? |
| 136 | * |
| 137 | * @return bool |
| 138 | */ |
| 139 | public function is_saved() : bool |
| 140 | { |
| 141 | return $this->id() !== null; |
| 142 | } |
| 143 | public function get_dataset() : ?array |
| 144 | { |
| 145 | $value = \get_option($this->option_name(), null); |
| 146 | return \is_array($value) ? $value : null; |
| 147 | } |
| 148 | public function has_dataset() : bool |
| 149 | { |
| 150 | return \is_array($this->get_dataset()); |
| 151 | } |
| 152 | /** |
| 153 | * Subtitle to show under the module name |
| 154 | * |
| 155 | * @return string |
| 156 | */ |
| 157 | public function subtitle() : string |
| 158 | { |
| 159 | // If the module has a range option, use the selected value |
| 160 | if ($this->has_dataset()) { |
| 161 | $relative_range = Relative_Date_Range::range_by_id($this->attributes['date_range'] ?? $this->attributes['busiest_date_range']); |
| 162 | if ($relative_range) { |
| 163 | return $relative_range->label(); |
| 164 | } |
| 165 | } |
| 166 | $number_of_days = $this->get_dataset_number_of_days(); |
| 167 | if (\is_int($number_of_days)) { |
| 168 | return \sprintf(\__("Last %s Days", 'independent-analytics'), $this->get_dataset_number_of_days()); |
| 169 | } |
| 170 | return ''; |
| 171 | } |
| 172 | public function refresh() |
| 173 | { |
| 174 | \delete_option($this->option_name()); |
| 175 | \update_option($this->option_name(), $this->calculate_dataset(), \false); |
| 176 | } |
| 177 | protected function get_dataset_number_of_days() : ?int |
| 178 | { |
| 179 | return null; |
| 180 | } |
| 181 | protected function option_name() : string |
| 182 | { |
| 183 | return 'iawp_module_' . $this->id(); |
| 184 | } |
| 185 | /** |
| 186 | * Get the name for the field option that was selcted for the given $field_id. |
| 187 | * |
| 188 | * @param string $field_id |
| 189 | * |
| 190 | * @return string|null |
| 191 | */ |
| 192 | protected function get_field_option_name(string $field_id) : ?string |
| 193 | { |
| 194 | $selected_field_option_id = $this->attributes[$field_id] ?? null; |
| 195 | if ($selected_field_option_id === null) { |
| 196 | return null; |
| 197 | } |
| 198 | $options = $this->get_form_field($field_id)->supported_values(); |
| 199 | $option = Collection::make($options)->first(function (Form_Field_Option $option) use($selected_field_option_id) { |
| 200 | return $option->id() === $selected_field_option_id; |
| 201 | }); |
| 202 | if ($option === null) { |
| 203 | return null; |
| 204 | } |
| 205 | return $option->name(); |
| 206 | } |
| 207 | /** |
| 208 | * @return ?Form_Field |
| 209 | */ |
| 210 | protected function get_form_field(string $id) : ?Form_Field |
| 211 | { |
| 212 | return Collection::make($this->form_fields)->first(function (Form_Field $field) use($id) { |
| 213 | return $field->id() === $id; |
| 214 | }); |
| 215 | } |
| 216 | /** |
| 217 | * Determine which report to associate with a module instance. |
| 218 | * |
| 219 | * @param array $attributes |
| 220 | * |
| 221 | * @return Report |
| 222 | */ |
| 223 | private function get_report(array $attributes) : Report |
| 224 | { |
| 225 | $default_report = Report_Finder::new()->get_base_report_for_type('views'); |
| 226 | $report_id = $attributes['report'] ?? $attributes['geo_report'] ?? null; |
| 227 | if ($report_id === null) { |
| 228 | return $default_report; |
| 229 | } |
| 230 | $report = Report_Finder::new()->fetch_report($report_id); |
| 231 | if ($report instanceof Report) { |
| 232 | return $report; |
| 233 | } else { |
| 234 | return $default_report; |
| 235 | } |
| 236 | } |
| 237 | /** |
| 238 | * Remove attributes that are not available for a given module |
| 239 | * |
| 240 | * @param array $attributes |
| 241 | * |
| 242 | * @return array |
| 243 | */ |
| 244 | private function prune_attributes(array $attributes, $for_update = \false) : array |
| 245 | { |
| 246 | if ($for_update) { |
| 247 | $valid_attributes = Collection::make(['name', 'is_full_width']); |
| 248 | } else { |
| 249 | $valid_attributes = Collection::make(['id', 'module_type', 'name', 'is_full_width']); |
| 250 | } |
| 251 | foreach ($this->form_fields as $form_field) { |
| 252 | $valid_attributes->push($form_field->id()); |
| 253 | } |
| 254 | $new_attributes = Arr::only($attributes, $valid_attributes->all()); |
| 255 | // Convert is_full_width into a boolean if present |
| 256 | if (\array_key_exists('is_full_width', $new_attributes) && !\is_bool($new_attributes['is_full_width'])) { |
| 257 | $new_attributes['is_full_width'] = $new_attributes['is_full_width'] === 'true'; |
| 258 | } |
| 259 | foreach ($new_attributes as $key => $value) { |
| 260 | if (\is_string($value)) { |
| 261 | $new_attributes[$key] = Security::string($value); |
| 262 | } |
| 263 | if (\is_array($value) && Arr::isList($value)) { |
| 264 | $new_attributes[$key] = Collection::make($value)->map(function ($value) { |
| 265 | if (\is_string($value)) { |
| 266 | return Security::string($value); |
| 267 | } |
| 268 | return $value; |
| 269 | })->all(); |
| 270 | } elseif (\is_array($value) && Arr::isAssoc($value)) { |
| 271 | unset($new_attributes[$key]); |
| 272 | } |
| 273 | } |
| 274 | return $new_attributes; |
| 275 | } |
| 276 | /** |
| 277 | * @return Form_Field[] |
| 278 | */ |
| 279 | private function create_form_fields() : array |
| 280 | { |
| 281 | return Collection::make($this->module_fields())->map(function (string $id) { |
| 282 | return new Form_Field($id, $this->report); |
| 283 | })->all(); |
| 284 | } |
| 285 | public static function last_refreshed_at() : string |
| 286 | { |
| 287 | $timestamp = \get_option('iawp_modules_refreshed_at', null); |
| 288 | // The option could be a string or an int. Normalize it. |
| 289 | if (\is_int($timestamp)) { |
| 290 | $timestamp = \strval($timestamp); |
| 291 | } |
| 292 | if (!\ctype_digit($timestamp)) { |
| 293 | return ''; |
| 294 | } |
| 295 | try { |
| 296 | $date = DateTime::createFromFormat('U', $timestamp, Timezone::utc_timezone()); |
| 297 | $date->setTimezone(Timezone::site_timezone()); |
| 298 | $text = $date->format(Format::time()); |
| 299 | return \sprintf(\_x('Last updated at %s.', 'The placeholder is a time of day', 'independent-analytics'), $text); |
| 300 | } catch (\Exception $e) { |
| 301 | return ''; |
| 302 | } |
| 303 | } |
| 304 | public static function new(string $module_type, array $attributes) : ?self |
| 305 | { |
| 306 | switch ($module_type) { |
| 307 | case 'top-ten': |
| 308 | return new \IAWP\Overview\Modules\Top_Ten_Module($attributes); |
| 309 | case 'quick-stats': |
| 310 | return new \IAWP\Overview\Modules\Quick_Stats_Module($attributes); |
| 311 | case 'line-chart': |
| 312 | return new \IAWP\Overview\Modules\Line_Chart_Module($attributes); |
| 313 | case 'pie-chart': |
| 314 | return new \IAWP\Overview\Modules\Pie_Chart_Module($attributes); |
| 315 | case 'map': |
| 316 | return new \IAWP\Overview\Modules\Map_Module($attributes); |
| 317 | case 'recent-views': |
| 318 | return new \IAWP\Overview\Modules\Recent_Views_Module($attributes); |
| 319 | case 'recent-conversions': |
| 320 | return new \IAWP\Overview\Modules\Recent_Conversions_Module($attributes); |
| 321 | case 'busiest-time-of-day': |
| 322 | return new \IAWP\Overview\Modules\Busiest_Time_Of_Day_Module($attributes); |
| 323 | case 'busiest-day-of-week': |
| 324 | return new \IAWP\Overview\Modules\Busiest_Day_Of_Week_Module($attributes); |
| 325 | case 'new-sessions': |
| 326 | return new \IAWP\Overview\Modules\New_Sessions_Module($attributes); |
| 327 | default: |
| 328 | return null; |
| 329 | } |
| 330 | } |
| 331 | /** |
| 332 | * Get a template version of each module for use when creating new modules |
| 333 | * |
| 334 | * @return self[] |
| 335 | */ |
| 336 | public static function get_template_modules() : array |
| 337 | { |
| 338 | return [new \IAWP\Overview\Modules\Top_Ten_Module(), new \IAWP\Overview\Modules\Quick_Stats_Module(), new \IAWP\Overview\Modules\Line_Chart_Module(), new \IAWP\Overview\Modules\Pie_Chart_Module(), new \IAWP\Overview\Modules\Map_Module(), new \IAWP\Overview\Modules\Recent_Views_Module(), new \IAWP\Overview\Modules\Recent_Conversions_Module(), new \IAWP\Overview\Modules\Busiest_Time_Of_Day_Module(), new \IAWP\Overview\Modules\Busiest_Day_Of_Week_Module(), new \IAWP\Overview\Modules\New_Sessions_Module()]; |
| 339 | } |
| 340 | /** |
| 341 | * Get the modules that are saved to wp_options |
| 342 | * |
| 343 | * @return self[] |
| 344 | */ |
| 345 | public static function get_saved_modules() : array |
| 346 | { |
| 347 | $options_storage = new WP_Options_Storage('iawp_overview_modules'); |
| 348 | $records = Collection::make($options_storage->all())->filter(function (array $module) { |
| 349 | if (\array_key_exists('report', $module) && $module['report'] === 'journeys') { |
| 350 | return \false; |
| 351 | } |
| 352 | return \true; |
| 353 | })->values()->all(); |
| 354 | // No records? Add the default modules. |
| 355 | if (\count($records) === 0 && \IAWPSCOPED\iawp()->get_option('iawp_default_modules_added', \false) === \false) { |
| 356 | \update_option('iawp_modules_refreshed_at', \time(), \true); |
| 357 | \update_option('iawp_default_modules_added', \true, \true); |
| 358 | foreach (self::default_modules() as $attributes) { |
| 359 | $module = self::new($attributes['module_type'], $attributes); |
| 360 | $module->save(\true); |
| 361 | } |
| 362 | $records = $options_storage->all(); |
| 363 | static::refresh_all_modules(); |
| 364 | } |
| 365 | return \array_map(function ($record) { |
| 366 | return self::new($record['module_type'], $record); |
| 367 | }, $records); |
| 368 | } |
| 369 | public static function refresh_all_modules() : void |
| 370 | { |
| 371 | foreach (self::get_saved_modules() as $module) { |
| 372 | $module->refresh(); |
| 373 | } |
| 374 | \update_option('iawp_modules_refreshed_at', \time(), \true); |
| 375 | } |
| 376 | public static function queue_refresh_all_modules() : void |
| 377 | { |
| 378 | foreach (self::get_saved_modules() as $module) { |
| 379 | \delete_option($module->option_name()); |
| 380 | } |
| 381 | \wp_schedule_single_event(\time(), 'iawp_module_refresh_now'); |
| 382 | \spawn_cron(); |
| 383 | } |
| 384 | public static function get_saved_module(string $id) : ?self |
| 385 | { |
| 386 | $modules = self::get_saved_modules(); |
| 387 | return Collection::make($modules)->first(function (self $module) use($id) { |
| 388 | return $module->id() === $id; |
| 389 | }); |
| 390 | } |
| 391 | /** |
| 392 | * @param string[] $ids |
| 393 | * |
| 394 | * @return void |
| 395 | */ |
| 396 | public static function set_module_order(array $ids) : void |
| 397 | { |
| 398 | $options_storage = new WP_Options_Storage('iawp_overview_modules'); |
| 399 | $options_storage->set_order($ids); |
| 400 | } |
| 401 | /** |
| 402 | * Delete all exiting modules and restore the defaults. |
| 403 | * |
| 404 | * @return void |
| 405 | */ |
| 406 | public static function reset() |
| 407 | { |
| 408 | $modules = self::get_saved_modules(); |
| 409 | foreach ($modules as $module) { |
| 410 | $module->delete(); |
| 411 | } |
| 412 | \delete_option('iawp_modules_refreshed_at'); |
| 413 | \delete_option('iawp_default_modules_added'); |
| 414 | } |
| 415 | public static function default_modules() : array |
| 416 | { |
| 417 | return [["module_type" => "line-chart", "name" => \esc_html__("Site Traffic", "independent-analytics"), "report" => "views", "primary_metric" => "visitors", "secondary_metric" => "views", "date_range" => "LAST_THIRTY"], ["module_type" => "top-ten", "name" => \esc_html__("Top 10 Pages", "independent-analytics"), "report" => "views", "sort_by" => "visitors", "sort_direction" => "desc", "date_range" => "LAST_SEVEN"], ["module_type" => "map", "name" => \esc_html__("Geographic Traffic", "independent-analytics"), "geo_report" => "geo", "date_range" => "LAST_SEVEN"], ["module_type" => "new-sessions", "name" => \esc_html__("New vs. Returning Sessions", "independent-analytics"), "date_range" => "LAST_THIRTY"], ["module_type" => "quick-stats", "name" => \esc_html__("Site Metrics", "independent-analytics"), "report" => "views", "statistics" => ["visitors", "views", "sessions", "average_session_duration", "bounce_rate", "views_per_session", "clicks"], "date_range" => "LAST_SEVEN", "is_full_width" => \true], ["module_type" => "recent-views", "name" => \esc_html__("Recent Views", "independent-analytics")], ["module_type" => "recent-conversions", "name" => \esc_html__("Recent Conversions", "independent-analytics"), "recent_conversion_types" => ["order", "form_submission", "click"]], ["module_type" => "pie-chart", "name" => \esc_html__("Devices", "independent-analytics"), "report" => "devices", "aggregatable_sort_by" => "visitors", "date_range" => "LAST_THIRTY"], ["module_type" => "pie-chart", "name" => \esc_html__("Top Traffic Sources", "independent-analytics"), "report" => "referrers", "aggregatable_sort_by" => "visitors", "date_range" => "LAST_SEVEN"], ["module_type" => "busiest-time-of-day", "name" => \esc_html__("Busiest Time of Day", "independent-analytics"), "busiest_date_range" => "LAST_NINETY"], ["module_type" => "busiest-day-of-week", "name" => \esc_html__("Busiest Day of Week", "independent-analytics"), "busiest_date_range" => "LAST_NINETY"]]; |
| 418 | } |
| 419 | } |
| 420 |