Skip to content

Commit

Permalink
DASH-156 improve query building and remove old data_grid code
Browse files Browse the repository at this point in the history
  • Loading branch information
moodledev committed Oct 7, 2020
1 parent 8c013b9 commit c3a5b44
Show file tree
Hide file tree
Showing 28 changed files with 534 additions and 1,202 deletions.
57 changes: 56 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -134,14 +134,67 @@ The following field definition transforms `1,5,123` into `Group 1, Group 5, Grou
]
```

# Dash Framework

Dash comprises smaller components that live inside the Dash Framework. These components generic, decoupled, and extendable APIs. A Dash Framework API must be unit tested and well documented.

Dash Framework APIs live in the `<component>\local\dash_framework` namespace of a plugin.

### List of standard Dash Framework APIs

#### [Query builder](#query-builder)

Generate SQL queries to be run by Moodle's Data API

#### [Filtering](#filtering)

Create generic filters.

### Creating a new Dash Framework API

#### Step 1 - Determine if your code should be an API

Any reusable chunk of functionality can be used as an API. Think of the Moodle File API or Custom Field API. These APIs provide generic functionality to be utilized in specific ways.

Unit testing will also reveal how portable your API code is. If you cannot test without tightly coupled dependencies, then perhaps the code should be business logic inside of a plugin, rather than a reusable API.

#### Step 2 - Create a new folder for your framework API classes

Add a new folder in `<pluginfolder>/classes/local/dash_framework`

As an example: `blocks/dash/classes/local/dash_framework/result_cache`

#### Step 3 - Create unit tests

Prefix your unit test class with `dash_framework_` and try to keep all tests within a single class. Follow this convention:

`<pluginfolder>/tests/dash_framework_result_cache_test.php`

Strive for full code coverage on your API and make changes that are backwards compatible.

#### Step 4 - Write the API code

"API" is used loosely in this documentation. Create friendly and easy to use PHP classes within your framework namespace. Here's a simple example:

```php
namespace block_dash\local\dash_framework\result_cache;

interface cacher {

public function set(string $cache_identifier, array $datatocache): void; // Use typehints and "SOLID" practices.

public function get(string $cache_identifier): array; // Public functions should be easy to use methods of consuming.
}
```

## Query builder

Under the hood Dash builds queries using a strict API for constructing queries. This API is decoupled from the rest of a dash's lifecycle.

Simple example:

