Migration to last.fm, removed old spotify api client. FOSRestBundle API serving data driven front end (JWT).

This commit is contained in:
boris committed 2026-02-27 16:52:57 +00:00
1 parent 391ecf1732
commit 56dea097d6
51 files changed
+1749 -844

No files matched your search

@@ -0,0 +1,203 @@
<?php
namespace App\Controller\Api\V1;
use App\Entity\Album;
use App\Entity\Review;
use App\Entity\User;
use App\Repository\AlbumRepository;
use App\Repository\ReviewRepository;
use Doctrine\ORM\EntityManagerInterface;
use FOS\RestBundle\Controller\AbstractFOSRestController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/api/v1/reviews')]
class ReviewApiController extends AbstractFOSRestController
{
public function __construct(
private readonly ReviewRepository $reviewRepository,
private readonly AlbumRepository $albumRepository,
private readonly EntityManagerInterface $entityManager,
) {
}
#[Route('', name: 'api_v1_review_index', methods: ['GET'])]
public function index(Request $request): Response
{
$limit = max(1, min(100, (int) $request->query->get('limit', 50)));
$reviews = $this->reviewRepository->findLatest($limit);
return $this->handleView($this->view([
'data' => array_map(fn(Review $review) => $this->serializeReview($review), $reviews),
], Response::HTTP_OK));
}
#[Route('/{id}', name: 'api_v1_review_show', requirements: ['id' => '\\d+'], methods: ['GET'])]
public function show(Review $review): Response
{
return $this->handleView($this->view(['data' => $this->serializeReview($review)], Response::HTTP_OK));
}
#[Route('', name: 'api_v1_review_create', methods: ['POST'])]
#[IsGranted('ROLE_USER')]
public function create(Request $request): Response
{
$payload = $this->readJsonPayload($request);
if ($payload === null) {
return $this->handleView($this->view(['error' => 'invalid_json'], Response::HTTP_BAD_REQUEST));
}
$albumId = trim((string) ($payload['album_id'] ?? ''));
$album = $this->resolveAlbum($albumId);
if (!$album instanceof Album) {
return $this->handleView($this->view(['error' => 'album_not_found'], Response::HTTP_NOT_FOUND));
}
$title = trim((string) ($payload['title'] ?? ''));
$content = trim((string) ($payload['content'] ?? ''));
$rating = (int) ($payload['rating'] ?? 0);
if ($title === '' || $content === '' || $rating < 1 || $rating > 10) {
return $this->handleView($this->view(['error' => 'title_content_rating_required'], Response::HTTP_BAD_REQUEST));
}
if (mb_strlen($content) < 20) {
return $this->handleView($this->view(['error' => 'content_too_short'], Response::HTTP_BAD_REQUEST));
}
$author = $this->getUser();
if (!$author instanceof User) {
return $this->handleView($this->view(['error' => 'authentication_required'], Response::HTTP_UNAUTHORIZED));
}
$review = new Review();
$review->setAlbum($album);
$review->setAuthor($author);
$review->setTitle($title);
$review->setContent($content);
$review->setRating($rating);
$this->entityManager->persist($review);
$this->entityManager->flush();
return $this->handleView($this->view(['data' => $this->serializeReview($review)], Response::HTTP_CREATED));
}
#[Route('/{id}', name: 'api_v1_review_update', requirements: ['id' => '\\d+'], methods: ['PUT', 'PATCH'])]
#[IsGranted('ROLE_USER')]
public function update(Review $review, Request $request): Response
{
$this->denyAccessUnlessGranted('REVIEW_EDIT', $review);
$payload = $this->readJsonPayload($request);
if ($payload === null) {
return $this->handleView($this->view(['error' => 'invalid_json'], Response::HTTP_BAD_REQUEST));
}
if (array_key_exists('title', $payload)) {
$title = trim((string) $payload['title']);
if ($title === '') {
return $this->handleView($this->view(['error' => 'title_cannot_be_empty'], Response::HTTP_BAD_REQUEST));
}
$review->setTitle($title);
}
if (array_key_exists('content', $payload)) {
$content = trim((string) $payload['content']);
if ($content === '') {
return $this->handleView($this->view(['error' => 'content_cannot_be_empty'], Response::HTTP_BAD_REQUEST));
}
if (mb_strlen($content) < 20) {
return $this->handleView($this->view(['error' => 'content_too_short'], Response::HTTP_BAD_REQUEST));
}
$review->setContent($content);
}
if (array_key_exists('rating', $payload)) {
$rating = (int) $payload['rating'];
if ($rating < 1 || $rating > 10) {
return $this->handleView($this->view(['error' => 'rating_out_of_range'], Response::HTTP_BAD_REQUEST));
}
$review->setRating($rating);
}
$this->entityManager->flush();
return $this->handleView($this->view(['data' => $this->serializeReview($review)], Response::HTTP_OK));
}
#[Route('/{id}', name: 'api_v1_review_delete', requirements: ['id' => '\\d+'], methods: ['DELETE'])]
#[IsGranted('ROLE_USER')]
public function delete(Review $review): Response
{
$this->denyAccessUnlessGranted('REVIEW_DELETE', $review);
$this->entityManager->remove($review);
$this->entityManager->flush();
return $this->handleView($this->view(null, Response::HTTP_NO_CONTENT));
}
/**
* @return array<string,mixed>
*/
private function serializeReview(Review $review): array
{
$album = $review->getAlbum();
return [
'id' => $review->getId(),
'title' => $review->getTitle(),
'content' => $review->getContent(),
'rating' => $review->getRating(),
'created_at' => $review->getCreatedAt()?->format(DATE_ATOM),
'updated_at' => $review->getUpdatedAt()?->format(DATE_ATOM),
'author' => [
'id' => $review->getAuthor()?->getId(),
'email' => $review->getAuthor()?->getEmail(),
'display_name' => $review->getAuthor()?->getDisplayName(),
],
'album' => [
'id' => $album?->getSource() === 'user' ? $album?->getLocalId() : $album?->getExternalId(),
'name' => $album?->getName(),
'source' => $album?->getSource(),
],
];
}
private function resolveAlbum(string $albumId): ?Album
{
if ($albumId === '') {
return null;
}
$local = $this->albumRepository->findOneByLocalId($albumId);
if ($local instanceof Album) {
return $local;
}
return $this->albumRepository->findOneByExternalId($albumId);
}
/**
* @return array<string,mixed>|null
*/
private function readJsonPayload(Request $request): ?array
{
$content = trim($request->getContent());
if ($content === '') {
return [];
}
try {
$decoded = $request->toArray();
} catch (\JsonException) {
return null;
}
return is_array($decoded) ? $decoded : null;
}
}