AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Class abstract_data_source.** @package block_dash* @copyright 2019 bdecent gmbh <https://bdecent.de>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/namespace block_dash\local\data_source;use block_dash\local\dash_framework\query_builder\builder;use block_dash\local\dash_framework\structure\field_interface;use block_dash\local\dash_framework\structure\table;use block_dash\local\data_grid\data\data_collection;use block_dash\local\data_grid\field\attribute\identifier_attribute;use block_dash\local\data_grid\data\data_collection_interface;use block_dash\local\data_grid\filter\filter_collection_interface;use block_dash\local\paginator;use block_dash\local\data_source\form\preferences_form;use block_dash\local\layout\grid_layout;use block_dash\local\layout\layout_factory;use block_dash\local\layout\layout_interface;use coding_exception;/*** Class abstract_data_source.** @package block_dash*/abstract class abstract_data_source implements data_source_interface, \templatable {/*** @var \context*/private $context;/*** @var data_collection_interface*/private $data;/*** @var filter_collection_interface*/private $filtercollection;/*** @var array*/private $preferences = [];/*** @var layout_interface*/private $layout;/*** @var field_interface[]*/private $fields;/*** @var field_interface[]*/private $sortedfields;/*** @var \block_base|null*/private $blockinstance = null;/*** @var builder*/private $query;/*** @var paginator*/protected $paginator;/*** @var table[]*/private $tables = [];/*** Constructor.** @param \context $context*/public function __construct(\context $context) {$this->context = $context;}/*** Get human readable name of data source.** @return string*/public function get_name() {return self::get_name_from_class(get_class($this));}/*** Get human readable name of data source.** @param string $fullclassname* @param bool $help Returns the help.* @return string* @throws coding_exception*/public static function get_name_from_class($fullclassname, $help=false) {$component = explode('\\', $fullclassname)[0];$class = array_reverse(explode('\\', $fullclassname))[0];$stringidentifier = "datasource:$class";$stringcomponent = $component;$stringmanager = get_string_manager();if ($stringmanager->string_exists($stringidentifier, $stringcomponent)) {$name = get_string($stringidentifier, $stringcomponent);$helpid = ['name' => $stringidentifier, 'component' => $stringcomponent];} else if ($stringmanager->string_exists($stringidentifier, 'block_dash')) {$name = get_string($stringidentifier, 'block_dash');$helpid = ['name' => $stringidentifier, 'component' => 'block_dash'];} else {$name = '[[' . $stringidentifier . ']]';$helpid = [];}if ($help && !empty($helpid)) {return ($stringmanager->string_exists($helpid['name'].'_help', $helpid['component'])) ? $helpid : [];}return ($help) ? $helpid : $name;}/*** Add table to this data source. If the table is used in a join in the main query.** @param table $table*/public function add_table(table $table): void {$this->tables[$table->get_alias()] = $table;}/*** Get tables that are in this data source's main query.** @return array*/public function get_tables(): array {return $this->tables;}/*** Get table pagination class.* @return paginator*/public function get_paginator(): paginator {if ($this->get_layout()->supports_pagination()) {$perpage = (int) $this->get_preferences('perpage');}$perpage = isset($perpage) && !empty($perpage) ? $perpage : \block_dash\local\paginator::PER_PAGE_DEFAULT;if ($this->paginator == null) {$this->paginator = new paginator(function () {$count = $this->get_query()->count();if ($maxlimit = $this->get_max_limit()) {return $maxlimit < $count ? $maxlimit : $count;}return $count;}, 0, $perpage);}return $this->paginator;}/*** Get fully built query for execution.** @return builder*/final public function get_query(): builder {if (is_null($this->query)) {$this->query = $this->get_query_template();if (count($this->get_available_fields()) == 0) {throw new \moodle_exception('Cannot build empty query in data source.');}if ($this->get_filter_collection() && $this->get_filter_collection()->has_filters()) {list ($filtersql, $filterparams) = $this->get_filter_collection()->get_sql_and_params();$this->query->where_raw($filtersql[0], $filterparams);}$fields = $this->get_available_fields();foreach ($fields as $field) {if (is_null($field->get_select())) {continue;}$this->query->select($field->get_select(), $field->get_alias());}foreach ($this->get_available_fields() as $field) {if ($field->get_sort()) {$this->query->orderby($field->get_select(), strtoupper($field->get_sort_direction()));}}// If there are multiple identifiers in the data source, construct a unique column.// This is to prevent warnings when multiple rows have the same value in the first column.$identifierselects = [];foreach ($this->get_available_fields() as $field) {if ($field->has_attribute(identifier_attribute::class)) {$identifierselects[] = $field->get_select();}}global $DB;$concat = $DB->sql_concat_join("'-'", $identifierselects);if (count($identifierselects) > 1) {$this->query->select($concat, 'unique_id');}if ($this->get_layout()->supports_pagination()) {$perpage = $this->get_per_page();// Shorten per page if pagination will exceed max limit.if ($maxlimit = $this->get_max_limit()) {if ($this->get_paginator()->get_limit_from() + $perpage > $maxlimit) {$offset = $this->get_paginator()->get_limit_from() + $perpage - $maxlimit;$perpage = $perpage - $offset;}}$this->query->limitfrom($this->get_paginator()->get_limit_from())->limitnum($perpage);} else {$this->query->limitfrom(0);if ($maxlimit = $this->get_max_limit()) {$this->query->limitnum($maxlimit);}}if ($sorting = $this->get_sorting()) {foreach ($sorting as $field => $direction) {// Configured field is removed then remove the order.if (is_null($this->get_field($field))) {continue;}$this->query->orderby($this->get_field($field)->get_sort_select(), $direction);}}}return $this->query;}/*** Get filter collection for data grid. Build if necessary.** @return filter_collection_interface*/final public function get_filter_collection() {if (is_null($this->filtercollection)) {$this->filtercollection = $this->build_filter_collection();$this->filtercollection->init();if ($this->get_preferences('filters')) {foreach ($this->get_preferences('filters') as $filtername => $filterpreferences) {if (is_array($filterpreferences) || is_object($filterpreferences)) {if ($this->filtercollection->has_filter($filtername)) {$this->filtercollection->get_filter($filtername)->set_preferences($filterpreferences);}}}}}return $this->filtercollection;}/*** Modify objects before data is retrieved.*/public function before_data() {if ($this->get_layout()->supports_field_visibility()) {foreach ($this->get_available_fields() as $availablefield) {$availablefield->set_visibility(field_interface::VISIBILITY_HIDDEN);}if ($this->preferences && isset($this->preferences['available_fields'])) {foreach ($this->preferences['available_fields'] as $fieldname => $preferences) {if (isset($preferences['visible'])) {if ($field = $this->get_field($fieldname)) {$field->set_visibility($preferences['visible']);}}}}}if ($this->preferences && isset($this->preferences['filters'])) {$enabledfilters = [];foreach ($this->preferences['filters'] as $filtername => $preference) {if (isset($preference['enabled']) && $preference['enabled']) {$enabledfilters[] = $filtername;}}// No preferences set yet, remove all filters.foreach ($this->get_filter_collection()->get_filters() as $filter) {if (!in_array($filter->get_name(), $enabledfilters)) {$this->get_filter_collection()->remove_filter($filter);}}} else {// No preferences set yet, remove all filters.foreach ($this->get_filter_collection()->get_filters() as $filter) {$this->get_filter_collection()->remove_filter($filter);}}$this->get_layout()->before_data();}/*** Get data collection.** @return data_collection_interface* @throws \moodle_exception*/final public function get_data() {if (is_null($this->data)) {// If the block has no preferences do not query any data.if (empty($this->get_all_preferences())) {return block_dash_get_data_collection();}$this->before_data();if (!$strategy = $this->get_layout()->get_data_strategy()) {throw new coding_exception('Not fully configured.');}if ($this->is_widget()) {$this->data = $this->get_widget_data();} else {$records = $this->get_query()->query();$this->data = $strategy->convert_records_to_data_collection($records, $this->get_sorted_fields());if ($modifieddata = $this->after_data($this->data)) {$this->data = $modifieddata;}}}return $this->data;}/*** Modify objects after data is retrieved.** @param data_collection_interface $datacollection*/public function after_data(data_collection_interface $datacollection) {return $this->get_layout()->after_data($datacollection);}/*** Explicitly set layout.** @param layout_interface $layout*/public function set_layout(layout_interface $layout) {$this->layout = $layout;}/*** Get layout.** @return layout_interface*/public function get_layout() {if (is_null($this->layout)) {if ($layout = $this->get_preferences('layout')) {$this->layout = layout_factory::build_layout($layout, $this);}// If still null default to grid layout.if (is_null($this->layout)) {$this->layout = new grid_layout($this);}}return $this->layout;}/*** Get context.** @return \context*/public function get_context() {return $this->context;}/*** Get template variables.** @param \renderer_base $output* @return array|\renderer_base|\stdClass|string* @throws coding_exception*/final public function export_for_template(\renderer_base $output) {$data = $this->get_layout()->export_for_template($output);$data['datasource'] = $this;return $data;}/*** Add form fields to the block edit form. IMPORTANT: Prefix field names with config_ otherwise the values will* not be saved.** @param \moodleform $form* @param \MoodleQuickForm $mform* @throws coding_exception*/public function build_preferences_form(\moodleform $form, \MoodleQuickForm $mform) {if ($form->get_tab() == preferences_form::TAB_GENERAL) {$mform->addElement('static', 'data_source_name', get_string('datasource', 'block_dash'), $this->get_name());$mform->addElement('select', 'config_preferences[layout]', get_string('layout', 'block_dash'),layout_factory::get_layout_form_options());$mform->setType('config_preferences[layout]', PARAM_TEXT);}if ($layout = $this->get_layout()) {$layout->build_preferences_form($form, $mform);}if ($form->get_tab() == preferences_form::TAB_FIELDS) {$mform->addElement('html', '<hr>');$sortablefields = [];foreach ($this->get_available_fields() as $field) {if ($field->get_option('supports_sorting') !== false) {$sortablefields[$field->get_alias()] = $field->get_table()->get_title() . ': ' . $field->get_title();}}$mform->addElement('select', 'config_preferences[default_sort]', get_string('defaultsortfield', 'block_dash'),$sortablefields);$mform->setType('config_preferences[default_sort]', PARAM_TEXT);$mform->addHelpButton('config_preferences[default_sort]', 'defaultsortfield', 'block_dash');$mform->addElement('select', 'config_preferences[default_sort_direction]',get_string('defaultsortdirection', 'block_dash'), [ 'asc' => 'ASC', 'desc' => 'DESC']);$mform->setType('config_preferences[default_sort_direction]', PARAM_TEXT);$mform->addElement('text', 'config_preferences[maxlimit]', get_string('maxlimit', 'block_dash'));$mform->setType('config_preferences[maxlimit]', PARAM_INT);$mform->addHelpButton('config_preferences[maxlimit]', 'maxlimit', 'block_dash');$mform->addElement('text', 'config_preferences[perpage]', get_string('perpage', 'block_dash'));$mform->setType('config_preferences[perpage]', PARAM_INT);$mform->addHelpButton('config_preferences[perpage]', 'perpage', 'block_dash');}}// Region Preferences./*** Get a specific preference.** @param string $name* @return mixed|array*/final public function get_preferences($name) {if ($this->preferences && isset($this->preferences[$name])) {return $this->preferences[$name];}return null;}/*** Get all preferences associated with the data source.** @return array*/final public function get_all_preferences() {return $this->preferences;}/*** Set preferences on this data source.** @param array $preferences*/final public function set_preferences(array $preferences) {$this->preferences = $preferences;}// Endregion./*** Get count query template.** @return string*/public function get_count_query_template() {return $this->get_query_template();}/*** Get group by fields.** @return string*/public function get_groupby() {return false;}/*** Get available field definitions.** @return field_interface[]*/final public function get_available_fields() {if (is_null($this->fields)) {// Get all available field definitions based on tables.$this->fields = [];foreach ($this->get_tables() as $table) {foreach ($table->get_fields() as $field) {$this->fields[$field->get_alias()] = $field;}}}return $this->fields;}/*** Check if report has a certain field** @param string $alias Field alias.* @return bool*/public function has_field(string $alias): bool {return isset($this->get_available_fields()[$alias]);}/*** Get field by name. Returns null if not found.** @param string $alias Field alias.* @return ?field_interface*/public function get_field(string $alias): ?field_interface {// Fields are keyed by name.if ($this->has_field($alias)) {return $this->get_available_fields()[$alias];}return null;}/*** Get sorted field definitions based on preferences.** @return field_interface[]*/public function get_sorted_fields() {if (is_null($this->sortedfields)) {$fields = $this->get_available_fields();;if ($this->get_layout()->supports_field_visibility()) {$sortedfields = [];// First add the identifiers, in order, so they always come first in the query.foreach ($fields as $key => $field) {if ($field->has_attribute(identifier_attribute::class)) {$sortedfields[] = $field;unset($fields[$key]);}}if ($availablefields = $this->get_preferences('available_fields')) {foreach ($availablefields as $fieldalias => $availablefield) {foreach ($fields as $key => $field) {if ($field->get_alias() == $fieldalias) {$sortedfields[] = $field;unset($fields[$key]);break;}}}foreach ($fields as $field) {$sortedfields[] = $field;}$fields = $sortedfields;}}$this->sortedfields = array_values($fields);}return $this->sortedfields;}/*** Get sorting.** @throws coding_exception*/public function get_sorting() {global $USER;if ($this->get_layout()->supports_sorting() && $this->get_block_instance()) {$cache = \cache::make_from_params(\cache_store::MODE_SESSION, 'block_dash', 'sort');if ($cache->has($USER->id . '_' . $this->get_block_instance()->instance->id)) {return $cache->get($USER->id . '_' . $this->get_block_instance()->instance->id);}}if ($defaultsort = $this->get_preferences('default_sort')) {return [$defaultsort => $this->get_preferences('default_sort_direction') ?? 'asc'];}return [];}/*** Get maximum number of records to query.** @return ?int*/public function get_max_limit() {return $this->get_preferences('maxlimit');}/*** Get per page number for pagination.** @return ?int*/public function get_per_page() {if ($perpage = $this->get_preferences('perpage')) {return $perpage;}return $this->get_paginator()->get_per_page();}/*** Set block instance.** @param \block_base $blockinstance*/public function set_block_instance(\block_base $blockinstance) {$this->blockinstance = $blockinstance;}/*** Get block instance.** @return null|\block_base*/public function get_block_instance() {return $this->blockinstance;}/*** Update the block fetched data before render.** @param array $data* @return void*/public function update_data_before_render(&$data) {return null;}/*** Set the data source supports debug.** @return bool;*/public function supports_debug() {return true;}/*** Type of the data source.** @return boolean*/public function is_widget() {return false;}}