Work in progress…

This commit is contained in:
dece 2023-05-11 17:03:52 +02:00
parent 9948627a7d
commit 852b5f3e17
30 changed files with 1998 additions and 6 deletions

2
.env
View file

@ -26,7 +26,7 @@ APP_SECRET=c798512b7c48614d5278e8e47bb556ce
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=8.0.32&charset=utf8mb4"
# DATABASE_URL="mysql://app:!ChangeMe!@127.0.0.1:3306/app?serverVersion=10.11.2-MariaDB&charset=utf8mb4"
DATABASE_URL="postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=15&charset=utf8"
DATABASE_URL="postgresql://lsbc:dev@127.0.0.1:5432/lsbc?serverVersion=15&charset=utf8"
###< doctrine/doctrine-bundle ###
###> symfony/messenger ###

1
.gitignore vendored
View file

@ -1,3 +1,4 @@
/public/uploads/
###> symfony/framework-bundle ###
/.env.local

11
.phpactor.json Normal file
View file

@ -0,0 +1,11 @@
{
"$schema": "/home/dece/Dev/DevTools/phpactor/phpactor.schema.json",
"language_server_psalm.enabled": true,
"symfony.enabled": true,
"indexer.exclude_patterns": [
"/vendor/**/Tests/**/*",
"/vendor/**/tests/**/*",
"/var/cache/**/*",
"/vendor/composer/**/*"
]
}

View file

@ -6,11 +6,13 @@
"require": {
"php": ">=8.1",
"ext-ctype": "*",
"ext-dom": "*",
"ext-iconv": "*",
"doctrine/annotations": "^2.0",
"doctrine/doctrine-bundle": "^2.9",
"doctrine/doctrine-migrations-bundle": "^3.2",
"doctrine/orm": "^2.15",
"easycorp/easyadmin-bundle": "^4",
"phpdocumentor/reflection-docblock": "^5.3",
"phpstan/phpdoc-parser": "^1.20",
"sensio/framework-extra-bundle": "^6.1",
@ -96,9 +98,10 @@
"symfony/browser-kit": "6.2.*",
"symfony/css-selector": "6.2.*",
"symfony/debug-bundle": "6.2.*",
"symfony/maker-bundle": "^1.0",
"symfony/maker-bundle": "^1.48",
"symfony/phpunit-bridge": "^6.2",
"symfony/stopwatch": "6.2.*",
"symfony/web-profiler-bundle": "6.2.*"
"symfony/web-profiler-bundle": "6.2.*",
"vimeo/psalm": "^5.11"
}
}

1055
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -12,4 +12,5 @@ return [
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
];

4
config/packages/uid.yaml Normal file
View file

@ -0,0 +1,4 @@
framework:
uid:
default_uuid_version: 7
time_based_uuid_version: 7

View file

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230506144146 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SEQUENCE episode_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE SEQUENCE podcast_id_seq INCREMENT BY 1 MINVALUE 1 START 1');
$this->addSql('CREATE TABLE episode (id INT NOT NULL, podcast_id INT NOT NULL, title VARCHAR(255) NOT NULL, description TEXT NOT NULL, audio_filename VARCHAR(255) DEFAULT NULL, publication_date TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_DDAA1CDA786136AB ON episode (podcast_id)');
$this->addSql('CREATE TABLE podcast (id INT NOT NULL, name VARCHAR(255) NOT NULL, website VARCHAR(255) NOT NULL, description TEXT NOT NULL, author VARCHAR(255) NOT NULL, email VARCHAR(255) NOT NULL, logo_filename VARCHAR(255) DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE TABLE messenger_messages (id BIGSERIAL NOT NULL, body TEXT NOT NULL, headers TEXT NOT NULL, queue_name VARCHAR(190) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, available_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, delivered_at TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, PRIMARY KEY(id))');
$this->addSql('CREATE INDEX IDX_75EA56E0FB7336F0 ON messenger_messages (queue_name)');
$this->addSql('CREATE INDEX IDX_75EA56E0E3BD61CE ON messenger_messages (available_at)');
$this->addSql('CREATE INDEX IDX_75EA56E016BA31DB ON messenger_messages (delivered_at)');
$this->addSql('CREATE OR REPLACE FUNCTION notify_messenger_messages() RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify(\'messenger_messages\', NEW.queue_name::text);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;');
$this->addSql('DROP TRIGGER IF EXISTS notify_trigger ON messenger_messages;');
$this->addSql('CREATE TRIGGER notify_trigger AFTER INSERT OR UPDATE ON messenger_messages FOR EACH ROW EXECUTE PROCEDURE notify_messenger_messages();');
$this->addSql('ALTER TABLE episode ADD CONSTRAINT FK_DDAA1CDA786136AB FOREIGN KEY (podcast_id) REFERENCES podcast (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP SEQUENCE episode_id_seq CASCADE');
$this->addSql('DROP SEQUENCE podcast_id_seq CASCADE');
$this->addSql('ALTER TABLE episode DROP CONSTRAINT FK_DDAA1CDA786136AB');
$this->addSql('DROP TABLE episode');
$this->addSql('DROP TABLE podcast');
$this->addSql('DROP TABLE messenger_messages');
}
}

