Work in progress…
This commit is contained in:
parent
9948627a7d
commit
852b5f3e17
2
.env
2
.env
|
@ -26,7 +26,7 @@ APP_SECRET=c798512b7c48614d5278e8e47bb556ce
|
||||||
# DATABASE_URL="sqlite:///%kernel.project_dir%/var/data.db"
|
# 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=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="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 ###
|
###< doctrine/doctrine-bundle ###
|
||||||
|
|
||||||
###> symfony/messenger ###
|
###> symfony/messenger ###
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
|
/public/uploads/
|
||||||
|
|
||||||
###> symfony/framework-bundle ###
|
###> symfony/framework-bundle ###
|
||||||
/.env.local
|
/.env.local
|
||||||
|
|
11
.phpactor.json
Normal file
11
.phpactor.json
Normal 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/**/*"
|
||||||
|
]
|
||||||
|
}
|
|
@ -6,11 +6,13 @@
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
"ext-ctype": "*",
|
"ext-ctype": "*",
|
||||||
|
"ext-dom": "*",
|
||||||
"ext-iconv": "*",
|
"ext-iconv": "*",
|
||||||
"doctrine/annotations": "^2.0",
|
"doctrine/annotations": "^2.0",
|
||||||
"doctrine/doctrine-bundle": "^2.9",
|
"doctrine/doctrine-bundle": "^2.9",
|
||||||
"doctrine/doctrine-migrations-bundle": "^3.2",
|
"doctrine/doctrine-migrations-bundle": "^3.2",
|
||||||
"doctrine/orm": "^2.15",
|
"doctrine/orm": "^2.15",
|
||||||
|
"easycorp/easyadmin-bundle": "^4",
|
||||||
"phpdocumentor/reflection-docblock": "^5.3",
|
"phpdocumentor/reflection-docblock": "^5.3",
|
||||||
"phpstan/phpdoc-parser": "^1.20",
|
"phpstan/phpdoc-parser": "^1.20",
|
||||||
"sensio/framework-extra-bundle": "^6.1",
|
"sensio/framework-extra-bundle": "^6.1",
|
||||||
|
@ -96,9 +98,10 @@
|
||||||
"symfony/browser-kit": "6.2.*",
|
"symfony/browser-kit": "6.2.*",
|
||||||
"symfony/css-selector": "6.2.*",
|
"symfony/css-selector": "6.2.*",
|
||||||
"symfony/debug-bundle": "6.2.*",
|
"symfony/debug-bundle": "6.2.*",
|
||||||
"symfony/maker-bundle": "^1.0",
|
"symfony/maker-bundle": "^1.48",
|
||||||
"symfony/phpunit-bridge": "^6.2",
|
"symfony/phpunit-bridge": "^6.2",
|
||||||
"symfony/stopwatch": "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
1055
composer.lock
generated
File diff suppressed because it is too large
Load diff
|
@ -12,4 +12,5 @@ return [
|
||||||
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
Symfony\Bundle\MonologBundle\MonologBundle::class => ['all' => true],
|
||||||
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
Symfony\Bundle\MakerBundle\MakerBundle::class => ['dev' => true],
|
||||||
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle::class => ['all' => true],
|
||||||
|
EasyCorp\Bundle\EasyAdminBundle\EasyAdminBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|
4
config/packages/uid.yaml
Normal file
4
config/packages/uid.yaml
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
framework:
|
||||||
|
uid:
|
||||||
|
default_uuid_version: 7
|
||||||
|
time_based_uuid_version: 7
|
54
migrations/Version20230506144146.php
Normal file
54
migrations/Version20230506144146.php
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
36
migrations/Version20230508171219.php
Normal file
36
migrations/Version20230508171219.php
Normal 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
16
psalm.xml
Normal 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>
|
0
public/uploads/files/.gitkeep
Normal file
0
public/uploads/files/.gitkeep
Normal file
0
public/uploads/images/.gitkeep
Normal file
0
public/uploads/images/.gitkeep
Normal file
12
src/Constants.php
Normal file
12
src/Constants.php
Normal 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/';
|
||||||
|
}
|
34
src/Controller/Admin/DashboardController.php
Normal file
34
src/Controller/Admin/DashboardController.php
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
33
src/Controller/Admin/EpisodeCrudController.php
Normal file
33
src/Controller/Admin/EpisodeCrudController.php
Normal 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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
80
src/Controller/Admin/Field/FileField.php
Normal file
80
src/Controller/Admin/Field/FileField.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
34
src/Controller/Admin/PodcastCrudController.php
Normal file
34
src/Controller/Admin/PodcastCrudController.php
Normal 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),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
51
src/Controller/FeedController.php
Normal file
51
src/Controller/FeedController.php
Normal 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
109
src/Entity/Episode.php
Normal 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
179
src/Entity/Podcast.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
66
src/Repository/EpisodeRepository.php
Normal file
66
src/Repository/EpisodeRepository.php
Normal 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()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
66
src/Repository/PodcastRepository.php
Normal file
66
src/Repository/PodcastRepository.php
Normal 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()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
81
src/Service/FeedService.php
Normal file
81
src/Service/FeedService.php
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
21
symfony.lock
21
symfony.lock
|
@ -35,6 +35,15 @@
|
||||||
"migrations/.gitignore"
|
"migrations/.gitignore"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"easycorp/easyadmin-bundle": {
|
||||||
|
"version": "4.6",
|
||||||
|
"recipe": {
|
||||||
|
"repo": "github.com/symfony/recipes",
|
||||||
|
"branch": "main",
|
||||||
|
"version": "3.0",
|
||||||
|
"ref": "b131e6cbfe1b898a508987851963fff485986285"
|
||||||
|
}
|
||||||
|
},
|
||||||
"phpunit/phpunit": {
|
"phpunit/phpunit": {
|
||||||
"version": "9.6",
|
"version": "9.6",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
@ -239,6 +248,18 @@
|
||||||
"templates/base.html.twig"
|
"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": {
|
"symfony/validator": {
|
||||||
"version": "6.2",
|
"version": "6.2",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|
6
templates/404.html.twig
Normal file
6
templates/404.html.twig
Normal 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
6
templates/500.html.twig
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
{% extends "base.html.twig" %}
|
||||||
|
|
||||||
|
{% block body %}
|
||||||
|
<h1>500</h1>
|
||||||
|
<p>An error occured, sorry! 😭</p>
|
||||||
|
{% endblock %}
|
11
templates/admin/field/file.html.twig
Normal file
11
templates/admin/field/file.html.twig
Normal 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 %}
|
|
@ -2,13 +2,16 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<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>">
|
<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 #}
|
{# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
|
||||||
{% block stylesheets %}
|
{% block stylesheets %}
|
||||||
|
<style>
|
||||||
|
body { margin: 1em auto; max-width: 800px; width: 95%; font: 18px/1.5 sans-serif; }
|
||||||
|
</style>
|
||||||
{{ encore_entry_link_tags('app') }}
|
{{ encore_entry_link_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
{{ encore_entry_script_tags('app') }}
|
{{ encore_entry_script_tags('app') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
10
templates/feed/index.html.twig
Normal file
10
templates/feed/index.html.twig
Normal 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 %}
|
11
templates/feed/podcast.html.twig
Normal file
11
templates/feed/podcast.html.twig
Normal 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 %}
|
Loading…
Reference in a new issue