Podcast and Episode management (still WIP…)

This commit is contained in:
dece 2023-10-14 18:26:21 +02:00
parent c39e7039ac
commit ab35d77b42
14 changed files with 418 additions and 15 deletions

View file

@ -0,0 +1,151 @@
<?php
namespace App\Controller;
use App\Entity\Episode;
use App\Form\EpisodeType;
use App\Repository\EpisodeRepository;
use App\Repository\PodcastRepository;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\FormError;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\String\Slugger\SluggerInterface;
#[Route('/manage/episodes')]
class EpisodeController extends AbstractController
{
public function __construct(protected LoggerInterface $logger)
{}
#[Route('/', name: 'app_episode_index', methods: ['GET'])]
public function index(EpisodeRepository $episodeRepository): Response
{
return $this->render('episode/index.html.twig', [
'episodes' => $episodeRepository->findAll(),
]);
}
#[Route('/new', name: 'app_episode_new', methods: ['GET', 'POST'])]
public function new(
Request $request,
EntityManagerInterface $entityManager,
PodcastRepository $podcastRepository,
SluggerInterface $slugger,
): Response
{
$queryPodcastId = $request->query->getInt('podcast', 0);
$episode = new Episode();
$form = $this->createForm(
EpisodeType::class,
$episode,
[
'owner' => $this->getUser(),
'selectedPodcast' => $queryPodcastId,
]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$this->logger->info("FLUMZO " . json_encode($form->getData()));
// Check that the user is the owner of the podcast this episode should belong to.
$podcastId = $form->get('podcast')->getData();
$podcast = $podcastRepository->find($podcastId ?? $queryPodcastId);
if (
$podcast === null
|| $podcast->getOwner()->getId() !== $this->getUser()->getId()
) {
$form->get('podcast')->addError(new FormError('Invalid podcast.'));
return $this->render('episode/new.html.twig', ['episode' => $episode, 'form' => $form]);
}
$episode->setPodcast($podcast);
// Ensure the uploaded audio file is saved properly.
if (!$this->handleAudioChange($form, $episode, $slugger)) {
$form->get('logo')->addError(new FormError('Could not upload audio.'));
return $this->render('episode/new.html.twig', ['episode' => $episode, 'form' => $form]);
}
$entityManager->persist($episode);
$entityManager->flush();
return $this->redirectToRoute('app_episode_index', [], Response::HTTP_SEE_OTHER);
}
return $this->render('episode/new.html.twig', [
'episode' => $episode,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_episode_show', methods: ['GET'])]
public function show(Episode $episode): Response
{
return $this->render('episode/show.html.twig', [
'episode' => $episode,
]);
}
#[Route('/{id}/edit', name: 'app_episode_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, Episode $episode, EntityManagerInterface $entityManager): Response
{
$form = $this->createForm(
EpisodeType::class,
$episode,
['selectedPodcast' => $episode->getPodcast()->getId()]
);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->flush();
return $this->redirectToRoute('app_episode_index', [], Response::HTTP_SEE_OTHER);
}
return $this->render('episode/edit.html.twig', [
'episode' => $episode,
'form' => $form,
]);
}
#[Route('/{id}', name: 'app_episode_delete', methods: ['POST'])]
public function delete(Request $request, Episode $episode, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$episode->getId(), $request->request->get('_token'))) {
$entityManager->remove($episode);
$entityManager->flush();
}
return $this->redirectToRoute('app_episode_index', [], Response::HTTP_SEE_OTHER);
}
protected function handleAudioChange(
FormInterface $form,
Episode $episode,
SluggerInterface $slugger
): bool
{
$audioFile = $form->get('audio')->getData();
if ($audioFile) {
$originalFilename = pathinfo($audioFile->getClientOriginalName(), PATHINFO_FILENAME);
$safeFilename = $slugger->slug($originalFilename);
$newFilename = $safeFilename . '-' . uniqid() . '.' . $audioFile->guessExtension();
try {
$audioFile->move($this->getParameter('audio_directory'), $newFilename);
} catch (FileException $e) {
return false;
}
$episode->setAudioFilename($newFilename);
}
return true;
}
}