View file

@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20230508171219 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE podcast ADD slug VARCHAR(255)');
$this->addSql('UPDATE podcast SET slug = LOWER(CAST(id AS VARCHAR))');
$this->addSql('ALTER TABLE podcast ALTER COLUMN slug SET NOT NULL');
$this->addSql('CREATE UNIQUE INDEX UNIQ_D7E805BD989D9B62 ON podcast (slug)');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE SCHEMA public');
$this->addSql('DROP INDEX UNIQ_D7E805BD989D9B62');
$this->addSql('ALTER TABLE podcast DROP slug');
}
}

16
psalm.xml Normal file
View file

@ -0,0 +1,16 @@
<?xml version="1.0"?>
<psalm
errorLevel="3"
resolveFromConfigFile="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
findUnusedBaselineEntry="true"
>
<projectFiles>
<directory name="src" />
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
</psalm>

View file

View file

12
src/Constants.php Normal file
View file

@ -0,0 +1,12 @@
<?php
namespace App;
class Constants
{
public const BASE_PUBLIC_DIR = 'public';
protected const UPLOADS_BASE_PATH = '/uploads';
public const IMAGES_BASE_PATH = Constants::UPLOADS_BASE_PATH.'/images/';
public const FILES_BASE_PATH = Constants::UPLOADS_BASE_PATH.'/files/';
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Controller\Admin;
use App\Controller\Admin\PodcastCrudController;
use App\Entity\Episode;
use App\Entity\Podcast;
use EasyCorp\Bundle\EasyAdminBundle\Config\Dashboard;
use EasyCorp\Bundle\EasyAdminBundle\Config\MenuItem;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractDashboardController;
use EasyCorp\Bundle\EasyAdminBundle\Router\AdminUrlGenerator;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class DashboardController extends AbstractDashboardController
{
#[Route('/admin', name: 'admin')]
public function index(): Response
{
$adminUrlGenerator = $this->container->get(AdminUrlGenerator::class);
return $this->redirect($adminUrlGenerator->setController(PodcastCrudController::class)->generateUrl());
}
public function configureDashboard(): Dashboard
{
return Dashboard::new()->setTitle('LSBC');
}
public function configureMenuItems(): iterable
{
yield MenuItem::linkToCrud('Podcasts', 'fas fa-list', Podcast::class);
yield MenuItem::linkToCrud('Episodes', 'fas fa-list', Episode::class);
}
}

View file

@ -0,0 +1,33 @@
<?php
namespace App\Controller\Admin;
use App\Constants;
use App\Entity\Episode;
use App\Controller\Admin\Field\FileField;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\AssociationField;
use EasyCorp\Bundle\EasyAdminBundle\Field\DateTimeField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
class EpisodeCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Episode::class;
}
public function configureFields(string $pageName): iterable
{
return [
TextField::new('title'),
TextEditorField::new('description'),
AssociationField::new('podcast'),
DateTimeField::new('publicationDate'),
FileField::new('audioFilename')
->setUploadDir(Constants::BASE_PUBLIC_DIR . Constants::FILES_BASE_PATH)
->setBasePath(Constants::FILES_BASE_PATH),
];
}
}

View file

