Files
tonehaus/src/Command/SeedDemoReviewsCommand.php
boris dae8f3d999
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 2m0s
wtf
2025-11-28 02:00:11 +00:00

208 lines
7.0 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Command;
use App\Entity\Album;
use App\Entity\Review;
use App\Entity\User;
use App\Repository\AlbumRepository;
use App\Repository\UserRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'app:seed-demo-reviews',
description: 'Generate demo reviews across existing albums.'
)]
class SeedDemoReviewsCommand extends Command
{
private const SUBJECTS = [
'Textures', 'Melodies', 'Lyrics', 'Drums', 'Synths', 'Vocals', 'Atmosphere', 'Production',
'Hooks', 'Transitions', 'Energy', 'Dynamics', 'Story', 'Beats', 'Guitars',
];
private const VERBS = [
'ignite', 'carry', 'elevate', 'anchor', 'transform', 'frame', 'redefine', 'ground', 'highlight',
'soften', 'energize', 'contrast', 'bend', 'reshape', 'underline',
];
private const QUALIFIERS = [
'beautifully', 'with surprising restraint', 'like neon waves', 'with cinematic flair',
'through dusty speakers', 'in unexpected directions', 'along a familiar path', 'with swagger',
'with delicate pulses', 'through midnight haze', 'under fluorescent skies', 'with raw urgency',
];
public function __construct(
private readonly AlbumRepository $albumRepository,
private readonly UserRepository $userRepository,
private readonly EntityManagerInterface $entityManager,
) {
parent::__construct();
}
protected function configure(): void
{
$this
->addOption('max-per-album', null, InputOption::VALUE_OPTIONAL, 'Maximum reviews per album', 10)
->addOption('min-per-album', null, InputOption::VALUE_OPTIONAL, 'Minimum reviews per selected album', 1)
->addOption('cover-percent', null, InputOption::VALUE_OPTIONAL, 'Percent of albums that should receive reviews (0-100)', 60)
->addOption('only-empty', null, InputOption::VALUE_NONE, 'Only seed albums that currently have no reviews');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$albums = $this->albumRepository->findAll();
$users = $this->userRepository->findAll();
if ($albums === [] || $users === []) {
$io->warning('Need at least one album and one user to seed reviews.');
return Command::FAILURE;
}
$minPerAlbum = max(0, (int) $input->getOption('min-per-album'));
$maxPerAlbum = max($minPerAlbum, (int) $input->getOption('max-per-album'));
$coverPercent = max(0, min(100, (int) $input->getOption('cover-percent')));
$selectedAlbums = $this->selectAlbums($albums, $coverPercent);
$onlyEmpty = (bool) $input->getOption('only-empty');
$created = 0;
$processedAlbums = 0;
foreach ($selectedAlbums as $album) {
if ($onlyEmpty && $this->albumHasReviews($album)) {
continue;
}
$targetReviews = random_int($minPerAlbum, max($minPerAlbum, $maxPerAlbum));
$created += $this->seedForAlbum($album, $users, $targetReviews);
$processedAlbums++;
}
$this->entityManager->flush();
if ($created === 0) {
$io->warning('No reviews were created. Try relaxing the filters or ensure there are albums without reviews.');
return Command::SUCCESS;
}
$io->success(sprintf('Created %d demo reviews across %d albums.', $created, max($processedAlbums, 1)));
return Command::SUCCESS;
}
/**
* @param list<Album> $albums
* @return list<Album>
*/
private function selectAlbums(array $albums, int $coverPercent): array
{
if ($coverPercent >= 100) {
return $albums;
}
$selected = [];
foreach ($albums as $album) {
if (random_int(1, 100) <= $coverPercent) {
$selected[] = $album;
}
}
return $selected === [] ? [$albums[array_rand($albums)]] : $selected;
}
/**
* @param list<User> $users
*/
private function seedForAlbum(Album $album, array $users, int $targetReviews): int
{
$created = 0;
$existingAuthors = $this->fetchExistingAuthors($album);
$availableUsers = array_filter($users, fn(User $user) => !isset($existingAuthors[$user->getId() ?? -1]));
if ($availableUsers === []) {
return 0;
}
$targetReviews = min($targetReviews, count($availableUsers));
shuffle($availableUsers);
$selectedUsers = array_slice($availableUsers, 0, $targetReviews);
foreach ($selectedUsers as $user) {
$review = new Review();
$review->setAlbum($album);
$review->setAuthor($user);
$review->setRating(random_int(4, 10));
$review->setTitle($this->generateTitle());
$review->setContent($this->generateContent($album));
$this->entityManager->persist($review);
$created++;
}
return $created;
}
/**
* @return array<int,bool>
*/
private function fetchExistingAuthors(Album $album): array
{
$qb = $this->entityManager->createQueryBuilder()
->select('IDENTITY(r.author) AS authorId')
->from(Review::class, 'r')
->where('r.album = :album')
->setParameter('album', $album);
$rows = $qb->getQuery()->getScalarResult();
$out = [];
foreach ($rows as $row) {
$out[(int) $row['authorId']] = true;
}
return $out;
}
private function albumHasReviews(Album $album): bool
{
$count = (int) $this->entityManager->createQueryBuilder()
->select('COUNT(r.id)')
->from(Review::class, 'r')
->where('r.album = :album')
->setParameter('album', $album)
->getQuery()
->getSingleScalarResult();
return $count > 0;
}
private function generateTitle(): string
{
$subject = self::SUBJECTS[random_int(0, count(self::SUBJECTS) - 1)];
$verb = self::VERBS[random_int(0, count(self::VERBS) - 1)];
return sprintf('%s %s the vibe', $subject, $verb);
}
private function generateContent(Album $album): string
{
$qualifier = self::QUALIFIERS[random_int(0, count(self::QUALIFIERS) - 1)];
return sprintf(
'Listening to "%s" feels like %s. %s %s %s, and by the end it lingers far longer than expected.',
$album->getName(),
$qualifier,
self::SUBJECTS[random_int(0, count(self::SUBJECTS) - 1)],
self::VERBS[random_int(0, count(self::VERBS) - 1)],
self::QUALIFIERS[random_int(0, count(self::QUALIFIERS) - 1)]
);
}
}