```php
use block_dash\local\query_builder\builder;
use block_dash\local\dash_framework\query_builder\builder;

$builder = new builder();
$results = $builder->select('c.id', 'c_id') // Column aliasing.
Expand All @@ -162,6 +215,8 @@ Results caching works by taking a snapshot of a query result. And storing the fo
* When the query results were cached
* When to invalidate the cache and query database

## Filtering

## Change log

### 1.0.2
Expand Down
3 changes: 2 additions & 1 deletion block_dash.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ public function get_content() {

$this->content = $bb->get_block_content();
} catch (\Exception $e) {
$this->content->text = $OUTPUT->notification($e->getMessage(), 'error');
throw $e;
//$this->content->text = $OUTPUT->notification($e->getMessage() . $e->getTraceAsString(), 'error');
}

return $this->content;
Expand Down
3 changes: 1 addition & 2 deletions classes/external.php
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,7 @@ public static function get_block_content($block_instance_id, $filter_form_data,
->apply_filter($filter['name'], $filter['value']);
}

$datagrid = $bb->get_configuration()->get_data_source()->get_data_grid();
$datagrid->get_paginator()->set_current_page($params['page']);
$bb->get_configuration()->get_data_source()->get_paginator()->set_current_page($params['page']);

return ['html' => $renderer->render_data_source($bb->get_configuration()->get_data_source())];
}
Expand Down
9 changes: 4 additions & 5 deletions classes/local/block_builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@

use block_dash\local\configuration\configuration_interface;
use block_dash\local\configuration\configuration;
use block_dash\local\data_grid\sql_data_grid;
use block_dash\output\query_debug;
use block_dash\output\renderer;

Expand Down Expand Up @@ -87,7 +86,7 @@ public function get_block_content() {
if ($this->configuration->is_fully_configured()) {
$bb = self::create($this->blockinstance);

$bb->get_configuration()->get_data_source()->get_data_grid()->get_paginator()->set_current_page(0);
$bb->get_configuration()->get_data_source()->get_paginator()->set_current_page(0);

$text .= $OUTPUT->render_from_template('block_dash/block', [
'preloaded' => $renderer->render_data_source($bb->get_configuration()->get_data_source()),
Expand All @@ -97,9 +96,9 @@ public function get_block_content() {
has_capability('block/dash:addinstance', $this->blockinstance->context)
]);

$datagrid = $bb->get_configuration()->get_data_source()->get_data_grid();
if (is_siteadmin() && $datagrid instanceof sql_data_grid) {
$text .= $renderer->render(new query_debug($datagrid->get_sql_and_params()[0], $datagrid->get_sql_and_params()[1]));
if (is_siteadmin()) {
[$sql, $params] = $bb->get_configuration()->get_data_source()->get_query()->get_sql_and_params();
$text .= $renderer->render(new query_debug($sql, $params));
}
} else {
$text .= \html_writer::tag('p', get_string('editthisblock', 'block_dash'));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,15 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace block_dash\local\query_builder;
namespace block_dash\local\dash_framework\query_builder;

use coding_exception;
use dml_exception;

/**
* Builds a query.
*
* @package block_dash\local\query_builder
* @package block_dash\local\dash_framework\query_builder
*/
class builder {

Expand All @@ -54,6 +54,16 @@ class builder {
*/
private $wheres = [];

/**
* @var string
*/
private $rawwhere;

/**
* @var array
*/
private $rawwhereparameters;

/**
* @var int Return a subset of records, starting at this point (optional).
*/
Expand All @@ -69,6 +79,23 @@ class builder {
*/
private $orderby = [];

/**
* @var join[]
*/
private $joins = [];

/**
* Extra conditions to be added in WHERE clause.
*
* @var array
*/
private $rawconditions = [];

/**
* @var array
*/
private $rawconditionparameters = [];

/**
* @param string $field
* @param string $alias
Expand All @@ -79,6 +106,20 @@ public function select(string $field, string $alias): builder {
return $this;
}

/**
* Set all selects on builder.
*
* @param array $selects [alias => field, ...]
* @return $this
*/
public function set_selects(array $selects): builder {
$this->selects = [];
foreach ($selects as $alias => $select) {
$this->selects[$alias] = $select;
}
return $this;
}

/**
* Set main table of query.
*
Expand All @@ -92,6 +133,48 @@ public function from(string $table, string $alias): builder {
return $this;
}

/**
* Join table in query.
*
* @param string $table Table name of joined table.
* @param string $alias Joined table alias.
* @param string $jointablefield Field of joined table to reference in join condition.
* @param string $origintablefield Field of origin table to join to.
* @param string $jointype SQL join type. See self::TYPE_*
* @param array $extraparameters Extra parameters used in join SQL.
* @return $this
*/
public function join(string $table, string $alias, string $jointablefield, string $origintablefield,
$jointype = join::TYPE_INNER_JOIN, array $extraparameters = []): builder {
$this->joins[] = new join($table, $alias, $jointablefield, $origintablefield, $jointype, $extraparameters);
return $this;
}

/**
* Add additional join condition to existing join.
*
* @param string $alias
* @param string $condition
* @return $this
* @throws coding_exception
*/
public function join_condition(string $alias, string $condition): builder {
$added = false;
foreach ($this->joins as $join) {
if ($join->get_alias() == $alias) {
$join->add_join_condition($condition);
$added = true;
break;
}
}

if (!$added) {
throw new coding_exception('Table alias not found: ' . $alias);
}

return $this;
}

/**
* Add where clause to query.
*
Expand Down Expand Up @@ -121,6 +204,16 @@ public function where_in_query(string $selector, string $query, array $params =
return $where;
}

/**
* @param string $wheresql
* @return builder
*/
public function where_raw(string $wheresql, array $parameters = []): builder {
$this->rawwhere = $wheresql;
$this->rawwhereparameters = $parameters;
return $this;
}

/**
* Order by a field.
*
Expand All @@ -138,6 +231,19 @@ public function orderby(string $field, string $direction): builder {
return $this;
}

/**
* Add raw condition to builder.
*
* @param string $condition
* @param array $parameters
* @return builder
*/
public function rawcondition(string $condition, array $parameters = []): builder {
$this->rawconditions[] = $condition;
$this->rawconditionparameters = $parameters;
return $this;
}

/**
* @return where[]
*/
Expand Down Expand Up @@ -212,17 +318,28 @@ protected function get_where_sql_and_params(): array {
$params = array_merge($params, $wparams);
}

if ($this->rawwhere) {
$wheresql[] = $this->rawwhere;
$params = array_merge($params, $this->rawwhereparameters);
}

return [implode(' AND ', $wheresql), $params];
}

/**
* @return array<string, array>
* @throws exception\invalid_operator_exception
*/
protected function get_sql_and_params(): array {
final public function get_sql_and_params(): array {
$sql = 'SELECT DISTINCT ' . $this->build_select() . ' FROM {' . $this->table . '} ' . $this->tablealias;
$params = [];

foreach ($this->joins as $join) {
[$jsql, $jparams] = $join->get_sql_and_params();
$sql .= ' ' . $jsql . ' ';
$params = array_merge($params, $jparams);
}

[$wsql, $wparams] = $this->get_where_sql_and_params();

if ($wsql) {
Expand Down Expand Up @@ -256,4 +373,19 @@ public function query() {

return $DB->get_records_sql($sql, $params, $this->get_limitfrom(), $this->get_limitnum());
}

/**
* Get number of records this query will return.
*
* @return int
* @throws dml_exception
* @throws exception\invalid_operator_exception
*/
public function count(): int {
$builder = clone $this;
$builder->set_selects(['count' => 'COUNT(DISTINCT ' . $this->tablealias . '.id)']);
$builder->limitfrom(0)->limitnum(0);
$records = $builder->query();
return array_values($records)[0]->count;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace block_dash\local\query_builder\exception;
namespace block_dash\local\dash_framework\query_builder\exception;

use moodle_exception;

/**
* Thrown when a where condition does not have any values.
*
* @package block_dash\local\query_builder
* @package block_dash\local\dash_framework\query_builder
*/
class invalid_operator_exception extends moodle_exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,14 @@
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/

namespace block_dash\local\query_builder\exception;
namespace block_dash\local\dash_framework\query_builder\exception;

use moodle_exception;

/**
* Thrown when a where condition does not have any values.
*
* @package block_dash\local\query_builder
* @package block_dash\local\dash_framework\query_builder
*/
class invalid_where_clause_exception extends moodle_exception {

Expand Down
Loading

0 comments on commit c3a5b44

Please sign in to comment.