@ -0,0 +1,80 @@
<?php
namespace App\Controller\Admin\Field;
use EasyCorp\Bundle\EasyAdminBundle\Config\Asset;
use EasyCorp\Bundle\EasyAdminBundle\Config\Option\TextAlign;
use EasyCorp\Bundle\EasyAdminBundle\Contracts\Field\FieldInterface;
use EasyCorp\Bundle\EasyAdminBundle\Field\FieldTrait;
use EasyCorp\Bundle\EasyAdminBundle\Form\Type\FileUploadType;
use Symfony\Contracts\Translation\TranslatableInterface;
/**
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
*/
final class FileField implements FieldInterface
{
use FieldTrait;
public const OPTION_BASE_PATH = 'basePath';
public const OPTION_UPLOAD_DIR = 'uploadDir';
public const OPTION_UPLOADED_FILE_NAME_PATTERN = 'uploadedFileNamePattern';
/**
* @param TranslatableInterface|string|false|null $label
*/
public static function new(string $propertyName, $label = null): self
{
return (new self())
->setProperty($propertyName)
->setLabel($label)
->setTemplatePath('admin/field/file.html.twig')
->setFormType(FileUploadType::class)
->addCssClass('field-file')
->addJsFiles(Asset::fromEasyAdminAssetPackage('field-file-upload.js'))
->setTextAlign(TextAlign::CENTER)
->setCustomOption(self::OPTION_BASE_PATH, null)
->setCustomOption(self::OPTION_UPLOAD_DIR, null)
->setCustomOption(self::OPTION_UPLOADED_FILE_NAME_PATTERN, '[name].[extension]');
}
public function setBasePath(string $path): self
{
$this->setCustomOption(self::OPTION_BASE_PATH, $path);
return $this;
}
/**
* Relative to project's root directory (e.g. use 'public/uploads/' for `<your-project-dir>/public/uploads/`)
* Default upload dir: `<your-project-dir>/public/uploads/images/`.
*/
public function setUploadDir(string $uploadDirPath): self
{
$this->setCustomOption(self::OPTION_UPLOAD_DIR, $uploadDirPath);
return $this;
}
/**
* @param string|\Closure $patternOrCallable
*
* If it's a string, uploaded files will be renamed according to the given pattern.
* The pattern can include the following special values:
* [day] [month] [year] [timestamp]
* [name] [slug] [extension] [contenthash]
* [randomhash] [uuid] [ulid]
* (e.g. [year]/[month]/[day]/[slug]-[contenthash].[extension])
*
* If it's a callable, you will be passed the Symfony's UploadedFile instance and you must
* return a string with the new filename.
* (e.g. fn(UploadedFile $file) => sprintf('upload_%d_%s.%s', random_int(1, 999), $file->getFilename(), $file->guessExtension()))
*/
public function setUploadedFileNamePattern($patternOrCallable): self
{
$this->setCustomOption(self::OPTION_UPLOADED_FILE_NAME_PATTERN, $patternOrCallable);
return $this;
}
}

View file

@ -0,0 +1,34 @@
<?php
namespace App\Controller\Admin;
use App\Constants;
use App\Entity\Podcast;
use EasyCorp\Bundle\EasyAdminBundle\Controller\AbstractCrudController;
use EasyCorp\Bundle\EasyAdminBundle\Field\EmailField;
use EasyCorp\Bundle\EasyAdminBundle\Field\ImageField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextField;
use EasyCorp\Bundle\EasyAdminBundle\Field\UrlField;
use EasyCorp\Bundle\EasyAdminBundle\Field\TextEditorField;
class PodcastCrudController extends AbstractCrudController
{
public static function getEntityFqcn(): string
{
return Podcast::class;
}
public function configureFields(string $pageName): iterable
{
return [
TextField::new('name'),
UrlField::new('website'),
TextEditorField::new('description'),
TextField::new('author'),
EmailField::new('email'),
ImageField::new('logoFilename')
->setUploadDir(Constants::BASE_PUBLIC_DIR . Constants::IMAGES_BASE_PATH)
->setBasePath(Constants::IMAGES_BASE_PATH),
];
}
}

View file

