wtf
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 2m0s
All checks were successful
CI - Build Tonehaus Docker image / tonehaus-ci-build (push) Successful in 2m0s
This commit is contained in:
207
src/Command/SeedDemoReviewsCommand.php
Normal file
207
src/Command/SeedDemoReviewsCommand.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?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)]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user