View file

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Entity\Podcast; use App\Entity\Podcast;
use App\Form\PodcastType; use App\Form\PodcastType;
use App\Repository\EpisodeRepository;
use App\Repository\PodcastRepository; use App\Repository\PodcastRepository;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
@ -97,7 +98,11 @@ class PodcastController extends AbstractController
} }
#[Route('/{id}', name: 'app_podcast_delete', methods: ['POST'])] #[Route('/{id}', name: 'app_podcast_delete', methods: ['POST'])]
public function delete(Request $request, Podcast $podcast, EntityManagerInterface $entityManager): Response public function delete(
Request $request,
Podcast $podcast,
EntityManagerInterface $entityManager
): Response
{ {
if ($this->isCsrfTokenValid('delete'.$podcast->getId(), $request->request->get('_token'))) { if ($this->isCsrfTokenValid('delete'.$podcast->getId(), $request->request->get('_token'))) {
$entityManager->remove($podcast); $entityManager->remove($podcast);
@ -130,4 +135,16 @@ class PodcastController extends AbstractController
return true; return true;
} }
#[Route('/{id}/episodes', name: 'app_podcast_episodes_index', methods: ['GET'])]
public function episodes_index(
Podcast $podcast,
EpisodeRepository $episodeRepository
): Response
{
return $this->render('episode/index.html.twig', [
'podcast' => $podcast,
'episodes' => $episodeRepository->findBy(['podcast' => $podcast]),
]);
}
} }

71
src/Form/EpisodeType.php Normal file
View file

@ -0,0 +1,71 @@
<?php
namespace App\Form;
use App\Entity\Episode;
use App\Entity\Podcast;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\QueryBuilder;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\File;
class EpisodeType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$owner = $options['owner'] ?? null;
$podcastId = $options['selectedPodcast'] ?? null;
$builder
->add('title')
->add('description')
->add('audio', FileType::class, [
'help' => 'The audio file of your episode.',
'mapped' => false,
'required' => false,
'constraints' => new File([
'maxSize' => '4m',
'mimeTypes' => ['audio/mpeg', 'audio/ogg', 'audio/opus', 'audio/aac', 'audio/flac', 'audio/webm'],
'mimeTypesMessage' => 'Please select an audio file (MP3, OGG audio, Opus, AAC, FLAC, WebM audio).',
]),
])
->add('publicationDate')
->add('podcast', EntityType::class, [
'help' => 'The podcast this episode belongs to. Cannot be changed after creation.',
'class' => Podcast::class,
'query_builder' => function (EntityRepository $er) use ($owner, $podcastId): QueryBuilder {
$qb = $er->createQueryBuilder('p');
if ($owner !== null) {
$qb
->andWhere('p.owner = :ownerId')
->setParameter('ownerId', $owner->getId());
}
if ($podcastId !== null) {
$qb
->andWhere('p.id = :podcastId')
->setParameter('podcastId', $podcastId);
}
return $qb->orderBy('p.name', 'ASC');
},
'placeholder' => false,
'disabled' => $podcastId !== null,
'empty_data' => "$podcastId",
])
;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Episode::class,
'owner' => null,
'selectedPodcast' => null,
]);
$resolver->setAllowedTypes('selectedPodcast', 'int');
}
}

View file

@ -13,6 +13,12 @@
{% endblock %} {% endblock %}
</head> </head>
<body> <body>
{% if app.user %}
<nav>
<a href="{{ path('app_podcast_index' )}}">Manage podcasts</a>
<a href="{{ path('app_episode_index' )}}">Manage episodes</a>
</nav>
{% endif %}
{% block body %}{% endblock %} {% block body %}{% endblock %}
</body> </body>
</html> </html>

