Generate grid

This commit is contained in:
thibaud-leclere
2026-01-31 16:17:24 +01:00
parent 1ebf8b99b3
commit a196fac6c6
10 changed files with 158 additions and 40 deletions

View File

@@ -1,3 +1,9 @@
body { body {
background-color: skyblue; background-color: skyblue;
font-family: 'Noto Sans', sans-serif;
}
#actors td {
width: 16px;
height: 16px;
} }

View File

@@ -29,6 +29,9 @@ doctrine:
alias: App alias: App
controller_resolver: controller_resolver:
auto_mapping: false auto_mapping: false
dql:
numeric_functions:
Random: App\Doctrine\Extension\Random
when@prod: when@prod:
doctrine: doctrine:

View File

@@ -2,12 +2,12 @@
namespace App\Context\TMDB; namespace App\Context\TMDB;
use App\Model\TMDB\TMDBActor; use App\Model\TMDB\TMDBMovieCredit;
class MovieCreditsContext class MovieCreditsContext
{ {
public function __construct( public function __construct(
/** @var TMDBActor[] */ /** @var TMDBMovieCredit[] */
public array $cast { get => $this->cast; }, public array $cast { get => $this->cast; },
) {} ) {}
} }

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Controller; namespace App\Controller;
use App\Gateway\TMDBGateway; use App\Gateway\TMDBGateway;
use App\Repository\ActorRepository;
use App\Repository\MovieRepository; use App\Repository\MovieRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@@ -14,24 +15,60 @@ use Symfony\Component\Serializer\SerializerInterface;
class HomepageController extends AbstractController class HomepageController extends AbstractController
{ {
public function __construct( public function __construct(
private readonly MovieRepository $movieRepository, private readonly ActorRepository $actorRepository
private readonly TMDBGateway $TMDBGateway,
) {} ) {}
#[Route('/')] #[Route('/')]
public function index(SerializerInterface $serializer): Response public function index(SerializerInterface $serializer): Response
{ {
$movie = $this->movieRepository->findOneBy([]); // Final actor to be guessed
$creditsContext = $this->TMDBGateway->getMovieCredits($movie->getTmdbId()); $mainActor = $this->actorRepository->findOneRandom(4);
$cast = $creditsContext->cast;
$actors = [];
foreach ($cast as $actor) {
if (2 <= $actor->popularity) {
$actors[] = $actor;
}
}
dd($actors);
return $this->render('homepage/index.html.twig'); // Actors for the grid
$actors = [];
$leftSize = 0;
$rightSize = 0;
foreach (str_split(strtolower($mainActor->getName())) as $char) {
if (!preg_match('/[a-z]/', $char)) {
continue;
}
$tryFindActor = 0;
do {
$actor = $this->actorRepository->findOneRandom(4, $char);
++$tryFindActor;
} while (
$actor === $mainActor
|| in_array($actor, array_map(fn ($actorMap) => $actorMap['actor'], $actors))
|| $tryFindActor < 5
);
$actorData = [
'actor' => $actor,
'pos' => strpos($actor->getName(), $char),
];
if ($leftSize < $actorData['pos']) {
$leftSize = $actorData['pos'];
}
$rightSizeActor = strlen($actor->getName()) - $actorData['pos'] - 1;
if ($rightSize < $rightSizeActor) {
$rightSize = $rightSizeActor;
}
$actors[] = $actorData;
}
// Predict grid size
$width = $rightSize + $leftSize + 1;
$middle = $leftSize;
return $this->render('homepage/index.html.twig', [
'mainActor' => $mainActor,
'actors' => $actors,
'width' => $width,
'middle' => $middle,
]);
} }
} }

View File

@@ -0,0 +1,27 @@
<?php
namespace App\Doctrine\Extension;
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
use Doctrine\ORM\Query\Parser;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
class Random extends FunctionNode
{
/**
* @throws QueryException
*/
public function parse(Parser $parser): void
{
$parser->match(TokenType::T_IDENTIFIER);
$parser->match(TokenType::T_OPEN_PARENTHESIS);
$parser->match(TokenType::T_CLOSE_PARENTHESIS);
}
public function getSql(SqlWalker $sqlWalker): string
{
return 'RANDOM()';
}
}

