Merged in feature/MAPG-140-solve-persistent-model-anomalies (pull request #121)

Feature/MAPG-140 solve persistent model anomalies
This commit is contained in:
Bence Pőcze 2020-06-20 00:10:48 +00:00
commit 80b850fdd7
26 changed files with 907 additions and 405 deletions

View File

@ -1,7 +1,7 @@
<?php namespace MapGuesser\Cli;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@ -20,10 +20,8 @@ class AddUserCommand extends Command
public function execute(InputInterface $input, OutputInterface $output): int
{
$user = new User([
'email' => $input->getArgument('email'),
]);
$user = new User();
$user->setEmail($input->getArgument('email'));
$user->setPlainPassword($input->getArgument('password'));
if ($input->hasArgument('type')) {
@ -31,9 +29,8 @@ class AddUserCommand extends Command
}
try {
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$pdm = new PersistentDataManager();
$pdm->saveToDb($user);
} catch (\Exception $e) {
$output->writeln('<error>Adding user failed!</error>');
$output->writeln('');

View File

@ -1,7 +1,6 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Bounds;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
@ -37,18 +36,16 @@ class GameController
{
$map = $this->mapRepository->getById($mapId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$session = $this->request->session();
if (!($state = $session->get('state')) || $state['mapId'] !== $mapId) {
$session->set('state', [
'mapId' => $mapId,
'area' => $map['area'],
'area' => $map->getArea(),
'rounds' => []
]);
}
return ['mapId' => $mapId, 'mapName' => $map['name'], 'bounds' => $bounds->toArray()];
return ['mapId' => $mapId, 'mapName' => $map->getName(), 'bounds' => $map->getBounds()->toArray()];
}
}

View File

@ -4,6 +4,7 @@ use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Response\JsonContent;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\Repository\PlaceRepository;
class GameFlowController
@ -33,11 +34,14 @@ class GameFlowController
}
if (count($state['rounds']) === 0) {
$place = $this->placeRepository->getForMapWithValidPano($mapId);
$state['rounds'][] = $place;
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, [], $placesWithoutPano);
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$session->set('state', $state);
$data = ['panoId' => $place['panoId']];
$data = ['panoId' => $place->getPanoIdCached()];
} else {
$rounds = count($state['rounds']);
$last = $state['rounds'][$rounds - 1];
@ -93,18 +97,20 @@ class GameFlowController
$exclude = array_merge($exclude, $round['placesWithoutPano'], [$round['placeId']]);
}
$place = $this->placeRepository->getForMapWithValidPano($mapId, $exclude);
$state['rounds'][] = $place;
$session->set('state', $state);
$placesWithoutPano = [];
$place = $this->placeRepository->getRandomForMapWithValidPano($mapId, $exclude, $placesWithoutPano);
$panoId = $place['panoId'];
$this->addNewRoundToState($state, $place, $placesWithoutPano);
$panoId = $place->getPanoIdCached();
} else {
$state['rounds'] = [];
$session->set('state', $state);
$panoId = null;
}
$session->set('state', $state);
$data = [
'result' => [
'position' => $position->toArray(),
@ -116,6 +122,16 @@ class GameFlowController
return new JsonContent($data);
}
private function addNewRoundToState(&$state, Place $place, array $placesWithoutPano): void
{
$state['rounds'][] = [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place->getId(),
'position' => $place->getPosition(),
'panoId' => $place->getPanoIdCached()
];
}
private function calculateDistance(Position $realPosition, Position $guessPosition): float
{
return $realPosition->calculateDistanceTo($guessPosition);

View File

@ -1,11 +1,9 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Model\User;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
@ -14,16 +12,17 @@ class LoginController
{
private IRequest $request;
private UserRepository $userRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->userRepository = new UserRepository();
}
public function getLoginForm()
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
@ -33,26 +32,18 @@ class LoginController
public function login(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
$data = ['success' => true];
return new JsonContent($data);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$user = $this->userRepository->getByEmail($this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($userData === null) {
if ($user === null) {
$data = ['error' => 'user_not_found'];
return new JsonContent($data);
}
$user = new User($userData);
if (!$user->getActive()) {
$data = ['error' => 'user_not_active'];
return new JsonContent($data);
@ -63,7 +54,7 @@ class LoginController
return new JsonContent($data);
}
$session->set('user', $user);
$this->request->setUser($user);
$data = ['success' => true];
return new JsonContent($data);
@ -71,7 +62,7 @@ class LoginController
public function logout(): IRedirect
{
$this->request->session()->delete('user');
$this->request->setUser(null);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}

View File

@ -1,13 +1,15 @@
<?php namespace MapGuesser\Controller;
use DateTime;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\MapRepository;
use MapGuesser\Repository\PlaceRepository;
use MapGuesser\Response\HtmlContent;
@ -21,6 +23,8 @@ class MapAdminController implements ISecured
private IRequest $request;
private PersistentDataManager $pdm;
private MapRepository $mapRepository;
private PlaceRepository $placeRepository;
@ -28,6 +32,7 @@ class MapAdminController implements ISecured
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->mapRepository = new MapRepository();
$this->placeRepository = new PlaceRepository();
}
@ -45,18 +50,14 @@ class MapAdminController implements ISecured
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
$bounds = Bounds::createDirectly($map['bound_south_lat'], $map['bound_west_lng'], $map['bound_north_lat'], $map['bound_east_lng']);
$places = $this->getPlaces($mapId);
$places = $this->getPlaces($map);
} else {
$map = [
'name' => self::$unnamedMapName,
'description' => ''
];
$bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
$map = new Map();
$map->setName(self::$unnamedMapName);
$places = [];
}
$data = ['mapId' => $mapId, 'mapName' => $map['name'], 'mapDescription' => str_replace('<br>', "\n", $map['description']), 'bounds' => $bounds->toArray(), 'places' => &$places];
$data = ['mapId' => $mapId, 'mapName' => $map->getName(), 'mapDescription' => str_replace('<br>', "\n", $map->getDescription()), 'bounds' => $map->getBounds()->toArray(), 'places' => &$places];
return new HtmlContent('admin/map_editor', $data);
}
@ -64,9 +65,9 @@ class MapAdminController implements ISecured
{
$placeId = (int) $this->request->query('placeId');
$placeData = $this->placeRepository->getById($placeId);
$place = $this->placeRepository->getById($placeId);
$data = ['panoId' => $placeData['panoId']];
$data = ['panoId' => $place->getFreshPanoId()];
return new JsonContent($data);
}
@ -76,8 +77,12 @@ class MapAdminController implements ISecured
\Container::$dbConnection->startTransaction();
if (!$mapId) {
$mapId = $this->addNewMap();
if ($mapId) {
$map = $this->mapRepository->getById($mapId);
} else {
$map = new Map();
$map->setName(self::$unnamedMapName);
$this->pdm->saveToDb($map);
}
if (isset($_POST['added'])) {
@ -85,11 +90,18 @@ class MapAdminController implements ISecured
foreach ($_POST['added'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $this->placeRepository->addToMap($mapId, [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng'],
'pano_id_cached_timestamp' => $placeRaw['panoId'] === -1 ? (new DateTime('-1 day'))->format('Y-m-d H:i:s') : null
])];
$place = new Place();
$place->setMap($map);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
if ($placeRaw['panoId'] === -1) {
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
}
$this->pdm->saveToDb($place);
$addedIds[] = ['tempId' => $placeRaw['id'], 'id' => $place->getId()];
}
} else {
$addedIds = [];
@ -99,10 +111,12 @@ class MapAdminController implements ISecured
foreach ($_POST['edited'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$this->placeRepository->modify((int) $placeRaw['id'], [
'lat' => (float) $placeRaw['lat'],
'lng' => (float) $placeRaw['lng']
]);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$place->setLat((float) $placeRaw['lat']);
$place->setLng((float) $placeRaw['lng']);
$place->setPanoIdCachedTimestampDate(new DateTime('-1 day'));
$this->pdm->saveToDb($place);
}
}
@ -110,45 +124,42 @@ class MapAdminController implements ISecured
foreach ($_POST['deleted'] as $placeRaw) {
$placeRaw = json_decode($placeRaw, true);
$this->placeRepository->delete($placeRaw['id']);
$place = $this->placeRepository->getById((int) $placeRaw['id']);
$this->pdm->deleteFromDb($place);
}
}
$mapBounds = $this->calculateMapBounds($mapId);
$mapBounds = $this->calculateMapBounds($map);
$map = [
'bound_south_lat' => $mapBounds->getSouthLat(),
'bound_west_lng' => $mapBounds->getWestLng(),
'bound_north_lat' => $mapBounds->getNorthLat(),
'bound_east_lng' => $mapBounds->getEastLng(),
'area' => $mapBounds->calculateApproximateArea(),
];
$map->setBounds($mapBounds);
$map->setArea($mapBounds->calculateApproximateArea());
if (isset($_POST['name'])) {
$map['name'] = $_POST['name'] ? $_POST['name'] : self::$unnamedMapName;
$map->setName($_POST['name'] ? $_POST['name'] : self::$unnamedMapName);
}
if (isset($_POST['description'])) {
$map['description'] = str_replace(["\n", "\r\n"], '<br>', $_POST['description']);
$map->setDescription(str_replace(["\n", "\r\n"], '<br>', $_POST['description']));
}
$this->saveMapData($mapId, $map);
$this->pdm->saveToDb($map);
\Container::$dbConnection->commit();
$data = ['mapId' => $mapId, 'added' => $addedIds];
$data = ['mapId' => $map->getId(), 'added' => $addedIds];
return new JsonContent($data);
}
public function deleteMap() {
$mapId = (int) $this->request->query('mapId');
$map = $this->mapRepository->getById($mapId);
\Container::$dbConnection->startTransaction();
$this->deletePlaces($mapId);
$this->deletePlaces($map);
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->delete();
$this->pdm->deleteFromDb($map);
\Container::$dbConnection->commit();
@ -156,26 +167,30 @@ class MapAdminController implements ISecured
return new JsonContent($data);
}
private function deletePlaces(int $mapId): void
private function deletePlaces(Map $map): void
{
//TODO: relations?
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id']);
$select->where('map_id', '=', $mapId);
$select->columns(Place::getFields());
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
while ($place = $result->fetch(IResultSet::FETCH_ASSOC)) {
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($place['id']);
$modify->delete();
while ($placeData = $result->fetch(IResultSet::FETCH_ASSOC)) {
$place = new Place();
$this->pdm->fillWithData($placeData, $place);
$this->pdm->deleteFromDb($place);
}
}
private function calculateMapBounds(int $mapId): Bounds
private function calculateMapBounds(Map $map): Bounds
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['lat', 'lng']);
$select->where('map_id', '=', $mapId);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();
@ -187,35 +202,12 @@ class MapAdminController implements ISecured
return $bounds;
}
private function addNewMap(): int
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->fill([
'name' => self::$unnamedMapName,
'description' => '',
'bound_south_lat' => 0.0,
'bound_west_lng' => 0.0,
'bound_north_lat' => 0.0,
'bound_east_lng' => 0.0
]);
$modify->save();
return $modify->getId();
}
private function saveMapData(int $mapId, array $map): void
{
$modify = new Modify(\Container::$dbConnection, 'maps');
$modify->setId($mapId);
$modify->fill($map);
$modify->save();
}
private function &getPlaces(int $mapId): array
private function &getPlaces(Map $map): array
{
//TODO: from repository or relations
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('map_id', '=', $mapId);
$select->where('map_id', '=', $map->getId());
$result = $select->execute();

View File

@ -19,6 +19,7 @@ class MapsController
public function getMaps(): IContent
{
//TODO: from repository
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns([
['maps', 'id'],

View File

@ -1,13 +1,14 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\Interfaces\Response\IRedirect;
use MapGuesser\Mailing\Mail;
use MapGuesser\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\Repository\UserConfirmationRepository;
use MapGuesser\Repository\UserRepository;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
use MapGuesser\Response\Redirect;
@ -16,16 +17,23 @@ class SignupController
{
private IRequest $request;
private PersistentDataManager $pdm;
private UserRepository $userRepository;
private UserConfirmationRepository $userConfirmationRepository;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
$this->userRepository = new UserRepository();
$this->userConfirmationRepository = new UserConfirmationRepository();
}
public function getSignupForm()
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
@ -35,9 +43,7 @@ class SignupController
public function signup(): IContent
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
//TODO: return with some error
$data = ['success' => true];
return new JsonContent($data);
@ -48,15 +54,9 @@ class SignupController
return new JsonContent($data);
}
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->where('email', '=', $this->request->post('email'));
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($userData !== null) {
$user = new User($userData);
$user = $this->userRepository->getByEmail($this->request->post('email'));
if ($user !== null) {
if ($user->getActive()) {
$data = ['error' => 'user_found'];
} else {
@ -75,25 +75,21 @@ class SignupController
return new JsonContent($data);
}
$user = new User([
'email' => $this->request->post('email'),
]);
$user = new User();
$user->setEmail($this->request->post('email'));
$user->setPlainPassword($this->request->post('password'));
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$userId = $modify->getId();
$this->pdm->saveToDb($user);
$token = hash('sha256', serialize($user) . random_bytes(10) . microtime());
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->set('user_id', $userId);
$modify->set('token', $token);
$modify->save();
$confirmation = new UserConfirmation();
$confirmation->setUser($user);
$confirmation->setToken($token);
$this->pdm->saveToDb($confirmation);
\Container::$dbConnection->commit();
@ -105,17 +101,11 @@ class SignupController
public function activate()
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
if ($confirmation === null) {
$data = [];
@ -124,42 +114,27 @@ class SignupController
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$this->pdm->deleteFromDb($confirmation);
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->set('active', true);
$modify->save();
$user = $this->userRepository->getById($confirmation->getUserId());
$user->setActive(true);
$this->pdm->saveToDb($user);
\Container::$dbConnection->commit();
$select = new Select(\Container::$dbConnection, 'users');
$select->columns(User::getFields());
$select->whereId($confirmation['user_id']);
$userData = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$user = new User($userData);
$session->set('user', $user);
$this->request->setUser($user);
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
public function cancel()
{
$session = $this->request->session();
if ($session->get('user')) {
if ($this->request->user() !== null) {
return new Redirect([\Container::$routeCollection->getRoute('index'), []], IRedirect::TEMPORARY);
}
$select = new Select(\Container::$dbConnection, 'user_confirmations');
$select->columns(['id', 'user_id']);
$select->where('token', '=', $this->request->query('token'));
$confirmation = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
$confirmation = $this->userConfirmationRepository->getByToken($this->request->query('token'));
if ($confirmation === null) {
$data = ['success' => false];
@ -168,13 +143,11 @@ class SignupController
\Container::$dbConnection->startTransaction();
$modify = new Modify(\Container::$dbConnection, 'user_confirmations');
$modify->setId($confirmation['id']);
$modify->delete();
$this->pdm->deleteFromDb($confirmation);
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->setId($confirmation['user_id']);
$modify->delete();
$user = $this->userRepository->getById($confirmation->getUserId());
$this->pdm->deleteFromDb($user);
\Container::$dbConnection->commit();

View File

@ -1,9 +1,10 @@
<?php namespace MapGuesser\Controller;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Interfaces\Authorization\ISecured;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Response\IContent;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\Response\HtmlContent;
use MapGuesser\Response\JsonContent;
@ -11,9 +12,12 @@ class UserController implements ISecured
{
private IRequest $request;
private PersistentDataManager $pdm;
public function __construct(IRequest $request)
{
$this->request = $request;
$this->pdm = new PersistentDataManager();
}
public function authorize(): bool
@ -25,6 +29,9 @@ class UserController implements ISecured
public function getProfile(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
$data = ['user' => $user->toArray()];
@ -33,6 +40,9 @@ class UserController implements ISecured
public function saveProfile(): IContent
{
/**
* @var User $user
*/
$user = $this->request->user();
if (!$user->checkPassword($this->request->post('password'))) {
@ -54,11 +64,7 @@ class UserController implements ISecured
$user->setPlainPassword($this->request->post('password_new'));
}
$modify = new Modify(\Container::$dbConnection, 'users');
$modify->fill($user->toArray());
$modify->save();
$this->request->session()->set('user', $user);
$this->pdm->saveToDb($user);
$data = ['success' => true];
return new JsonContent($data);

View File

@ -1,7 +1,6 @@
<?php namespace MapGuesser\Database\Query;
use MapGuesser\Interfaces\Database\IConnection;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\Database\Utils;
class Modify
@ -14,8 +13,6 @@ class Modify
private array $attributes = [];
private array $original = [];
private ?string $externalId = null;
private bool $autoIncrement = true;
@ -116,48 +113,15 @@ class Modify
private function update(): void
{
/*$diff = $this->generateDiff();
$attributes = $this->attributes;
unset($attributes[$this->idName]);
if (count($diff) === 0) {
return;
}*/
$diff = $this->attributes;
unset($diff[$this->idName]);
$set = $this->generateColumnsWithBinding(array_keys($diff));
$set = $this->generateColumnsWithBinding(array_keys($attributes));
$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;
$stmt->execute(array_merge($attributes, [$this->idName => $this->attributes[$this->idName]]));
}
public static function generateColumnsWithBinding(array $columns): string

View File

@ -32,10 +32,13 @@ class Select
private array $limit;
public function __construct(IConnection $connection, string $table)
public function __construct(IConnection $connection, ?string $table = null)
{
$this->connection = $connection;
$this->table = $table;
if ($table !== null) {
$this->table = $table;
}
}
public function setIdName(string $idName): Select
@ -52,6 +55,13 @@ class Select
return $this;
}
public function from(string $table): Select
{
$this->table = $table;
return $this;
}
public function columns(array $columns): Select
{
$this->columns = array_merge($this->columns, $columns);

View File

@ -8,5 +8,9 @@ interface IUser
public function hasPermission(int $permission): bool;
public function getUniqueId();
public function getDisplayName(): string;
public function checkPassword(string $password): bool;
}

View File

@ -14,5 +14,7 @@ interface IRequest
public function session(): ISession;
public function setUser(?IUser $user): void;
public function user(): ?IUser;
}

View File

@ -1,49 +0,0 @@
<?php namespace MapGuesser\Model;
abstract class BaseModel
{
protected static array $fields;
protected $id = null;
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public function __construct(array $data)
{
foreach ($data as $key => $value) {
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$this->$method($value);
}
}
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
function toArray(): array
{
$array = [];
foreach (self::getFields() as $key) {
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$array[$key] = $this->$method();
}
}
return $array;
}
}

View File

@ -0,0 +1,103 @@
<?php namespace MapGuesser\PersistentData\Model;
use MapGuesser\Util\Geo\Bounds;
class Map extends Model
{
protected static string $table = 'maps';
protected static array $fields = ['name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area'];
private string $name = '';
private string $description = '';
private Bounds $bounds;
private float $area = 0.0;
public function __construct()
{
$this->bounds = Bounds::createDirectly(-90.0, -180.0, 90.0, 180.0);
}
public function setName(string $name): void
{
$this->name = $name;
}
public function setDescription(string $description): void
{
$this->description = $description;
}
public function setBounds(Bounds $bounds): void
{
$this->bounds = $bounds;
}
public function setBoundSouthLat(float $bound_south_lat): void
{
$this->bounds->setSouthLat($bound_south_lat);
}
public function setBoundWestLng(float $bound_west_lng): void
{
$this->bounds->setWestLng($bound_west_lng);
}
public function setBoundNorthLat(float $bound_north_lat): void
{
$this->bounds->setNorthLat($bound_north_lat);
}
public function setBoundEastLng(float $bound_east_lng): void
{
$this->bounds->setEastLng($bound_east_lng);
}
public function setArea(float $area): void
{
$this->area = $area;
}
public function getName(): string
{
return $this->name;
}
public function getDescription(): string
{
return $this->description;
}
public function getBounds(): Bounds
{
return $this->bounds;
}
public function getBoundSouthLat(): float
{
return $this->bounds->getSouthLat();
}
public function getBoundWestLng(): float
{
return $this->bounds->getWestLng();
}
public function getBoundNorthLat(): float
{
return $this->bounds->getNorthLat();
}
public function getBoundEastLng(): float
{
return $this->bounds->getEastLng();
}
public function getArea(): float
{
return $this->area;
}
}

View File

@ -0,0 +1,69 @@
<?php namespace MapGuesser\PersistentData\Model;
abstract class Model
{
protected static string $table;
protected static array $fields;
protected static array $relations = [];
protected $id = null;
private array $snapshot = [];
public static function getTable(): string
{
return static::$table;
}
public static function getFields(): array
{
return array_merge(['id'], static::$fields);
}
public static function getRelations(): array
{
return static::$relations;
}
public function setId($id): void
{
$this->id = $id;
}
public function getId()
{
return $this->id;
}
public function toArray(): array
{
$array = [];
foreach (self::getFields() as $key) {
$method = 'get' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($this, $method)) {
$array[$key] = $this->$method();
}
}
return $array;
}
public function saveSnapshot(): void
{
$this->snapshot = $this->toArray();
}
public function resetSnapshot(): void
{
$this->snapshot = [];
}
public function getSnapshot(): array
{
return $this->snapshot;
}
}

View File

@ -0,0 +1,150 @@
<?php namespace MapGuesser\PersistentData\Model;
use DateInterval;
use DateTime;
use MapGuesser\Http\Request;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Util\Geo\Position;
class Place extends Model
{
protected static string $table = 'places';
protected static array $fields = ['map_id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp'];
protected static array $relations = ['map' => Map::class];
private ?Map $map = null;
private ?int $mapId = null;
private Position $position;
private ?string $panoIdCached = null;
private ?DateTime $panoIdCachedTimestamp = null;
public function __construct()
{
$this->position = new Position(0.0, 0.0);
}
public function setMap(Map $map): void
{
$this->map = $map;
}
public function setMapId(int $mapId): void
{
$this->mapId = $mapId;
}
public function setPosition(Position $position): void
{
$this->position = $position;
}
public function setLat(float $lat): void
{
$this->position->setLat($lat);
}
public function setLng(float $lng): void
{
$this->position->setLng($lng);
}
public function setPanoIdCached(?string $panoIdCached): void
{
$this->panoIdCached = $panoIdCached;
}
public function setPanoIdCachedTimestampDate(?DateTime $panoIdCachedTimestamp): void
{
$this->panoIdCachedTimestamp = $panoIdCachedTimestamp;
}
public function setPanoIdCachedTimestamp(?string $panoIdCachedTimestamp): void
{
if ($panoIdCachedTimestamp !== null) {
$this->panoIdCachedTimestamp = new DateTime($panoIdCachedTimestamp);
}
}
public function getMap(): ?Map
{
return $this->map;
}
public function getMapId(): ?int
{
return $this->mapId;
}
public function getPosition(): Position
{
return $this->position;
}
public function getLat(): float
{
return $this->position->getLat();
}
public function getLng(): float
{
return $this->position->getLng();
}
public function getFreshPanoId(bool $canBeIndoor = false): ?string
{
if (
$this->panoIdCachedTimestamp !== null &&
(clone $this->panoIdCachedTimestamp)->add(new DateInterval('P1D')) > new DateTime()
) {
return $this->panoIdCached;
}
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $this->position->getLat() . ',' . $this->position->getLng(),
'source' => $canBeIndoor ? 'default' : 'outdoor'
]);
$response = $request->send();
$panoData = json_decode($response->getBody(), true);
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
// enable indoor panos if no outdoor found
if ($panoId === null && !$canBeIndoor) {
return $this->getFreshPanoId(true);
}
$this->panoIdCached = $panoId;
$this->panoIdCachedTimestamp = new DateTime('now');
(new PersistentDataManager())->saveToDb($this);
return $panoId;
}
public function getPanoIdCached(): ?string
{
return $this->panoIdCached;
}
public function getPanoIdCachedTimestampDate(): ?DateTime
{
return $this->panoIdCachedTimestamp;
}
public function getPanoIdCachedTimestamp(): ?string
{
if ($this->panoIdCachedTimestamp === null) {
return null;
}
return $this->panoIdCachedTimestamp->format('Y-m-d H:i:s');
}
}

View File

@ -1,16 +1,18 @@
<?php namespace MapGuesser\Model;
<?php namespace MapGuesser\PersistentData\Model;
use MapGuesser\Interfaces\Authentication\IUser;
class User extends BaseModel implements IUser
class User extends Model implements IUser
{
private static array $types = ['user', 'admin'];
protected static string $table = 'users';
protected static array $fields = ['email', 'password', 'type', 'active'];
private string $email;
private static array $types = ['user', 'admin'];
private string $password;
private string $email = '';
private string $password = '';
private string $type = 'user';
@ -75,6 +77,11 @@ class User extends BaseModel implements IUser
}
}
public function getUniqueId()
{
return $this->id;
}
public function getDisplayName(): string
{
return $this->email;

View File

@ -0,0 +1,44 @@
<?php namespace MapGuesser\PersistentData\Model;
class UserConfirmation extends Model
{
protected static string $table = 'user_confirmations';
protected static array $fields = ['user_id', 'token'];
private ?User $user = null;
private ?int $userId = null;
private string $token = '';
public function setUser(User $user): void
{
$this->user = $user;
}
public function setUserId(int $userId): void
{
$this->userId = $userId;
}
public function setToken(string $token): void
{
$this->token = $token;
}
public function getUser(): ?User
{
return $this->user;
}
public function getUserId(): ?int
{
return $this->userId;
}
public function getToken(): string
{
return $this->token;
}
}

View File

@ -0,0 +1,206 @@
<?php namespace MapGuesser\PersistentData;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\Model;
class PersistentDataManager
{
public function selectFromDb(Select $select, string $type, bool $withRelations = false): ?Model
{
$table = call_user_func([$type, 'getTable']);
$fields = call_user_func([$type, 'getFields']);
$select->from($table);
//TODO: only with some relations?
if ($withRelations) {
$relations = call_user_func([$type, 'getRelations']);
$columns = [];
foreach ($fields as $field) {
$columns[] = [$table, $field];
}
$columns = array_merge($columns, $this->getRelationColumns($relations));
$this->leftJoinRelations($select, $table, $relations);
$select->columns($columns);
} else {
$select->columns($fields);
}
//TODO: return with array?
$data = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
if ($data === null) {
return null;
}
$model = new $type();
$this->fillWithData($data, $model);
return $model;
}
public function selectFromDbById($id, string $type, bool $withRelations = false): ?Model
{
$select = new Select(\Container::$dbConnection);
$select->whereId($id);
return $this->selectFromDb($select, $type, $withRelations);
}
public function fillWithData(array $data, Model $model): void
{
$relations = $model::getRelations();
$relationData = [];
foreach ($data as $key => $value) {
if ($this->extractRelationData($key, $value, $relationData, $relations)) {
continue;
}
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
if (method_exists($model, $method)) {
$model->$method($value);
}
}
$this->setRelations($model, $relationData);
$model->saveSnapshot();
}
public function loadRelationsFromDb(Model $model, bool $recursive): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel . 'Id';
$methodSet = 'set' . $camel;
$relationId = $model->$methodGet();
if ($relationId !== null) {
$relationModel = $this->selectFromDbById($relationId, $relationType, $recursive);
$model->$methodSet($relationModel);
}
}
}
public function saveToDb(Model $model): void
{
$this->syncRelations($model);
$modified = $model->toArray();
$id = $model->getId();
$modify = new Modify(\Container::$dbConnection, $model::getTable());
if ($id !== null) {
$original = $model->getSnapshot();
foreach ($original as $key => $value) {
if ($value === $modified[$key]) {
unset($modified[$key]);
}
}
if (count($modified) > 0) {
$modify->setId($id);
$modify->fill($modified);
$modify->save();
}
} else {
$modify->fill($modified);
$modify->save();
$model->setId($modify->getId());
}
$model->saveSnapshot();
}
public function deleteFromDb(Model $model): void
{
$modify = new Modify(\Container::$dbConnection, $model::getTable());
$modify->setId($model->getId());
$modify->delete();
$model->setId(null);
$model->resetSnapshot();
}
private function getRelationColumns(array $relations): array
{
$columns = [];
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
foreach (call_user_func([$relationType, 'getFields']) as $relationField) {
$columns[] = [$relationTable, $relationField, $relation . '__' . $relationField];
}
}
return $columns;
}
private function leftJoinRelations(Select $select, string $table, array $relations): void
{
foreach ($relations as $relation => $relationType) {
$relationTable = call_user_func([$relationType, 'getTable']);
$select->leftJoin($relationTable, [$relationTable, 'id'], '=', [$table, $relation . '_id']);
}
}
private function extractRelationData(string $key, $value, array &$relationData, array $relations): bool
{
$found = false;
foreach ($relations as $relation => $relationType) {
if (substr($key, 0, strlen($relation . '__')) === $relation . '__') {
$found = true;
$relationData[$relation][substr($key, strlen($relation . '__'))] = $value;
break;
}
}
return $found;
}
private function setRelations(Model $model, array &$relations): void
{
foreach ($model::getRelations() as $relation => $relationType) {
if (isset($relations[$relation])) {
$object = new $relationType();
$this->fillWithData($relations[$relation], $object);
$method = 'set' . str_replace('_', '', ucwords($relation, '_'));
$model->$method($object);
}
}
}
private function syncRelations(Model $model): void
{
foreach ($model::getRelations() as $relation => $relationType) {
$camel = str_replace('_', '', ucwords($relation, '_'));
$methodGet = 'get' . $camel;
$methodSet = 'set' . $camel . 'Id';
$relationModel = $model->$methodGet();
if ($relationModel !== null) {
$model->$methodSet($relationModel->getId());
}
}
}
}

View File

@ -1,16 +1,19 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\Interfaces\Database\IResultSet;
use MapGuesser\PersistentData\Model\Map;
use MapGuesser\PersistentData\PersistentDataManager;
class MapRepository
{
public function getById(int $mapId)
{
$select = new Select(\Container::$dbConnection, 'maps');
$select->columns(['id', 'name', 'description', 'bound_south_lat', 'bound_west_lng', 'bound_north_lat', 'bound_east_lng', 'area']);
$select->whereId($mapId);
private PersistentDataManager $pdm;
return $select->execute()->fetch(IResultSet::FETCH_ASSOC);
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $mapId): ?Map
{
return $this->pdm->selectFromDbById($mapId, Map::class);
}
}

View File

@ -1,95 +1,43 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Util\Geo\Position;
use MapGuesser\Database\Query\Select;
use MapGuesser\Database\Query\Modify;
use MapGuesser\Http\Request;
use MapGuesser\Interfaces\Database\IResultSet;
use DateTime;
use DateInterval;
use MapGuesser\PersistentData\Model\Place;
use MapGuesser\PersistentData\PersistentDataManager;
class PlaceRepository
{
public function getById(int $placeId)
private PersistentDataManager $pdm;
public function __construct()
{
$place = $this->selectFromDbById($placeId);
$panoId = $this->requestPanoId($place);
$position = new Position($place['lat'], $place['lng']);
return [
'position' => $position,
'panoId' => $panoId
];
$this->pdm = new PersistentDataManager();
}
public function getForMapWithValidPano(int $mapId, array $exclude = []): array
public function getById(int $placeId): ?Place
{
return $this->pdm->selectFromDbById($placeId, Place::class);
}
public function getRandomForMapWithValidPano(int $mapId, array $exclude = [], array &$placesWithoutPano): Place
{
$placesWithoutPano = [];
do {
$place = $this->selectFromDbForMap($mapId, $exclude);
$place = $this->selectRandomFromDbForMap($mapId, $exclude);
$panoId = $this->requestPanoId($place);
$panoId = $place->getFreshPanoId();
if ($panoId === null) {
$placesWithoutPano[] = $exclude[] = $place['id'];
$placesWithoutPano[] = $exclude[] = $place->getId();
}
} while ($panoId === null);
$position = new Position($place['lat'], $place['lng']);
return [
'placesWithoutPano' => $placesWithoutPano,
'placeId' => $place['id'],
'position' => $position,
'panoId' => $panoId
];
}
public function addToMap(int $mapId, array $place): int
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->set('map_id', $mapId);
$modify->fill($place);
$modify->save();
return $modify->getId();
}
public function modify(int $id, array $place): void
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($id);
$modify->set('pano_id_cached', null);
$modify->set('pano_id_cached_timestamp', null);
$modify->fill($place);
$modify->save();
}
public function delete(int $id): void
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($id);
$modify->delete();
}
private function selectFromDbById(int $placeId): array
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->whereId($placeId);
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
return $place;
}
private function selectFromDbForMap(int $mapId, array $exclude): array
private function selectRandomFromDbForMap(int $mapId, array $exclude): ?Place
{
$select = new Select(\Container::$dbConnection, 'places');
$select->columns(['id', 'lat', 'lng', 'pano_id_cached', 'pano_id_cached_timestamp']);
$select->where('id', 'NOT IN', $exclude);
$select->where('map_id', '=', $mapId);
@ -99,49 +47,6 @@ class PlaceRepository
$select->orderBy('id');
$select->limit(1, $randomOffset);
$place = $select->execute()->fetch(IResultSet::FETCH_ASSOC);
return $place;
}
private function requestPanoId(array $place, bool $canBeIndoor = false): ?string
{
if (
$place['pano_id_cached_timestamp'] &&
(new DateTime($place['pano_id_cached_timestamp']))->add(new DateInterval('P1D')) > new DateTime()
) {
return $place['pano_id_cached'];
}
$request = new Request('https://maps.googleapis.com/maps/api/streetview/metadata', Request::HTTP_GET);
$request->setQuery([
'key' => $_ENV['GOOGLE_MAPS_SERVER_API_KEY'],
'location' => $place['lat'] . ',' . $place['lng'],
'source' => $canBeIndoor ? 'default' : 'outdoor'
]);
$response = $request->send();
$panoData = json_decode($response->getBody(), true);
$panoId = $panoData['status'] === 'OK' ? $panoData['pano_id'] : null;
// enable indoor panos if no outdoor found
if ($panoId === null && !$canBeIndoor) {
return $this->requestPanoId($place, true);
}
$this->saveCachedPanoId($place['id'], $panoId);
return $panoId;
}
private function saveCachedPanoId(int $placeId, ?string $panoId): void
{
$modify = new Modify(\Container::$dbConnection, 'places');
$modify->setId($placeId);
$modify->set('pano_id_cached', $panoId);
$modify->set('pano_id_cached_timestamp', (new DateTime())->format('Y-m-d H:i:s'));
$modify->save();
return $this->pdm->selectFromDb($select, Place::class);
}
}

