implermented key import

This commit is contained in:
Michel Fedde 2024-07-05 22:29:35 +02:00
parent ace0de4063
commit 74e1b25fcf
20 changed files with 1035 additions and 12 deletions

View file

@ -6,6 +6,7 @@ namespace GamesShop;
use Doctrine\DBAL\Connection;
use Doctrine\DBAL\DriverManager;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\ORMSetup;
use GamesShop\Environment\EnvironmentHandler;
@ -27,6 +28,7 @@ final class DoctrineManager
$entityManager = new EntityManager($connection, $config);
$container->addShared(EntityManager::class, $entityManager);
$container->addShared(EntityManagerInterface::class, $entityManager);
$container->addShared(Connection::class, $connection);
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace GamesShop\Entities\Games;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'games')]
final class Game
{
#[ORM\Id]
#[ORM\Column(type: 'integer', options: ['unsigned' => true])]
#[ORM\GeneratedValue]
private int|null $id;
#[ORM\Column]
private string $name;
/**
* @param string $name
*/
public function __construct(string $name)
{
$this->name = $name;
}
}

View file

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace GamesShop\Entities\Games;
use Doctrine\ORM\Mapping as ORM;
use GamesShop\Entities\Account\User;
#[ORM\Entity]
#[ORM\Table(name: 'keys')]
final class Key
{
#[ORM\Id]
#[ORM\Column(type: 'integer', options: ['unsigned' => true])]
#[ORM\GeneratedValue]
private int|null $id;
#[ORM\ManyToOne]
private Game $game;
#[ORM\ManyToOne]
private User $contributedUser;
#[ORM\Column]
private string $key;
#[ORM\Column(type: 'string', enumType: Store::class)]
private Store $store;
#[ORM\Column(nullable: true)]
private string|null $storeLink;
#[ORM\Column]
private string|null $fromWhere;
public function __construct(Game $game, User $contributedUser, string $key, Store $store, ?string $storeLink, ?string $fromWhere)
{
$this->game = $game;
$this->contributedUser = $contributedUser;
$this->key = $key;
$this->store = $store;
$this->storeLink = $storeLink;
$this->fromWhere = $fromWhere;
}
}

View file

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace GamesShop\Entities\Games;
enum KeyAttribute: string
{
case NONE = 'none';
case GAME_NAME = "game_name";
case KEY = "key";
case STORE = 'store';
case FROM = 'from';
public static function casesAsAssociative(): array {
$result = [];
foreach (self::cases() as $case) {
$result[$case->name] = $case->value;
}
return $result;
}
}

View file

@ -0,0 +1,15 @@
<?php
declare(strict_types=1);
namespace GamesShop\Entities\Games;
enum Store: string
{
case STEAM = 'steam';
case GOG = 'gog';
case EPICGAMES = 'epicgames';
case ORIGIN = 'origin';
case UPLAY = 'uplay';
case BATTLENET = 'battlenet';
case EXTERNAL = 'external';
}

View file

@ -0,0 +1,165 @@
<?php
declare(strict_types=1);
namespace GamesShop\Importer;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use GamesShop\Entities\Account\User;
use GamesShop\Entities\Games\Game;
use GamesShop\Entities\Games\Key;
use GamesShop\Entities\Games\KeyAttribute;
use GamesShop\Entities\Games\Store;
use PhpOffice\PhpSpreadsheet\IOFactory;
final class GameImporter
{
private const HEADER_ROW_INDEX = 1;
private const STORE_ADDITIONAL_CASES = [
'epic' => Store::EPICGAMES,
'ea' => Store::ORIGIN,
'eaplay' => Store::ORIGIN,
'ubisoft' => Store::UPLAY,
'activision' => Store::BATTLENET
];
public function __construct(
private readonly EntityManagerInterface $entityManager,
) { }
/**
* @param string $path
* @return ImportColumnInterpretation[]
*/
public function interpret(string $path): array {
$spreadsheet = IOFactory::load($path);
$worksheet = $spreadsheet->getSheet(0);
$result = [];
foreach ($worksheet->getColumnIterator() as $column) {
$columnIndex = $column->getColumnIndex();
$value = $worksheet->getCell(sprintf('%s%d', $columnIndex, self::HEADER_ROW_INDEX))->getValueString();
if (empty(trim($value))) {
continue;
}
$guessedAttribute = $this->guessAttribute($value);
$result[] = new ImportColumnInterpretation(
$columnIndex,
$value,
$guessedAttribute
);
}
return $result;
}
private function guessAttribute(string $value): KeyAttribute|null {
$value = trim($value);
$value = strtolower($value);
$value = str_replace(' ', '_', $value);
$attribute = match($value) {
'key' => KeyAttribute::KEY,
'name', 'game_name', 'game' => KeyAttribute::GAME_NAME,
'from' => KeyAttribute::FROM,
'store', 'for' => KeyAttribute::STORE,
default => null
};
return $attribute;
}
/**
* @param string[] $columnDefinitions
*/
public function import(string $path, array $columnDefinitions, User $contributedUser): array {
$spreadsheet = IOFactory::load($path);
$worksheet = $spreadsheet->getSheet(0);
$totalRows = 0;
$addedAmount = 0;
foreach ($worksheet->getRowIterator(self::HEADER_ROW_INDEX + 1) as $row) {
$totalRows++;
$values = [
'name' => null,
'key' => null,
'from' => null,
'store' => null,
'store_link' => null
];
foreach ($columnDefinitions as $columnIndex => $attribute) {
$value = $worksheet->getCell(sprintf('%s%d', $columnIndex, $row->getRowIndex()))->getValueString();
switch(KeyAttribute::from($attribute)) {
case KeyAttribute::NONE:
break;
case KeyAttribute::GAME_NAME:
$values['name'] = $value;
break;
case KeyAttribute::KEY:
$values['key'] = $value;
break;
case KeyAttribute::FROM:
$values['from'] = $value;
break;
case KeyAttribute::STORE:
$store = $this->interpretStore($value);
$values['store'] = $store;
if ($store === Store::EXTERNAL) {
$values['store_link'] = $value;
}
break;
}
}
if ($values['key'] === null || $values['name'] === null || $values['store'] === null) {
continue;
}
$game = $this->entityManager->getRepository(Game::class)->findOneBy([ 'name' => $values['name'] ]);
if ($game === null) {
$game = new Game($values['name']);
}
$key = new Key(
$game,
$contributedUser,
$values['key'],
$values['store'],
$values['store_link'],
$values['from'],
);
$this->entityManager->persist($game);
$this->entityManager->persist($key);
$addedAmount++;
}
$this->entityManager->flush();
return [$totalRows, $addedAmount];
}
private function interpretStore(string $storeString): Store {
$storeString = trim($storeString);
$storeString = strtolower($storeString);
$storeString = str_replace(' ', '', $storeString);
$triedConversion = Store::tryFrom($storeString);
if ($triedConversion !== null) {
return $triedConversion;
}
if (array_key_exists($storeString, self::STORE_ADDITIONAL_CASES)) {
return self::STORE_ADDITIONAL_CASES[$storeString];
}
return Store::EXTERNAL;
}
}

