<?php
namespace App\Controller\Admin\Player;
use App\Controller\Admin\AbstractVoucherController;
use App\Entity\Client;
use App\Entity\Player;
use App\Entity\PlayerGroup;
use App\Entity\VoucherGroup;
use App\Form\Admin\VoucherCreateType;
use App\Form\Admin\VoucherImportType;
use App\Repository\ClientRepository;
use App\Repository\PlayerRepository;
use App\Repository\VoucherRepository;
use App\Validation\Admin\VoucherCreateContract;
use App\Validation\Admin\VoucherImportContract;
use DateTime;
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
use Doctrine\ORM\EntityManagerInterface;
use DomainException;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
use Symfony\Component\Form\FormError;
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\Validator\Validator\ValidatorInterface;
use Twig\Environment;
/**
* @Route("/players/{slug}/vouchers")
* @IsGranted("ROLE_CLIENT")
* @Cache(mustRevalidate=true)
*/
class VouchersController extends AbstractVoucherController
{
private ValidatorInterface $validator;
private VoucherRepository $voucherRepo;
public function __construct(ClientRepository $clientrepo, EntityManagerInterface $em, RequestStack $stack, Environment $twig, Security $security, ValidatorInterface $validator, VoucherRepository $voucherRepo) {
parent::__construct($clientrepo, $em, $stack, $twig, $security);
$this->validator = $validator;
$this->voucherRepo = $voucherRepo;
}
/**
* @Route("/")
*/
public function list(Request $r, $slug, PlayerRepository $playerrepo) {
$player = $playerrepo->getBySlug($slug);
if(!$player) {
throw $this->createNotFoundException();
}
$client = $this->ensureClient($player->getClient());
$this->setPlayerContext($player);
// Handle creation form
$createForm = $this->createForm(VoucherCreateType::class, new VoucherCreateContract(), [
'client' => $client,
'player' => $player,
]);
$createForm->handleRequest($r);
$importForm = $this->createForm(VoucherImportType::class);
$importForm->handleRequest($r);
if($createForm->isSubmitted() && $createForm->isValid()) {
$this->voucherRepo->createVouchers($player, $createForm->getData(), $this->getUser());
$this->addFlash('success', "Voucher(s) created");
return $this->redirectToRoute("app_admin_player_vouchers_list", ['slug' => $slug]);
}
if($importForm->isSubmitted() && $importForm->isValid()) {
$file = $importForm->get("file")->getData();
try {
$codes = $this->getCodesFromFile($player, $client, $file);
$this->voucherRepo->importVouchers($player, $this->getUser(), $codes);
$this->addFlash('success', count($codes)." voucher(s) imported");
return $this->redirectToRoute("app_admin_player_vouchers_list", ['slug' => $slug]);
} catch(DomainException $ex) {
$importForm->get('file')->addError(new FormError($ex->getMessage()));
} catch(UniqueConstraintViolationException $ex) {
preg_match("/Duplicate entry '[0-9]+-([^']+)'/", $ex->getMessage(), $matches);
$duplicate = $matches[1] ?? 'unkwnown';
$importForm->get('file')->addError(new FormError("Can't import file, one or more codes already exists ({$duplicate})"));
}
}
return $this->renderVoucherList('admin/player/vouchers.html.twig', $createForm, $importForm);
}
protected function getCodesFromFile(Player $player, Client $client, File $file): array {
$fp = fopen($file->getPathname(), 'r');
$columns = fgetcsv($fp, 128, ';');
if(!is_array($columns) || !count($columns)) {
throw new DomainException("Aucune colonne dans le fichier");
}
$allColumns = ['type', 'code', 'group'];
$requiredColumns = ['type', 'code'];
if(!$player->getIsLive()) {
$allColumns = array_merge($allColumns, ['replay', 'validitystart', 'validityend']);
$requiredColumns[] = "replay";
}
// Compute mappings
$mapping = [];
foreach($columns as $i => $col) {
$mapped = false;
foreach($allColumns as $name) {
if(strtolower(trim($col)) == $name) {
$mapping[$name] = $i;
$mapped = true;
break;
}
}
if(!$mapped) {
throw new \DomainException("Can't map column '{$col}'");
}
}
// Check required columns
foreach($requiredColumns as $name) {
if(!isset($mapping[$name])) {
throw new DomainException("Missing {$name} column");
}
}
// Get list of voucher groups
// Todo: don't load all voucher groups, get on need basis while discovering codes
$groups = array_reduce($client->getVoucherGroups()->toArray(), fn($result, VoucherGroup $group) => $result + [$group->getName() => $group], []);
$defaultGroup = $client->getDefaultGroup();
$line = 1;
$codes = [];
while(!feof($fp)) {
$data = fgetcsv($fp, 128, ';');
// Ignore empty lines
if(!$data || !count($data)) {
continue;
}
if(count($data) != count($mapping)) {
throw new DomainException("Bad number of columns in line en ligne $line");
}
$code = new VoucherImportContract();
$code->type = trim($data[$mapping['type']]);
$code->code = trim($data[$mapping['code']]);
if(!isset($mapping['group'])) {
$code->group = $defaultGroup;
} else {
$code->group = $groups[trim($data[$mapping['group']])] ?? null;
if(!$code->group) {
throw new DomainException("Inexistant voucher group {$data[$mapping['group']]} in line {$line}");
}
}
if(isset($mapping['validitystart'])) {
$code->dateStart = DateTime::createFromFormat("Y-m-d", trim($data[$mapping['validitystart']]));
}
if(isset($mapping['validityend'])) {
$code->dateEnd = DateTime::createFromFormat("Y-m-d", trim($data[$mapping['validityend']]));
}
$errors = $this->validator->validate($code);
if(count($errors)) {
throw new \DomainException("Error line {$line} for code {$code->code}: {$errors[0]->getMessage()}");
}
$codes[$code->code] = $code;
$line++;
}
return $codes;
}
}