View File

@ -0,0 +1,28 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\UserConfirmation;
use MapGuesser\PersistentData\PersistentDataManager;
class UserConfirmationRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userConfirmationId): ?UserConfirmation
{
return $this->pdm->selectFromDbById($userConfirmationId, UserConfirmation::class);
}
public function getByToken(string $token): ?UserConfirmation
{
$select = new Select(\Container::$dbConnection);
$select->where('token', '=', $token);
return $this->pdm->selectFromDb($select, UserConfirmation::class);
}
}

View File

@ -0,0 +1,28 @@
<?php namespace MapGuesser\Repository;
use MapGuesser\Database\Query\Select;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
class UserRepository
{
private PersistentDataManager $pdm;
public function __construct()
{
$this->pdm = new PersistentDataManager();
}
public function getById(int $userId): ?User
{
return $this->pdm->selectFromDbById($userId, User::class);
}
public function getByEmail(string $email): ?User
{
$select = new Select(\Container::$dbConnection);
$select->where('email', '=', $email);
return $this->pdm->selectFromDb($select, User::class);
}
}

View File

@ -3,6 +3,9 @@
use MapGuesser\Interfaces\Authentication\IUser;
use MapGuesser\Interfaces\Request\IRequest;
use MapGuesser\Interfaces\Request\ISession;
use MapGuesser\PersistentData\Model\User;
use MapGuesser\PersistentData\PersistentDataManager;
use MapGuesser\Repository\UserRepository;
class Request implements IRequest
{
@ -16,12 +19,24 @@ class Request implements IRequest
private Session $session;
private UserRepository $userRepository;
private ?User $user = null;
public function __construct(string $base, array &$get, array &$post, array &$session)
{
$this->base = $base;
$this->get = &$get;
$this->post = &$post;
$this->session = new Session($session);
$this->userRepository = new UserRepository();
$userId = $this->session->get('userId');
if ($userId !== null) {
$this->user = $this->userRepository->getById($userId);
}
}
public function setParsedRouteParams(array &$routeParams)
@ -61,8 +76,18 @@ class Request implements IRequest
return $this->session;
}
public function setUser(?IUser $user): void
{
if ($user === null) {
$this->session->delete('userId');
return;
}
$this->session->set('userId', $user->getUniqueId());
}
public function user(): ?IUser
{
return $this->session->get('user');
return $this->user;
}
}

View File

@ -59,6 +59,26 @@ class Bounds
}
}
public function setSouthLat(float $southLat): void
{
$this->southLat = $southLat;
}
public function setWestLng(float $westLng): void
{
$this->westLng = $westLng;
}
public function setNorthLat(float $northLat): void
{
$this->northLat = $northLat;
}
public function setEastLng(float $eastLng): void
{
$this->eastLng = $eastLng;
}
public function getSouthLat(): float
{
return $this->southLat;

View File

@ -13,6 +13,16 @@ class Position
$this->lng = $lng;
}
public function setLat(float $lat): void
{
$this->lat = $lat;
}
public function setLng(float $lng): void
{
$this->lng = $lng;
}
public function getLat(): float
{
return $this->lat;