Implement a bunch of stuff.

Things are coming along fast!
This commit is contained in:
Starbeamrainbowlabs 2019-01-15 15:46:24 +00:00
parent 66eeb9010d
commit cef578d7eb
Signed by: sbrl
GPG key ID: 1BE5172E637709C2
14 changed files with 293 additions and 24 deletions

View file

@ -17,6 +17,9 @@ $autoloader->register();
$di_builder = new DI\ContainerBuilder(); $di_builder = new DI\ContainerBuilder();
$di_builder->addDefinitions("di_config.php"); $di_builder->addDefinitions("di_config.php");
// TODO: In production, we should use something like this - see http://php-di.org/doc/container-configuration.html
// $builder->enableCompilation(__DIR__ . '/tmp');
// $builder->writeProxiesToFile(true, __DIR__ . '/tmp/proxies');
$di_container = $di_builder->build(); $di_container = $di_builder->build();

View file

@ -3,6 +3,13 @@
use Psr\Container\ContainerInterface; use Psr\Container\ContainerInterface;
use SBRL\TomlConfig; use SBRL\TomlConfig;
use AirQuality\Repositories\IDeviceRepository;
use AirQuality\Repositories\MariaDBDeviceRepository;
use AirQuality\Repositories\IMeasurementDataRepository;
use AirQuality\Repositories\MariaDBMeasurementDataRepository;
use AirQuality\Repositories\IMeasurementTypeRepository;
use AirQuality\Repositories\MariaDBMeasurementTypeRepository;
return [ return [
"settings.file_default" => "data/settings.toml", "settings.file_default" => "data/settings.toml",
"settings.file_custom" => "settings.default.toml", "settings.file_custom" => "settings.default.toml",
@ -14,5 +21,10 @@ return [
); );
}, },
IDeviceRepository::class => DI\autowire(MariaDBDeviceRepository::class),
IMeasurementDataRepository::class => DI\autowire(MariaDBMeasurementDataRepository::class),
IMeasurementTypeRepository::class => DI\autowire(MariaDBMeasurementTypeRepository::class),
\SBRL\SessionManager::class => DI\factory([ \SBRL\SessionManager::class, "get_instance" ]) \SBRL\SessionManager::class => DI\factory([ \SBRL\SessionManager::class, "get_instance" ])
]; ];

View file

@ -2,34 +2,84 @@
namespace AirQuality\Actions; namespace AirQuality\Actions;
use \SBRL\TomlConfig;
use \AirQuality\Repositories\IMeasurementDataRepository;
use \AirQuality\Repositories\IMeasurementTypeRepository;
use \AirQuality\ApiResponseSender;
class FetchData { use \AirQuality\Validator;
use \AirQuality\PerfFormatter;
class FetchData implements IAction {
/** @var TomlConfig */
private $settings; private $settings;
/** @var IMeasurementDataRepository */
private $measurement_repo;
/** @var IMeasurementTypeRepository */
private $type_repo;
/** @var ApiResponseSender */
private $sender;
/** @var Validator */
private $validator; private $validator;
public function __construct( public function __construct(
\SBRL\TomlConfig $in_settings) { TomlConfig $in_settings,
IMeasurementDataRepository $in_measurement_repo,
IMeasurementTypeRepository $in_type_repo,
ApiResponseSender $in_sender) {
$this->settings = $in_settings; $this->settings = $in_settings;
$this->validator = new \AirQuality\Validator($_GET); $this->measurement_repo = $in_measurement_repo;
$this->type_repo = $in_type_repo;
$this->sender = $in_sender;
$this->validator = new Validator($_GET);
} }
public function handle() : Boolean { public function handle() : boolean {
global $start_time; global $start_time;
$start_handle = microtime(true);
// 1: Validate params // 1: Validate params
$this->validator->is_datetime("datetime"); $this->validator->is_datetime("datetime");
$this->validator->exists("reading_type");
$this->validator->is_max_length("reading_type", 256);
$this->validator->run(); $this->validator->run();
if(!$this->type_repo->is_valid_type($_GET["reading_type"])) {
$this->sender->send_error_plain(
400, "Error: That reading type is invalid.", [
[ "x-time-taken", PerfFormatter::format_perf_data($start_time, $start_handle, null) ]
]
);
return false;
}
// 2: Pull data from database // 2: Pull data from database
$data = $this->measurement_repo->get_readings_by_date(
new \DateTime($_GET["datetime"]),
$_GET["reading_type"]
);
// 2.5: Validate data from database // 2.5: Validate data from database
if(empty($data)) {
http_response_code(404);
header("content-type: text/plain");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, null));
echo("Error: No data could be found for that timestamp.");
return false;
}
// 3: Serialise data // 3: Serialise data
$response = json_encode("Coming soon"); $start_encode = microtime(true);
$response = json_encode($data);
// 4: Send response // 4: Send response
header("x-time-taken: " . (microtime(true) - $start_time) . "ms");
header("content-type: application/json"); header("content-type: application/json");
header("x-time-taken: " . PerfFormatter::format_perf_data($start_time, $start_handle, $start_encode));
echo($response); echo($response);
return true; return true;
} }