@ -0,0 +1,51 @@
<?php
namespace App\Controller;
use App\Repository\PodcastRepository;
use App\Service\FeedService;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class FeedController extends AbstractController
{
#[Route('/', name: 'app_index')]
public function index(
PodcastRepository $podcastRepository,
): Response
{
$podcasts = $podcastRepository->findAll();
return $this->render('feed/index.html.twig', ['podcasts' => $podcasts]);
}
#[Route('/podcasts/{slug}', name: 'app_podcast')]
public function podcast(
string $slug,
PodcastRepository $podcastRepository,
): Response
{
$podcast = $podcastRepository->findOneBy(['slug' => $slug]);
return $this->render('feed/podcast.html.twig', ['podcast' => $podcast]);
}
#[Route('/podcasts/{slug}/feed', name: 'app_podcast_feed')]
public function podcastFeed(
PodcastRepository $podcastRepository,
FeedService $feedService
): Response
{
$podcast = $podcastRepository->find(1);
if ($podcast === null)
return $this->render('404.html.twig');
$xml = $feedService->generate($podcast);
if ($xml === false)
return $this->render('500.html.twig');
$response = new Response();
$response->headers->set('Content-Type', 'application/rss+xml');
$response->setContent($xml);
return $response;
}
}

109
src/Entity/Episode.php Normal file
View file

@ -0,0 +1,109 @@
<?php
namespace App\Entity;
use App\Constants;
use App\Entity\Podcast;
use App\Repository\EpisodeRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: EpisodeRepository::class)]
##[Vich\Uploadable]
class Episode
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
#[ORM\Column(length: 255)]
private string $title;
#[ORM\Column(type: Types::TEXT)]
private string $description;
#[ORM\Column(length: 255, nullable: true)]
private ?string $audioFilename;
#[ORM\ManyToOne(inversedBy: 'episodes')]
#[ORM\JoinColumn(nullable: false)]
private Podcast $podcast;
#[ORM\Column(type: Types::DATETIME_MUTABLE)]
private \DateTimeInterface $publicationDate;
public function getId(): int
{
return $this->id;
}
public function getTitle(): string
{
return $this->title;
}
public function setTitle(string $title): self
{
$this->title = $title;
return $this;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function getAudioFilename(): ?string
{
return $this->audioFilename;
}
public function setAudioFilename(?string $audioFilename): self
{
$this->audioFilename = $audioFilename;
return $this;
}
public function getPodcast(): Podcast
{
return $this->podcast;
}
public function setPodcast(Podcast $podcast): self
{
$this->podcast = $podcast;
return $this;
}
public function getPublicationDate(): \DateTimeInterface
{
return $this->publicationDate;
}
public function setPublicationDate(\DateTimeInterface $publicationDate): self
{
$this->publicationDate = $publicationDate;
return $this;
}
public function getAudioUrl(): ?string
{
if ($this->audioFilename === null) {
return null;
}
return Constants::FILES_BASE_PATH . $this->audioFilename;
}
}

179
src/Entity/Podcast.php Normal file
View file

