Merged in feature/MAPG-43-workout-concept-for-handling-db (pull request #54)

Feature/MAPG-43 workout concept for handling db
This commit is contained in:
Bence Pőcze 2020-05-28 22:21:29 +00:00
commit 647f5a1392
21 changed files with 1062 additions and 108 deletions

View File

@ -4,8 +4,7 @@
"description": "MapGuesser Application", "description": "MapGuesser Application",
"license": "GNU GPL 3.0", "license": "GNU GPL 3.0",
"require": { "require": {
"vlucas/phpdotenv": "^4.1", "vlucas/phpdotenv": "^4.1"
"romanpitak/php-rest-client": "^1.2"
}, },
"require-dev": {}, "require-dev": {},
"autoload": { "autoload": {

47
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "3778b9431ef3d22705bdbf1653102a1a", "content-hash": "29431cf83ee884f01ee954b1068c0ccc",
"packages": [ "packages": [
{ {
"name": "phpoption/phpoption", "name": "phpoption/phpoption",
@ -61,51 +61,6 @@
], ],
"time": "2020-03-21T18:07:53+00:00" "time": "2020-03-21T18:07:53+00:00"
}, },
{
"name": "romanpitak/php-rest-client",
"version": "v1.2.1",
"source": {
"type": "git",
"url": "https://github.com/romanpitak/PHP-REST-Client.git",
"reference": "728b6c44040a13daeb8033953f5f3cdd3dde1980"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/romanpitak/PHP-REST-Client/zipball/728b6c44040a13daeb8033953f5f3cdd3dde1980",
"reference": "728b6c44040a13daeb8033953f5f3cdd3dde1980",
"shasum": ""
},
"require": {
"ext-curl": "*",
"php": ">=5.3.3"
},
"type": "library",
"autoload": {
"psr-4": {
"RestClient\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Roman Piták",
"email": "roman@pitak.net",
"homepage": "http://pitak.net",
"role": "Developer"
}
],
"description": "REST client library.",
"homepage": "https://github.com/romanpitak/PHP-REST-Client",
"keywords": [
"client",
"http",
"rest"
],
"time": "2015-02-19T15:32:16+00:00"
},
{ {
"name": "symfony/polyfill-ctype", "name": "symfony/polyfill-ctype",
"version": "v1.17.0", "version": "v1.17.0",

View File

@ -15,4 +15,11 @@ if (!empty($_ENV['DEV'])) {
ini_set('display_errors', '0'); ini_set('display_errors', '0');
} }
class Container
{
static MapGuesser\Interfaces\Database\IConnection $dbConnection;
}
Container::$dbConnection = new MapGuesser\Database\Mysql\Connection($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
session_start(); session_start();

View File

@ -1,8 +0,0 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\View\ViewBase;
interface ControllerInterface
{
public function run(): ViewBase;
}

View File

@ -1,15 +1,15 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Controller\IController;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Bounds; use MapGuesser\Util\Geo\Bounds;
use MapGuesser\View\HtmlView; use MapGuesser\View\HtmlView;
use MapGuesser\View\JsonView; use MapGuesser\View\JsonView;
use MapGuesser\View\ViewBase; use MapGuesser\Interfaces\View\IView;
use mysqli;
class GameController implements ControllerInterface class GameController implements IController
{ {
private mysqli $mysql;
private bool $jsonResponse; private bool $jsonResponse;
// demo map // demo map
@ -17,12 +17,10 @@ class GameController implements ControllerInterface
public function __construct($jsonResponse = false) public function __construct($jsonResponse = false)
{ {
$this->mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
$this->jsonResponse = $jsonResponse; $this->jsonResponse = $jsonResponse;
} }
public function run(): ViewBase public function run(): IView
{ {
$bounds = $this->getMapBounds(); $bounds = $this->getMapBounds();
@ -45,10 +43,11 @@ class GameController implements ControllerInterface
private function getMapBounds(): Bounds private function getMapBounds(): Bounds
{ {
$stmt = $this->mysql->prepare('SELECT bound_south_lat, bound_west_lng, bound_north_lat, bound_east_lng FROM maps WHERE id=?'); $select = new Select(\Container::$dbConnection, 'maps');
$stmt->bind_param("i", $this->mapId); $select->columns(['bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng']);
$stmt->execute(); $select->whereId($this->mapId);
$map = $stmt->get_result()->fetch_assoc();
$map = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']); $bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);

View File

@ -1,27 +1,22 @@
<?php namespace MapGuesser\Controller; <?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Controller\IController;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Util\Geo\Position; use MapGuesser\Util\Geo\Position;
use MapGuesser\View\JsonView; use MapGuesser\View\JsonView;
use MapGuesser\View\ViewBase; use MapGuesser\Interfaces\View\IView;
use mysqli;
use RestClient\Client;
class PositionController implements ControllerInterface class PositionController implements IController
{ {
const NUMBER_OF_ROUNDS = 5; const NUMBER_OF_ROUNDS = 5;
const MAX_SCORE = 1000; const MAX_SCORE = 1000;
private mysqli $mysql;
// demo map // demo map
private int $mapId = 1; private int $mapId = 1;
public function __construct() public function run(): IView
{
$this->mysql = new mysqli($_ENV['DB_HOST'], $_ENV['DB_USER'], $_ENV['DB_PASSWORD'], $_ENV['DB_NAME']);
}
public function run(): ViewBase
{ {
if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $this->mapId) { if (!isset($_SESSION['state']) || $_SESSION['state']['mapId'] !== $this->mapId) {
$data = ['error' => 'No valid session found!']; $data = ['error' => 'No valid session found!'];
@ -120,45 +115,34 @@ class PositionController implements ControllerInterface
private function selectNewPlace(array $exclude): array private function selectNewPlace(array $exclude): array
{ {
$condition = ''; $select = new Select(\Container::$dbConnection, 'places');
$params = ['i', &$this->mapId]; $select->columns(['id', 'lat', 'lng']);
if (($numExcluded = count($exclude)) > 0) { $select->where('id', 'NOT IN', $exclude);
$condition .= ' AND id NOT IN (' . implode(',', array_fill(0, $numExcluded, '?')) . ')'; $select->where('map_id', '=', $this->mapId);
$params[0] .= str_repeat('i', $numExcluded);
foreach ($exclude as &$placeId) {
$params[] = &$placeId;
}
}
$stmt = $this->mysql->prepare('SELECT COUNT(*) AS num FROM places WHERE map_id=? ' . $condition . ''); $numberOfPlaces = $select->count();
call_user_func_array([$stmt, 'bind_param'], $params);
$stmt->execute();
$numberOfPlaces = $stmt->get_result()->fetch_assoc()['num'];
$randomOffset = random_int(0, $numberOfPlaces - 1); $randomOffset = random_int(0, $numberOfPlaces - 1);
$params[0] .= 'i'; $select->orderBy('id');
$params[] = &$randomOffset; $select->limit(1, $randomOffset);
$stmt = $this->mysql->prepare('SELECT id, lat, lng FROM places WHERE map_id=? ' . $condition . ' ORDER BY id LIMIT 1 OFFSET ?'); $place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
call_user_func_array([$stmt, 'bind_param'], $params);
$stmt->execute();
return $stmt->get_result()->fetch_assoc(); return $place;
} }
private function getPanorama(Position $position): ?string private function getPanorama(Position $position): ?string
{ {
$query = [ $request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'], 'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $position->getLat() . ',' . $position->getLng(), 'location' => $position->getLat() . ',' . $position->getLng(),
'source' => 'outdoor' 'source' => 'outdoor'
]; ]);
$client = new Client('https://maps.googleapis.com/maps/api/streetview'); $response = $request->send();
$request = $client->newRequest('metadata?' . http_build_query($query));
$response = $request->getResponse();
$panoData = json_decode($response->getParsedResponse(), true); $panoData = json_decode($response->getBody(), true);
if ($panoData['status'] !== 'OK') { if ($panoData['status'] !== 'OK') {
return null; return null;
@ -172,10 +156,10 @@ class PositionController implements ControllerInterface
return $realPosition->calculateDistanceTo($guessPosition); return $realPosition->calculateDistanceTo($guessPosition);
} }
private function calculateScore(float $distance, float $area) private function calculateScore(float $distance, float $area): int
{ {
$goodness = 1.0 - ($distance / sqrt($area)); $goodness = 1.0 - ($distance / sqrt($area));
return round(pow(static::MAX_SCORE, $goodness)); return (int) round(pow(static::MAX_SCORE, $goodness));
} }
} }

View File

@ -0,0 +1,114 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Database\IStatement;
use mysqli;
class Connection implements IConnection
{
private mysqli $connection;
public function __construct(string $host, string $user, string $password, string $db, int $port = -1, string $socket = null)
{
if ($port < 0) {
$port = ini_get('mysqli.default_port');
}
if ($socket === null) {
$socket = ini_get('mysqli.default_socket');
}
$this->connection = new mysqli($host, $user, $password, $db, $port, $socket);
if ($this->connection->connect_error) {
throw new \Exception('Connection failed: ' . $this->connection->connect_error);
}
if (!$this->connection->set_charset('utf8mb4')) {
throw new \Exception($this->connection->error);
}
}
public function __destruct()
{
$this->connection->close();
}
public function startTransaction(): void
{
if (!$this->connection->autocommit(false)) {
throw new \Exception($this->connection->error);
}
}
public function commit(): void
{
if (!$this->connection->commit() || !$this->connection->autocommit(true)) {
throw new \Exception($this->connection->error);
}
}
public function rollback(): void
{
if (!$this->connection->rollback() || !$this->connection->autocommit(true)) {
throw new \Exception($this->connection->error);
}
}
public function query(string $query): ?IResultSet
{
if (!($result = $this->connection->query($query))) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
if ($result !== true) {
return new ResultSet($result);
}
return null;
}
public function multiQuery(string $query): array
{
if (!$this->connection->multi_query($query)) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
$ret = [];
do {
if ($result = $this->connection->store_result()) {
$ret[] = new ResultSet($result);
} else {
$ret[] = null;
}
$this->connection->more_results();
} while ($this->connection->next_result());
if ($this->connection->error) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
return $ret;
}
public function prepare(string $query): IStatement
{
if (!($stmt = $this->connection->prepare($query))) {
throw new \Exception($this->connection->error . '. Query: ' . $query);
}
return new Statement($stmt);
}
public function lastId(): int
{
return $this->connection->insert_id;
}
public function getAffectedRows(): int
{
return $this->connection->affected_rows;
}
}

View File

@ -0,0 +1,62 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IResultSet;
use mysqli_result;
class ResultSet implements IResultSet
{
private mysqli_result $result;
public function __construct(mysqli_result $result)
{
$this->result = $result;
}
public function fetch(int $type = IResultSet::FETCH_ASSOC)
{
return $this->result->fetch_array($this->convertFetchType($type));
}
public function fetchAll(int $type = IResultSet::FETCH_ASSOC)
{
return $this->result->fetch_all($this->convertFetchType($type));
}
public function fetchOneColumn(string $valueName, string $keyName = null)
{
$array = [];
while ($r = $this->fetch(IResultSet::FETCH_ASSOC)) {
if (isset($keyName)) {
$array[$r[$keyName]] = $r[$valueName];
} else {
$array[] = $r[$valueName];
}
}
return $array;
}
private function convertFetchType(int $type): int
{
switch ($type) {
case IResultSet::FETCH_ASSOC:
$internal_type = MYSQLI_ASSOC;
break;
case IResultSet::FETCH_BOTH:
$internal_type = MYSQLI_BOTH;
break;
case IResultSet::FETCH_NUM:
$internal_type = MYSQLI_NUM;
break;
default:
$internal_type = MYSQLI_BOTH;
break;
}
return $internal_type;
}
}

View File

@ -0,0 +1,79 @@
<?php namespace MapGuesser\Database\Mysql;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Database\IStatement;
use mysqli_stmt;
class Statement implements IStatement
{
private mysqli_stmt $stmt;
public function __construct(mysqli_stmt $stmt)
{
$this->stmt = $stmt;
}
public function __destruct()
{
$this->stmt->close();
}
public function execute(array $params = []): ?IResultSet
{
if ($params) {
$ref_params = [''];
foreach ($params as &$param) {
$type = gettype($param);
switch ($type) {
case 'integer':
case 'double':
case 'string':
$t = $type[0];
break;
case 'NULL':
$t = 's';
break;
case 'boolean':
$param = (string) (int) $param;
$t = 's';
break;
case 'array':
$param = json_encode($param);
$t = 's';
break;
}
if (!isset($t)) {
throw new \Exception('Data type ' . $type . ' not supported!');
}
$ref_params[] = &$param;
$ref_params[0] .= $t;
}
if (!call_user_func_array([$this->stmt, 'bind_param'], $ref_params)) {
throw new \Exception($this->stmt->error);
}
}
if (!$this->stmt->execute()) {
throw new \Exception($this->stmt->error);
}
if ($result_set = $this->stmt->get_result()) {
return new ResultSet($result_set);
}
return null;
}
public function getAffectedRows(): int
{
return $this->stmt->affected_rows;
}
}

157
src/Database/Query/Modify.php Executable file
View File

@ -0,0 +1,157 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\Utils;
class Modify
{
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $attributes = [];
private array $original = [];
private bool $autoIncrement = false;
public function __construct(IConnection $connection, string $table)
{
$this->connection = $connection;
$this->table = $table;
}
public function setIdName(string $idName): Modify
{
$this->idName = $idName;
return $this;
}
public function setAutoIncrement(bool $autoIncrement = true): Modify
{
$this->autoIncrement = $autoIncrement;
return $this;
}
public function fill(array $attributes): Modify
{
$this->attributes = array_merge($this->attributes, $attributes);
return $this;
}
public function set(string $name, $value): Modify
{
$this->attributes[$name] = $value;
return $this;
}
public function setId($id): Modify
{
$this->attributes[$this->idName] = $id;
return $this;
}
public function save(): void
{
if (isset($this->attributes[$this->idName])) {
$this->update();
} else {
$this->insert();
}
}
public function delete(): void
{
if (!isset($this->attributes[$this->idName])) {
throw new \Exception('No primary key specified!');
}
$query = 'DELETE FROM ' . Utils::backtick($this->table) . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute([$this->idName => $this->attributes[$this->idName]]);
}
private function insert(): void
{
if (!$this->autoIncrement) {
$this->attributes[$this->idName] = $this->generateKey();
}
$set = $this->generateColumnsWithBinding(array_keys($this->attributes));
$query = 'INSERT INTO ' . Utils::backtick($this->table) . ' SET ' . $set;
$stmt = $this->connection->prepare($query);
$stmt->execute($this->attributes);
if ($this->autoIncrement) {
$this->attributes[$this->idName] = $this->connection->lastId();
}
}
private function update(): void
{
$diff = $this->generateDiff();
if (count($diff) === 0) {
return;
}
$set = $this->generateColumnsWithBinding(array_keys($diff));
$query = 'UPDATE ' . Utils::backtick($this->table) . ' SET ' . $set . ' WHERE ' . Utils::backtick($this->idName) . '=?';
$stmt = $this->connection->prepare($query);
$stmt->execute(array_merge($diff, [$this->idName => $this->attributes[$this->idName]]));
}
private function readFromDB(array $columns): void
{
$select = (new Select($this->connection, $this->table))
->setIdName($this->idName)
->whereId($this->attributes[$this->idName])
->columns($columns);
$this->original = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
}
private function generateDiff(): array
{
$this->readFromDB(array_keys($this->attributes));
$diff = [];
foreach ($this->attributes as $name => $value) {
$original = $this->original[$name];
if ($original != $value) {
$diff[$name] = $value;
}
}
return $diff;
}
public static function generateColumnsWithBinding(array $columns): string
{
array_walk($columns, function(&$value, $key) {
$value = Utils::backtick($value) . '=?';
});
return implode(',', $columns);
}
private function generateKey(): string
{
return substr(hash('sha256', serialize($this->attributes) . random_bytes(10) . microtime()), 0, 7);
}
}

View File

@ -0,0 +1,402 @@
<?php namespace MapGuesser\Database\Query;
use Closure;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\RawExpression;
use MapGuesser\Database\Utils;
class Select
{
const CONDITION_WHERE = 0;
const CONDITION_HAVING = 1;
private IConnection $connection;
private string $table;
private string $idName = 'id';
private array $tableAliases = [];
private array $joins = [];
private array $columns = [];
private array $conditions = [self::CONDITION_WHERE => [], self::CONDITION_HAVING => []];
private array $groups = [];
private array $orders = [];
private array $limit;
public function __construct(IConnection $connection, string $table)
{
$this->connection = $connection;
$this->table = $table;
}
public function setIdName(string $idName): Select
{
$this->idName = $idName;
return $this;
}
public function setTableAliases(array $tableAliases): Select
{
$this->tableAliases = array_merge($this->tableAliases, $tableAliases);
return $this;
}
public function columns(array $columns): Select
{
$this->columns = array_merge($this->columns, $columns);
return $this;
}
public function innerJoin($table, $column1, string $relation, $column2): Select
{
$this->addJoin('INNER', $table, $column1, $relation, $column2);
return $this;
}
public function leftJoin($table, $column1, string $relation, $column2): Select
{
$this->addJoin('LEFT', $table, $column1, $relation, $column2);
return $this;
}
public function whereId($value): Select
{
$this->addWhereCondition('AND', $this->idName, '=', $value);
return $this;
}
public function where($column, string $relation = null, $value = null): Select
{
$this->addWhereCondition('AND', $column, $relation, $value);
return $this;
}
public function orWhere($column, string $relation = null, $value = null): Select
{
$this->addWhereCondition('OR', $column, $relation, $value);
return $this;
}
public function having($column, string $relation = null, $value = null): Select
{
$this->addHavingCondition('AND', $column, $relation, $value);
return $this;
}
public function orHaving($column, string $relation = null, $value = null): Select
{
$this->addHavingCondition('OR', $column, $relation, $value);
return $this;
}
public function groupBy($column): Select
{
$this->groups[] = $column;
return $this;
}
public function orderBy($column, string $type = 'asc'): Select
{
$this->orders[] = [$column, $type];
return $this;
}
public function limit(int $limit, int $offset = 0): Select
{
$this->limit = [$limit, $offset];
return $this;
}
public function resetLimit(): void
{
$this->limit = null;
}
public function paginate(int $page, int $itemsPerPage)
{
$this->limit($itemsPerPage, ($page - 1) * $itemsPerPage);
return $this;
}
public function execute(): IResultSet
{
list($query, $params) = $this->generateQuery();
return $this->connection->prepare($query)->execute($params);
}
public function count(): int
{
if (count($this->groups) > 0 || count($this->conditions[self::CONDITION_HAVING]) > 0) {
$orders = $this->orders;
$this->orders = [];
list($query, $params) = $this->generateQuery();
$result = $this->connection->prepare('SELECT COUNT(*) num_rows FROM (' . $query . ') x')
->execute($params)
->fetch(IResultSet::FETCH_NUM);
$this->orders = $orders;
return $result[0];
} else {
$columns = $this->columns;
$orders = $this->orders;
$this->columns = [new RawExpression('COUNT(*) num_rows')];
$this->orders = [];
list($query, $params) = $this->generateQuery();
$result = $this->connection->prepare($query)
->execute($params)
->fetch(IResultSet::FETCH_NUM);
$this->columns = $columns;
$this->orders = $orders;
return $result[0];
}
}
private function addJoin(string $type, $table, $column1, string $relation, $column2): void
{
$this->joins[] = [$type, $table, $column1, $relation, $column2];
}
private function addWhereCondition(string $logic, $column, string $relation, $value): void
{
$this->conditions[self::CONDITION_WHERE][] = [$logic, $column, $relation, $value];
}
private function addHavingCondition(string $logic, $column, string $relation, $value): void
{
$this->conditions[self::CONDITION_HAVING][] = [$logic, $column, $relation, $value];
}
private function generateQuery(): array
{
$queryString = 'SELECT ' . $this->generateColumns() . ' FROM ' . $this->generateTable($this->table, true);
if (count($this->joins) > 0) {
$queryString .= ' ' . $this->generateJoins();
}
if (count($this->conditions[self::CONDITION_WHERE]) > 0) {
list($wheres, $whereParams) = $this->generateConditions(self::CONDITION_WHERE);
$queryString .= ' WHERE ' . $wheres;
} else {
$whereParams = [];
}
if (count($this->groups) > 0) {
$queryString .= ' GROUP BY ' . $this->generateGroupBy();
}
if (count($this->conditions[self::CONDITION_HAVING]) > 0) {
list($havings, $havingParams) = $this->generateConditions(self::CONDITION_HAVING);
$queryString .= ' HAVING ' . $havings;
} else {
$havingParams = [];
}
if (count($this->orders) > 0) {
$queryString .= ' ORDER BY ' . $this->generateOrderBy();
}
if (isset($this->limit)) {
$queryString .= ' LIMIT ' . $this->limit[1] . ', ' . $this->limit[0];
}
return [$queryString, array_merge($whereParams, $havingParams)];
}
private function generateTable($table, bool $defineAlias = false): string
{
if ($table instanceof RawExpression) {
return (string) $table;
}
if (isset($this->tableAliases[$table])) {
return ($defineAlias ? Utils::backtick($this->tableAliases[$table]) . ' ' . Utils::backtick($table) : Utils::backtick($table));
}
return Utils::backtick($table);
}
private function generateColumn($column): string
{
if ($column instanceof RawExpression) {
return (string) $column;
}
if (is_array($column)) {
$out = '';
if ($column[0]) {
$out .= $this->generateTable($column[0]) . '.';
}
$out .= Utils::backtick($column[1]);
if (!empty($column[2])) {
$out .= ' ' . Utils::backtick($column[2]);
}
return $out;
} else {
return Utils::backtick($column);
}
}
private function generateColumns(): string
{
$columns = $this->columns;
array_walk($columns, function (&$value, $key) {
$value = $this->generateColumn($value);
});
return implode(',', $columns);
}
private function generateJoins(): string
{
$joins = $this->joins;
array_walk($joins, function (&$value, $key) {
$value = $value[0] . ' JOIN ' . $this->generateTable($value[1], true) . ' ON ' . $this->generateColumn($value[2]) . ' ' . $value[3] . ' ' . $this->generateColumn($value[4]);
});
return implode(' ', $joins);
}
private function generateConditions(string $type): array
{
$conditions = '';
$params = [];
foreach ($this->conditions[$type] as $condition) {
list($logic, $column, $relation, $value) = $condition;
if ($column instanceof Closure) {
list($conditionsStringFragment, $paramsFragment) = $this->generateComplexConditionFragment($type, $column);
} else {
list($conditionsStringFragment, $paramsFragment) = $this->generateConditionFragment($condition);
}
if ($conditions !== '') {
$conditions .= ' ' . $logic . ' ';
}
$conditions .= $conditionsStringFragment;
$params = array_merge($params, $paramsFragment);
}
return [$conditions, $params];
}
private function generateConditionFragment(array $condition): array
{
list($logic, $column, $relation, $value) = $condition;
if ($column instanceof RawExpression) {
return [(string) $column, []];
}
$conditionsString = $this->generateColumn($column) . ' ';
if ($value === null) {
return [$conditionsString . ($relation == '=' ? 'IS NULL' : 'IS NOT NULL'), []];
}
$conditionsString .= strtoupper($relation) . ' ';;
switch ($relation = strtolower($relation)) {
case 'between':
$params = [$value[0], $value[1]];
$conditionsString .= '? AND ?';
break;
case 'in':
case 'not in':
$params = $value;
if (count($value) > 0) {
$conditionsString .= '(' . implode(', ', array_fill(0, count($value), '?')) . ')';
} else {
$conditionsString = $relation == 'in' ? '0' : '1';
}
break;
default:
$params = [$value];
$conditionsString .= '?';
}
return [$conditionsString, $params];
}
private function generateComplexConditionFragment(string $type, Closure $conditionCallback): array
{
$instance = new static($this->connection, $this->table);
$instance->tableAliases = $this->tableAliases;
$conditionCallback($instance);
list($conditions, $params) = $instance->generateConditions($type);
return ['(' . $conditions . ')', $params];
}
private function generateGroupBy(): string
{
$groups = $this->groups;
array_walk($groups, function (&$value, $key) {
$value = $this->generateColumn($value);
});
return implode(',', $groups);
}
private function generateOrderBy(): string
{
$orders = $this->orders;
array_walk($orders, function (&$value, $key) {
$value = $this->generateColumn($value[0]) . ' ' . $value[1];
});
return implode(',', $orders);
}
}

View File

@ -0,0 +1,16 @@
<?php namespace MapGuesser\Database;
class RawExpression
{
private string $expression;
public function __construct(string $expression)
{
$this->expression = $expression;
}
public function __toString(): string
{
return $this->expression;
}
}

7
src/Database/Utils.php Normal file
View File

@ -0,0 +1,7 @@
<?php namespace MapGuesser\Database;
class Utils {
public static function backtick(string $name) {
return '`' . $name . '`';
}
}

93
src/Http/Request.php Normal file
View File

@ -0,0 +1,93 @@
<?php namespace MapGuesser\Http;
class Request
{
const HTTP_GET = 0;
const HTTP_POST = 1;
private string $url;
private int $method;
private string $query = '';
private array $headers = [];
public function __construct(string $url, int $method = self::HTTP_GET)
{
$this->url = $url;
$this->method = $method;
}
public function setQuery($query)
{
if (is_string($query)) {
$this->query = $query;
} else {
$this->query = http_build_query($query);
}
}
public function setHeaders(array $headers)
{
$this->headers = array_merge($this->headers, $headers);
}
public function send(): Response
{
$ch = curl_init();
if ($this->method === self::HTTP_GET) {
$url = $this->url . '?' . $this->query;
} elseif ($this->method === self::HTTP_POST) {
$url = $this->url;
curl_setopt($ch, CURLOPT_POST, 1);
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->query);
}
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($ch, CURLOPT_USERAGENT, 'MapGuesser cURL/1.0');
if (count($this->headers) > 0) {
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->headers);
}
$responseHeaders = [];
curl_setopt(
$ch,
CURLOPT_HEADERFUNCTION,
function ($ch, $header) use (&$responseHeaders) {
$len = strlen($header);
$header = explode(':', $header, 2);
if (count($header) < 2) {
return $len;
}
$responseHeaders[strtolower(trim($header[0]))][] = trim($header[1]);
return $len;
}
);
$responseBody = curl_exec($ch);
if ($responseBody === false) {
$error = curl_error($ch);
curl_close($ch);
throw new \Exception($error);
}
curl_close($ch);
return new Response($responseBody, $responseHeaders);
}
}

24
src/Http/Response.php Normal file
View File

@ -0,0 +1,24 @@
<?php namespace MapGuesser\Http;
class Response
{
private string $body;
private array $headers;
public function __construct(string $body, array $headers)
{
$this->body = $body;
$this->headers = $headers;
}
public function getBody()
{
return $this->body;
}
public function getHeaders()
{
return $this->headers;
}
}

View File

@ -0,0 +1,8 @@
<?php namespace MapGuesser\Interfaces\Controller;
use MapGuesser\Interfaces\View\IView;
interface IController
{
public function run(): IView;
}

View File

@ -0,0 +1,20 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IConnection
{
public function startTransaction(): void;
public function commit(): void;
public function rollback(): void;
public function query(string $query): ?IResultSet;
public function multiQuery(string $query): array;
public function prepare(string $query): IStatement;
public function lastId(): int;
public function getAffectedRows(): int;
}

View File

@ -0,0 +1,16 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IResultSet
{
const FETCH_ASSOC = 0;
const FETCH_NUM = 1;
const FETCH_BOTH = 2;
public function fetch(int $type);
public function fetchAll(int $type);
public function fetchOneColumn(string $valueName, string $keyName);
}

View File

@ -0,0 +1,8 @@
<?php namespace MapGuesser\Interfaces\Database;
interface IStatement
{
public function execute(array $params): ?IResultSet;
public function getAffectedRows(): int;
}

View File

@ -0,0 +1,10 @@
<?php namespace MapGuesser\Interfaces\View;
interface IView
{
public function &getData(): array;
public function &render(): string;
public function getContentType(): string;
}

View File

@ -1,6 +1,8 @@
<?php namespace MapGuesser\View; <?php namespace MapGuesser\View;
abstract class ViewBase use MapGuesser\Interfaces\View\IView;
abstract class ViewBase implements IView
{ {
protected array $data; protected array $data;