View file

@ -0,0 +1,7 @@
<?php
namespace AirQuality\Actions;
interface IAction {
public function handle() : boolean;
}

View file

@ -0,0 +1,24 @@
<?php
namespace AirQuality;
/**
* Automates the sending of API responses.
*/
class ApiResponseSender
{
function __construct() {
}
public function send_error_plain($code, $message, $extra_headers = []) {
http_response_code($code);
header("content-type: text/plain");
header("content-length: " . strlen($message)); // strlen isn't multibyte-safe, so it'll give us the actual length of the string in bytes, not characters - which is precisely what we want here :D
foreach($extra_headers as $header)
header("{$header[0]}: {$header[1]}");
echo($message);
}
}

View file

@ -14,10 +14,10 @@ class Database
private $pdo_options = [ private $pdo_options = [
// https://devdocs.io/php/pdo.setattribute // https://devdocs.io/php/pdo.setattribute
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, \PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => false, \PDO::ATTR_EMULATE_PREPARES => false,
PDO::ATTR_AUTOCOMMIT => false \PDO::ATTR_AUTOCOMMIT => false
]; ];
function __construct(\SBRL\TomlConfig $in_settings) { function __construct(\SBRL\TomlConfig $in_settings) {
@ -29,6 +29,7 @@ class Database
$this->get_connection_string(), $this->get_connection_string(),
$this->settings->get("database.username"), $this->settings->get("database.username"),
$this->settings->get("database.password"), $this->settings->get("database.password"),
$this->pdo_options
); );
// Make this connection read-only // Make this connection read-only
if(self::read_only) if(self::read_only)

25
logic/PerfFormatter.php Normal file
View file