View file

@ -0,0 +1,4 @@
<form method="post" action="{{ path('app_episode_delete', {'id': episode.id}) }}" onsubmit="return confirm('Are you sure you want to delete this item?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ episode.id) }}">
<button class="btn">Delete</button>
</form>

View file

@ -0,0 +1,4 @@
{{ form_start(form) }}
{{ form_widget(form) }}
<button class="btn">{{ button_label|default('Save') }}</button>
{{ form_end(form) }}

View file

@ -0,0 +1,13 @@
{% extends 'base.html.twig' %}
{% block title %}Edit Episode{% endblock %}
{% block body %}
<h1>Edit Episode</h1>
{{ include('episode/_form.html.twig', {'button_label': 'Update'}) }}
<a href="{{ path('app_episode_index') }}">Go back to list</a>
{{ include('episode/_delete_form.html.twig') }}
{% endblock %}

View file

@ -0,0 +1,56 @@
{% extends 'base.html.twig' %}
{% block title %}Episode index{% endblock %}
{% block body %}
<h1>
{% if podcast is defined %}
Episodes for {{ podcast.name }}
{% else %}
All your episodes
{% endif %}
</h1>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
<th>Publication date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{% for episode in episodes %}
<tr>
<td>{{ episode.id }}</td>
<td>{{ episode.title }}</td>
<td>{{ episode.publicationDate ? episode.publicationDate|date('Y-m-d H:i:s') : '' }}</td>
<td>
<a href="{{ path('app_episode_show', {'id': episode.id}) }}"
><button>Show</button></a>
<a href="{{ path('app_episode_edit', {'id': episode.id}) }}"
><button>Edit</button></a>
</td>
</tr>
{% else %}
<tr>
<td colspan="4">No episodes found.</td>
</tr>
{% endfor %}
</tbody>
</table>
{% if podcast is defined %}
<a href="{{ path('app_episode_new', {'podcast': podcast.id }) }}"
><button>Create new episode of this podcast</button></a>
<a href="{{ path('app_podcast_show', {'id': podcast.id}) }}">Back to podcast</a>
{% else %}
<a href="{{ path('app_episode_new') }}"><button>Create new episode</button></a>
<p>
To manage episodes of a specific podcast, open the podcast page and use
the “Manage podcasts” link.
</p>
{% endif %}
{% endblock %}

View file