View file

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace GamesShop\Importer;
use GamesShop\Entities\Games\KeyAttribute;
use JsonSerializable;
final readonly class ImportColumnInterpretation implements JsonSerializable
{
public function __construct(
public string $index,
public string $displayName,
public KeyAttribute|null $guessedAttribute
)
{ }
public function jsonSerialize(): array
{
return [
'index' => $this->index,
'displayName' => $this->displayName,
'guessedAttribute' => $this->guessedAttribute ?? null
];
}
}

View file

@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api\Web;
use GamesShop\Importer\GameImporter;
use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\UploadedFile;
use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class ImportKeysPrepareRoute
{
public function __construct(
private readonly LoginHandler $loginHandler,
private readonly GameImporter $importer,
) { }
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException();
}
$user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException();
}
/**
* @var UploadedFile $file
*/
$file = $request->getUploadedFiles()['file'] ?? null;
if (!$file === null) {
throw new BadRequestException();
}
$fileName = tempnam(sys_get_temp_dir(), 'ImportKeys');
$file->moveTo($fileName);
$results = $this->importer->interpret($fileName);
unlink($fileName);
return new JsonResponse($results);
}
}

View file

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing\Api\Web;
use GamesShop\Importer\GameImporter;
use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission;
use Laminas\Diactoros\Response\JsonResponse;
use Laminas\Diactoros\UploadedFile;
use League\Route\Http\Exception\BadRequestException;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class ImportKeysRoute
{
public function __construct(
private readonly LoginHandler $loginHandler,
private readonly GameImporter $importer,
) { }
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException();
}
$user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException();
}
/**
* @var UploadedFile $file
*/
$file = $request->getUploadedFiles()['file'] ?? null;
if (!$file === null) {
throw new BadRequestException();
}
$fileName = tempnam(sys_get_temp_dir(), 'ImportKeys');
$file->moveTo($fileName);
$columnDefs = $request->getParsedBody()['columns'];
[$total, $imported] = $this->importer->import($fileName, $columnDefs, $user);
unlink($fileName);
return new JsonResponse([ 'success' => true, 'total' => $total, 'imported' => $imported ]);
}
}

View file

@ -9,5 +9,8 @@ final class WebAPIRoutes
{
public static function applyRoutes(RouteGroup $group): void {
$group->post('/users/{id:number}', UserModifyRoute::class);
$group->post('/keys/import/prepare', ImportKeysPrepareRoute::class);
$group->post('/keys/import/perform', ImportKeysRoute::class);
}
}

View file

@ -0,0 +1,41 @@
<?php
declare(strict_types=1);
namespace GamesShop\Routing;
use GamesShop\Login\LoginHandler;
use GamesShop\Login\UserPermission;
use GamesShop\Routing\Responses\TemplateResponse;
use League\Route\Http\Exception\ForbiddenException;
use League\Route\Http\Exception\UnauthorizedException;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final class KeysRoute
{
public function __construct(
private readonly LoginHandler $loginHandler
)
{
}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
if (!$this->loginHandler->isLoggedIn()) {
throw new UnauthorizedException();
}
$user = $this->loginHandler->getCurrentUser();
if (!$user->getPermission()->hasLevel(UserPermission::PROVIDER)) {
throw new ForbiddenException();
}
return new TemplateResponse('key-manager');
}
public static function applyRoutes(\League\Route\Router $router): void
{
$router->get('/keys', KeysRoute::class);
}
}

View file

@ -42,6 +42,8 @@ final class Router
IndexRoute::applyRoutes($router);
LoginRoutes::addRoutes($router);
SetupRoute::applyRoutes($router);
KeysRoute::applyRoutes($router);
AdminAccountConfigRoute::applyRoutes($router);
APIRoutes::applyRoutes($router);