@ -0,0 +1,179 @@
<?php
namespace App\Entity;
use App\Repository\PodcastRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\String\Slugger\SluggerInterface;
#[ORM\Entity(repositoryClass: PodcastRepository::class)]
#[ORM\HasLifecycleCallbacks]
class Podcast
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private int $id;
#[ORM\Column(length: 255)]
private string $name;
#[ORM\Column(length: 255, unique: true)]
private string $slug;
#[ORM\OneToMany(mappedBy: 'podcast', targetEntity: Episode::class)]
private Collection $episodes;
#[ORM\Column(length: 255)]
private string $website;
#[ORM\Column(type: Types::TEXT)]
private string $description;
#[ORM\Column(length: 255)]
private string $author;
#[ORM\Column(length: 255)]
private string $email;
#[ORM\Column(length: 255, nullable: true)]
private string $logoFilename;
public function __construct()
{
$this->episodes = new ArrayCollection();
}
public function __toString(): string
{
return $this->name;
}
public function getId(): int
{
return $this->id;
}
public function getName(): string
{
return $this->name;
}
public function setName(string $name): self
{
$this->name = $name;
return $this;
}
public function getSlug(): string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
#[ORM\PrePersist]
public function generateSlug(SluggerInterface $slugger): void
{
$this->slug = $this->id . '-' . $slugger->slug($this->name)->lower();
}
/**
* @return Collection<int, Episode>
*/
public function getEpisodes(): Collection
{
return $this->episodes;
}
public function addEpisode(Episode $episode): self
{
if (!$this->episodes->contains($episode)) {
$this->episodes->add($episode);
$episode->setPodcast($this);
}
return $this;
}
public function removeEpisode(Episode $episode): self
{
if ($this->episodes->removeElement($episode)) {
// set the owning side to null (unless already changed)
if ($episode->getPodcast() === $this) {
$episode->setPodcast(null);
}
}
return $this;
}
public function getWebsite(): string
{
return $this->website;
}
public function setWebsite(string $website): self
{
$this->website = $website;
return $this;
}
public function getDescription(): string
{
return $this->description;
}
public function setDescription(string $description): self
{
$this->description = $description;
return $this;
}
public function getAuthor(): string
{
return $this->author;
}
public function setAuthor(string $authorName): self
{
$this->author = $authorName;
return $this;
}
public function getEmail(): string
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
public function getLogoFilename(): string
{
return $this->logoFilename;
}
public function setLogoFilename(string $logoFilename): self
{
$this->logoFilename = $logoFilename;
return $this;
}
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\Episode;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Episode>
*
* @method Episode|null find($id, $lockMode = null, $lockVersion = null)
* @method Episode|null findOneBy(array $criteria, array $orderBy = null)
* @method Episode[] findAll()
* @method Episode[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class EpisodeRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Episode::class);
}
public function save(Episode $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Episode $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return Episode[] Returns an array of Episode objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('e.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Episode
// {
// return $this->createQueryBuilder('e')
// ->andWhere('e.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -0,0 +1,66 @@
<?php
namespace App\Repository;
use App\Entity\Podcast;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<Podcast>
*
* @method Podcast|null find($id, $lockMode = null, $lockVersion = null)
* @method Podcast|null findOneBy(array $criteria, array $orderBy = null)
* @method Podcast[] findAll()
* @method Podcast[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class PodcastRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Podcast::class);
}
public function save(Podcast $entity, bool $flush = false): void
{
$this->getEntityManager()->persist($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
public function remove(Podcast $entity, bool $flush = false): void
{
$this->getEntityManager()->remove($entity);
if ($flush) {
$this->getEntityManager()->flush();
}
}
// /**
// * @return Podcast[] Returns an array of Podcast objects
// */
// public function findByExampleField($value): array
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->orderBy('p.id', 'ASC')
// ->setMaxResults(10)
// ->getQuery()
// ->getResult()
// ;
// }
// public function findOneBySomeField($value): ?Podcast
// {
// return $this->createQueryBuilder('p')
// ->andWhere('p.exampleField = :val')
// ->setParameter('val', $value)
// ->getQuery()
// ->getOneOrNullResult()
// ;
// }
}

View file

@ -0,0 +1,81 @@
<?php
namespace App\Service;
use App\Constants;
use App\Entity\Podcast;
use DOMDocument;
use DOMElement;
use DOMText;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class FeedService
{
protected EntityManagerInterface $entityManager;
protected ParameterBagInterface $parameterBag;
protected string $baseFeedUrl;
public function __construct(
EntityManagerInterface $entityManager,
ParameterBagInterface $parameterBag,
UrlGeneratorInterface $urlGenerator,
)
{
$this->entityManager = $entityManager;
$this->parameterBag = $parameterBag;
$this->baseFeedUrl = $urlGenerator->generate(
'app_index',
[],
UrlGeneratorInterface::ABSOLUTE_URL
) . Constants::FILES_BASE_PATH;
}
public const DOCUMENT_VERSION = '2.0';
public function generate(Podcast $podcast): string|false
{
$document = new DOMDocument();
$rssElement = new DOMElement('rss');
$document->appendChild($rssElement);
$rssElement->setAttribute('version', $this::DOCUMENT_VERSION);
$channelElement = new DOMElement('channel');
$rssElement->appendChild($channelElement);
$titleElement = new DOMElement('title');
$channelElement->appendChild($titleElement);
$titleElement->appendChild(new DOMText($podcast->getName()));
$channelElement->appendChild(new DOMElement('description', $podcast->getDescription()));
$channelElement->appendChild(new DOMElement('link', $podcast->getWebsite()));
$episodes = $this->entityManager->createQuery(
'SELECT e FROM App\Entity\Episode e'
.' WHERE e.podcast = :podcastId'
.' AND e.audioFilename IS NOT NULL'
)->setParameter('podcastId', $podcast->getId())->getResult();
foreach ($episodes as $episode) {
$audioUrl = $this->baseFeedUrl . $episode->getAudioFilename(); // Also used as ID.
$itemElement = new DOMElement('item');
$channelElement->appendChild($itemElement);
$titleElement = new DOMElement('title');
$itemElement->appendChild($titleElement);
$titleElement->appendChild(new DOMText($episode->getTitle()));
$itemElement->appendChild(new DOMElement('description', $episode->getDescription()));
$itemElement->appendChild(new DOMElement('link', $audioUrl));
$itemElement->appendChild(new DOMElement('guid', $audioUrl));
$enclosureElement = new DOMElement('enclosure');
$itemElement->appendChild($enclosureElement);
$enclosureElement->setAttribute('url', $audioUrl);
$filepath =
$this->parameterBag->get('kernel.project_dir')
. '/'
. Constants::BASE_PUBLIC_DIR
. Constants::FILES_BASE_PATH
. $episode->getAudioFilename();
$enclosureElement->setAttribute('type', mime_content_type($filepath));
$enclosureElement->setAttribute('length', (string) filesize($filepath));
}
return $document->saveXML();
}
}

View file

@ -35,6 +35,15 @@
"migrations/.gitignore"
]
},
"easycorp/easyadmin-bundle": {
"version": "4.6",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "3.0",
"ref": "b131e6cbfe1b898a508987851963fff485986285"
}
},
"phpunit/phpunit": {
"version": "9.6",
"recipe": {
@ -239,6 +248,18 @@
"templates/base.html.twig"
]
},
"symfony/uid": {
"version": "6.2",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "6.2",
"ref": "d294ad4add3e15d7eb1bae0221588ca89b38e558"
},
"files": [
"config/packages/uid.yaml"
]
},
"symfony/validator": {
"version": "6.2",
"recipe": {

6
templates/404.html.twig Normal file
View file

@ -0,0 +1,6 @@
{% extends "base.html.twig" %}
{% block body %}
<h1>404</h1>
<p>Page not found. 🤔🔎</p>
{% endblock %}

6
templates/500.html.twig Normal file
View file

@ -0,0 +1,6 @@
{% extends "base.html.twig" %}
{% block body %}
<h1>500</h1>
<p>An error occured, sorry! 😭</p>
{% endblock %}

View file

@ -0,0 +1,11 @@
{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
{% set files = field.formattedValue %}
{% if files is not iterable %}
{% set files = [files] %}
{% endif %}
{% for file in files %}
<span>{{ file }}</span>
{% endfor %}

View file

@ -2,13 +2,16 @@
<html>
<head>
<meta charset="UTF-8">
<title>{% block title %}Welcome!{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}LSBC{% endblock %}</title>
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 128 128%22><text y=%221.2em%22 font-size=%2296%22>⚫️</text></svg>">
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
{% block stylesheets %}
<style>
body { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
</style>
{{ encore_entry_link_tags('app') }}
{% endblock %}
{% block javascripts %}
{{ encore_entry_script_tags('app') }}
{% endblock %}

View file

@ -0,0 +1,10 @@
{% extends 'base.html.twig' %}
{% block body %}
<p>Podcasts available on this LSBC instance:</p>
<ul>
{% for podcast in podcasts %}
<li><a href="{{ path('app_podcast', {slug: podcast.slug}) }}">{{ podcast.name }}</a></li>
{% endfor %}
</ul>
{% endblock %}

View file

@ -0,0 +1,11 @@
{% extends 'base.html.twig' %}
{% block body %}
<h1>{{ podcast.name }}</h1>
<p>
By {{ podcast.author }}.<br>
Contact: <code>{{ podcast.email }}</code><br>
{% set feed = url('app_podcast_feed', {slug: podcast.slug}) %}
Feed URL: <a href="{{ feed }}">{{ feed }}</a><br>
</p>
{% endblock %}