@ -0,0 +1,19 @@
{% extends 'base.html.twig' %}
{% block title %}New episode{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
select { width: auto; background-image: none; }
</style>
{% endblock %}
{% block body %}
<h1>Create new episode</h1>
{{ include('episode/_form.html.twig') }}
<a href="{{ path('app_episode_index') }}">Go back to list</a>
{% endblock %}

View file

@ -0,0 +1,49 @@
{% extends 'base.html.twig' %}
{% block title %}Episode “{{ episode.title }}{% endblock %}
{% block stylesheets %}
{{ parent() }}
<style>
th { min-width: 8em; }
</style>
{% endblock %}
{% block body %}
<h1>Episode “{{ episode.title }}”</h1>
<table class="table">
<tbody>
<tr>
<th>ID</th>
<td>{{ episode.id }}</td>
</tr>
<tr>
<th>Title</th>
<td>{{ episode.title }}</td>
</tr>
<tr>
<th>Description</th>
<td>{{ episode.description }}</td>
</tr>
<tr>
<th>Audio file name</th>
<td>{{ episode.audioFilename }}</td>
</tr>
<tr>
<th>Audio</th>
<td><mark>TODO</mark></td>
</tr>
<tr>
<th>Publication date</th>
<td>{{ episode.publicationDate ? episode.publicationDate|date('Y-m-d H:i:s') : '' }}</td>
</tr>
</tbody>
</table>
<a href="{{ path('app_episode_index') }}">back to list</a>
<a href="{{ path('app_episode_edit', {'id': episode.id}) }}">edit</a>
{{ include('episode/_delete_form.html.twig') }}
{% endblock %}

View file

@ -1,13 +1,13 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Edit Podcast{% endblock %} {% block title %}Edit podcast {{ podcast.name }}{% endblock %}
{% block body %} {% block body %}
<h1>Edit Podcast</h1> <h1>Edit podcast {{ podcast.name }}</h1>
{{ include('podcast/_form.html.twig', {'button_label': 'Update'}) }} {{ include('podcast/_form.html.twig', {'button_label': 'Update'}) }}
<a href="{{ path('app_podcast_index') }}">back to list</a> <a href="{{ path('app_podcast_index') }}">Go back to list</a>
{{ include('podcast/_delete_form.html.twig') }} {{ include('podcast/_delete_form.html.twig') }}
{% endblock %} {% endblock %}

View file

@ -15,11 +15,11 @@
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
<th>Id</th> <th>ID</th>
<th>Name</th> <th>Name</th>
<th>Author</th> <th>Author</th>
<th>Logo</th> <th>Logo</th>
<th>actions</th> <th>Actions</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -44,5 +44,5 @@
</tbody> </tbody>
</table> </table>
<a href="{{ path('app_podcast_new') }}"><button>Create new</button></a> <a href="{{ path('app_podcast_new') }}"><button>Create new podcast</button></a>
{% endblock %} {% endblock %}

View file

@ -1,11 +1,11 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}New Podcast{% endblock %} {% block title %}New podcast{% endblock %}
{% block body %} {% block body %}
<h1>Create new Podcast</h1> <h1>Create a new podcast</h1>
{{ include('podcast/_form.html.twig') }} {{ include('podcast/_form.html.twig') }}
<a href="{{ path('app_podcast_index') }}">back to list</a> <a href="{{ path('app_podcast_index') }}">Go back to list</a>
{% endblock %} {% endblock %}

View file

@ -1,6 +1,6 @@
{% extends 'base.html.twig' %} {% extends 'base.html.twig' %}
{% block title %}Podcast{% endblock %} {% block title %}Podcast {{ podcast.name }}{% endblock %}
{% block stylesheets %} {% block stylesheets %}
{{ parent() }} {{ parent() }}
@ -15,7 +15,7 @@
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
<th>Id</th> <th>ID</th>
<td>{{ podcast.id }}</td> <td>{{ podcast.id }}</td>
</tr> </tr>
<tr> <tr>
@ -43,7 +43,7 @@
<td>{{ podcast.email }}</td> <td>{{ podcast.email }}</td>
</tr> </tr>
<tr> <tr>
<th>LogoFilename</th> <th>Logo file name</th>
<td>{{ podcast.logoFilename }}</td> <td>{{ podcast.logoFilename }}</td>
</tr> </tr>
<tr> <tr>
@ -54,12 +54,25 @@
<th>Owner</th> <th>Owner</th>
<td>{{ podcast.owner }}</td> <td>{{ podcast.owner }}</td>
</tr> </tr>
<tr>
<th>Episodes</th>
<td>
<a href="{{ path('app_podcast_episodes_index', {'id': podcast.id}) }}"
>Manage episodes</a>
<ol>
{% for episode in podcast.episodes %}
<li>{{ episode.title }}</li>
{% endfor %}
</ol>
</td>
</tr>
</tbody> </tbody>
</table> </table>
<a href="{{ path('app_podcast_index') }}">back to list</a> <a href="{{ path('app_podcast_index') }}">Go back to list</a>
<a href="{{ path('app_podcast_edit', {'id': podcast.id}) }}">edit</a> <a href="{{ path('app_podcast_edit', {'id': podcast.id}) }}"
><button>Edit</button></a>
{{ include('podcast/_delete_form.html.twig') }} {{ include('podcast/_delete_form.html.twig') }}
{% endblock %} {% endblock %}