@ -0,0 +1,25 @@
<?php
/**
* Formats performance data to be sent in a HTTP header.
*/
class PerfFormatter {
function __construct() {
}
public static function format_perf_data($start_init, $start_handle, $start_encode) {
$now = microtime(true);
$time_total = round($now - $start_init, 2);
$time_setup = round($start_handle - $start_init, 2);
$time_handle = round(($start_encode ?? $now) - $start_handle, 2);
$time_encode = round($now - $start_encode, 2);
$result = "{$time_total}ms total | {$time_setup}ms setup, {$time_handle}ms handle";
if(!empty($start_encode))
$result .= ", {$time_encode}ms encode";
return $result;
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace AirQuality\Repositories;
interface IDeviceRepository {
}

View file

@ -3,5 +3,5 @@
namespace AirQuality\Repositories; namespace AirQuality\Repositories;
interface IMeasurementDataRepository { interface IMeasurementDataRepository {
public function get_readings_by_date(DateTime $datetime, string $reading_type); public function get_readings_by_date(\DateTime $datetime, string $reading_type);
} }

View file

@ -3,6 +3,23 @@
namespace AirQuality\Repositories; namespace AirQuality\Repositories;
interface IMeasurementTypeRepository { interface IMeasurementTypeRepository {
public function is_valid_type(); /**
* Returns whether the specified type is valid or not.
* @param string $type_name The name of the type to validate.
* @return boolean Whether the specified type name is valid or not.
*/
public function is_valid_type(string $type_name);
/**
* Gets the friendly name for the specified type name.
* @param string $type_name The type name to get the friendly name for.
* @return string The friendly name for the specified type name.
*/
public function get_friendly_name(string $type_name);
/**
* Returns all the currently known meeasurement types.
* @return array All the measurement types currently known.
*/
public function get_all_types(); public function get_all_types();
} }

View file

@ -0,0 +1,29 @@
<?php
namespace AirQuality\Repositories;
/**
* Fetches device info from a MariaDB database.
*/
class MariaDBDeviceRepository implements IDeviceRepository {
public static $table_name = "devices";
public static $column_device_id = "device_id";
public static $column_device_name = "device_name";
public static $column_device_type = "device_type";
public static $column_owner_id = "owner_id";
public static $column_lat = "device_latitude";
public static $column_long = "device_longitude";
// ------------------------------------------------------------------------
/**
* The database connection.
* @var \AirQuality\Database
*/
private $database;
function __construct(\AirQuality\Database $in_database) {
$this->database = $in_database;
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace AirQuality\Repositories;
/**
* Fetches measurement readings from a MariaDB database.
*/
class MariaDBMeasurementDataRepository implements IMeasurementDataRepository {
public static $table_name_metadata = "readings";
public static $table_name_values = "reading_values";
public static $column_values_id = "id";
public static $column_values_reading_id = "reading_id";
public static $column_values_value = "value";
public static $column_values_reading_value_types_id = "reading_value_types_id";
public static $column_metadata_id = "id";
public static $column_metadata_storedon = "storedon";
public static $column_metadata_recordedon = "recordedon";
public static $column_metadata_device_id = "device_id";
public static $column_metadata_lat = "reading_latitude";
public static $column_metadata_long = "reading_longitude";
// ------------------------------------------------------------------------
/**
* The database connection.
* @var \AirQuality\Database
*/
private $database;
function __construct(\AirQuality\Database $in_database) {
$this->database = $in_database;
}
public function get_readings_by_date(\DateTime $datetime, string $reading_type) {
return $this->database->query(
"SELECT
$this->table_name_values.*,
$this->table_name_metadata.device_id,
COALESCE(
$this->table_name_metadata.$this->column_metadata_recordedon,
$this->table_name_metadata.$this->column_metadata_storedon
) AS datetime,
COALESCE(
$this->table_name_metadata.$this->column_metadata_lat,
{MariaDBDeviceRepository::$table_name}.device_latitude
) AS latitude,
COALESCE(
$this->table_name_metadata.$this->column_metadata_long,
devices.device_longitude
) AS longitude
FROM $this->table_name_values
JOIN $this->table_name_metadata ON $this->table_name_values.$this->column_values_reading_id = $this->table_name_metadata.id
JOIN devices ON $this->table_name_metadata.$this->column_metadata_device_id = devices.device_id
WHERE COALESCE(
$this->table_name_metadata.$this->column_metadata_recordedon,
$this->table_name_metadata.$this->column_metadata_storedon
) = :datetime",
[
"datetime" => $datetime,
"reading_type" => $reading_type
]
);
}
}

View file

@ -0,0 +1,39 @@
<?php
namespace AirQuality\Repositories;
/**
* Fetches device info from a MariaDB database.
*/
class MariaDBMeasurementTypeRepository implements IMeasurementTypeRepository {
public static $table_name = "reading_value_types";
public static $column_id = "id";
public static $column_friendly_text = "friendly_text";
// ------------------------------------------------------------------------
/**
* The database connection.
* @var \AirQuality\Database
*/
private $database;
function __construct(\AirQuality\Database $in_database) {
$this->database = $in_database;
}
public function is_valid_type(string $type_name) {
throw new Exception("Error: Not implemented yet :-\\");
}
public function get_friendly_name(string $type_name) {
// TODO: Cache results here? Maybe https://packagist.org/packages/thumbtack/querycache will be of some use
throw new Exception("Error: Not implemented yet :-\\");
}
public function get_all_types() {
throw new Exception("Error: Not implemented yet :-\\");
}
}

View file

@ -66,17 +66,6 @@ class Validator {
); );
} }
public function is_valid_email($key) {
$this->add_test(
$key,
function($data) {
$validator = new \EmailValidator\Validator();
return $validator->isValid($data);
},
400,
"That email address isn't valid."
);
}
public function is_datetime($key) { public function is_datetime($key) {
$this->add_test( $this->add_test(