View File

@@ -2,6 +2,7 @@
namespace App\Gateway; namespace App\Gateway;
use App\Context\TMDB\ActorCreditsContext;
use App\Context\TMDB\MovieCreditsContext; use App\Context\TMDB\MovieCreditsContext;
use App\Context\TMDB\MovieSearchContext; use App\Context\TMDB\MovieSearchContext;
use App\Exception\GatewayException; use App\Exception\GatewayException;
@@ -44,7 +45,7 @@ readonly class TMDBGateway
*/ */
public function getMovieCredits(int $movieId): ?MovieCreditsContext public function getMovieCredits(int $movieId): ?MovieCreditsContext
{ {
$url = str_replace('{id}', $movieId, $this->host.self::MOVIE_CREDITS_URI); $url = $this->host.str_replace('{id}', $movieId, self::MOVIE_CREDITS_URI);
return $this->fetchSerialized('GET', $url, MovieCreditsContext::class); return $this->fetchSerialized('GET', $url, MovieCreditsContext::class);
} }

View File

@@ -2,7 +2,7 @@
namespace App\Model\TMDB; namespace App\Model\TMDB;
class TMDBActor class TMDBMovieCredit
{ {
public function __construct( public function __construct(
public int $id { get => $this->id; }, public int $id { get => $this->id; },

View File

@@ -16,28 +16,26 @@ class ActorRepository extends ServiceEntityRepository
parent::__construct($registry, Actor::class); parent::__construct($registry, Actor::class);
} }
// /** public function findOneRandom(?float $popularity = null, ?string $char = null): Actor
// * @return Actor[] Returns an array of Actor objects {
// */ $qb = $this->createQueryBuilder('o');
// public function findByExampleField($value): array $expr = $qb->expr();
// {
// return $this->createQueryBuilder('a')
// ->andWhere('a.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('a.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Actor if (!empty($popularity)) {
// { $qb->andWhere($expr->gte('o.popularity', ':popularity'))
// return $this->createQueryBuilder('a') ->setParameter('popularity', $popularity);
// ->andWhere('a.exampleField = :val') }
// ->setParameter('val', $value)
// ->getQuery() if (!empty($char)) {
// ->getOneOrNullResult() $qb->andWhere($expr->like('o.name', ':name'))
// ; ->setParameter('name', '%'.$char.'%');
// } }
return $qb
->orderBy('RANDOM()')
->setMaxResults(1)
->getQuery()
->getOneOrNullResult()
;
}
} }

21
src/Twig/AppExtension.php Normal file
View File

@@ -0,0 +1,21 @@
<?php
namespace App\Twig;
use Twig\Extension\AbstractExtension;
use Twig\TwigFilter;
class AppExtension extends AbstractExtension
{
public function getFilters(): array
{
return [
new TwigFilter('match', [$this, 'match']),
];
}
public function match(string $string, string $pattern): bool
{
return preg_match($pattern, $string);
}
}

View File

@@ -1 +1,26 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block body %}
<table id="actors">
{% set iActor = 0 %}
{% for mainChar in mainActor.name|split('') %}
{% if not mainChar|match('/[a-zA-Z]/') %}
<tr><td></td></tr>
{% else %}
{% set actor = actors[iActor] %}
<tr>
{% set i = 0 %}
{% set start = middle - actor.pos %}
{% for c in range(0, width) %}
{% if c >= start and c - start < actor.actor.name|length %}
<td {% if c - start == actor.pos %}style="color:red;"{% endif %}>{{ actor.actor.name|slice(c - start, 1)|upper }}</td>
{% else %}
<td></td>
{% endif %}
{% endfor %}
</tr>
{% set iActor = iActor + 1 %}
{% endif %}
{% endfor %}
</table>
{% endblock %}