All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 2m0s
208 lines
7.0 KiB
PHP
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)]
|
|
);
|
|
}
|
|
}
|
|
|