Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions db/migrations/20260820120000_add_image_to_articles.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

use Phinx\Migration\AbstractMigration;

final class AddImageToArticles extends AbstractMigration
{
public function change(): void
{
$this
->table('afup_site_article')
->addColumn('image', 'string', [
'limit' => 255,
'null' => true,
'default' => null,
'after' => 'contenu',
])
->save()
;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
namespace AppBundle\Controller\Admin\Site\Article;

use AppBundle\AuditLog\Audit;
use AppBundle\Site\ArticleImageStorage;
use AppBundle\Site\Entity\Article;
use AppBundle\Site\Entity\Repository\ArticleRepository;
use AppBundle\Site\Form\ArticleType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\String\Slugger\AsciiSlugger;
Expand All @@ -18,6 +20,7 @@ final class AddArticleAction extends AbstractController
public function __construct(
private readonly ArticleRepository $articleRepository,
private readonly Audit $audit,
private readonly ArticleImageStorage $articleImageStorage,
) {}

public function __invoke(Request $request): Response
Expand All @@ -32,6 +35,11 @@ public function __invoke(Request $request): Response
$article->raccourci = (new AsciiSlugger())->slug($article->titre)->lower()->toString();
}

$uploadedImage = $form->get('image')->getData();
if ($uploadedImage instanceof UploadedFile) {
$article->image = $this->articleImageStorage->store($uploadedImage, $article);
}

$this->articleRepository->save($article);
$this->audit->log('Ajout de l\'article ' . $article->titre);
$this->addFlash('notice', 'L\'article ' . $article->titre . ' a été ajouté');
Expand All @@ -43,6 +51,7 @@ public function __invoke(Request $request): Response
'formTitle' => 'Ajouter un article',
'submitLabel' => 'Ajouter',
'article' => $article,
'imageUrl' => null,
]);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace AppBundle\Controller\Admin\Site\Article;

use AppBundle\AuditLog\Audit;
use AppBundle\Site\ArticleImageStorage;
use AppBundle\Site\Entity\Repository\ArticleRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
Expand All @@ -17,6 +18,7 @@ public function __construct(
private readonly ArticleRepository $articleRepository,
private readonly CsrfTokenManagerInterface $csrfTokenManager,
private readonly Audit $audit,
private readonly ArticleImageStorage $articleImageStorage,
) {}

public function __invoke(int $id, string $token): RedirectResponse
Expand All @@ -31,6 +33,7 @@ public function __invoke(int $id, string $token): RedirectResponse
throw $this->createNotFoundException();
}

$this->articleImageStorage->remove($article);
$this->articleRepository->delete($article);
$this->audit->log('Suppression de l\'article ' . $article->titre);
$this->addFlash('notice', 'L\'article ' . $article->titre . ' a été supprimé');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,11 @@
namespace AppBundle\Controller\Admin\Site\Article;

use AppBundle\AuditLog\Audit;
use AppBundle\Site\ArticleImageStorage;
use AppBundle\Site\Entity\Repository\ArticleRepository;
use AppBundle\Site\Form\ArticleType;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;

Expand All @@ -16,6 +18,7 @@ final class EditArticleAction extends AbstractController
public function __construct(
private readonly ArticleRepository $articleRepository,
private readonly Audit $audit,
private readonly ArticleImageStorage $articleImageStorage,
) {}

public function __invoke(int $id, Request $request): Response
Expand All @@ -25,9 +28,17 @@ public function __invoke(int $id, Request $request): Response
throw $this->createNotFoundException();
}

$form = $this->createForm(ArticleType::class, $article, ['is_new' => false]);
$form = $this->createForm(ArticleType::class, $article);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$uploadedImage = $form->get('image')->getData();
if ($uploadedImage instanceof UploadedFile) {
$article->image = $this->articleImageStorage->store($uploadedImage, $article);
} elseif ($article->image && $form->get('supprimerImage')->getData() === true) {
$this->articleImageStorage->remove($article);
$article->image = null;
}

$this->articleRepository->save($article);
$this->audit->log('Modification de l\'article ' . $article->titre);
$this->addFlash('notice', 'L\'article ' . $article->titre . ' a été modifié');
Expand All @@ -39,6 +50,7 @@ public function __invoke(int $id, Request $request): Response
'article' => $article,
'formTitle' => 'Modifier un article',
'submitLabel' => 'Modifier',
'imageUrl' => $this->articleImageStorage->getUrl($article),
]);
}
}
3 changes: 3 additions & 0 deletions sources/AppBundle/Controller/Website/News/DisplayAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use AppBundle\Event\Model\Event;
use AppBundle\Event\Model\Repository\EventRepository;
use AppBundle\Site\ArticleImageStorage;
use AppBundle\Site\Entity\Article;
use AppBundle\Site\Entity\Repository\ArticleRepository;
use AppBundle\Site\Enum\ArticleEtat;
Expand All @@ -21,6 +22,7 @@ public function __construct(
private readonly AuthorizationCheckerInterface $authorizationChecker,
private readonly EventRepository $eventRepository,
private readonly ArticleRepository $articleRepository,
private readonly ArticleImageStorage $articleImageStorage,
) {}

public function __invoke(string $code): Response
Expand All @@ -39,6 +41,7 @@ public function __invoke(string $code): Response
'previous' => $this->articleRepository->findPrevious($article),
'next' => $this->articleRepository->findNext($article),
'related_event' => $this->getRelatedEvent($article),
'image_url' => $this->articleImageStorage->getUrl($article),
]);
}

Expand Down
89 changes: 89 additions & 0 deletions sources/AppBundle/Site/ArticleImageStorage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace AppBundle\Site;

use AppBundle\Site\Entity\Article;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Filesystem\Exception\IOException;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\String\Slugger\AsciiSlugger;

final readonly class ArticleImageStorage
{
public const string PUBLIC_PATH = '/uploads/articles/';

private Filesystem $filesystem;

public function __construct(
#[Autowire('%kernel.project_dir%/htdocs/uploads/articles')]
private string $basePath,
) {
$this->filesystem = new Filesystem();
}

public function store(UploadedFile $file, Article $article): string
{
// On supprime d'abord l'image précédente si elle existe
$this->remove($article);

$this->createDirectory();

$fileName = $this->generateFileName($file, $article);
$file->move($this->basePath, $fileName);

return $fileName;
}

public function remove(Article $article): void
{
if ($article->image === null) {
return;
}

$this->filesystem->remove($this->basePath . '/' . $article->image);
}

public function getUrl(Article $article): ?string
{
if ($article->image === null) {
return null;
}

if (!$this->filesystem->exists($this->basePath . '/' . $article->image)) {
return null;
}

return self::PUBLIC_PATH . $article->image;
}

private function generateFileName(UploadedFile $file, Article $article): string
{
$slug = (new AsciiSlugger())->slug((string) $article->titre)
->lower()
->truncate(60)
->trim('-')
->toString();

if ($slug === '') {
$slug = 'article';
}

// Pour faire sauter le cache quand l'image est modifiée
$randomString = bin2hex(random_bytes(4));

return sprintf('%s-%s.%s', $slug, $randomString, $file->guessExtension() ?? 'jpg',);
}

private function createDirectory(): void
{
try {
$this->filesystem->mkdir($this->basePath, 0755);
} catch (IOException $exception) {
throw new FileException('Could not create directory for storage', 0, $exception);
}
}
}
3 changes: 3 additions & 0 deletions sources/AppBundle/Site/Entity/Article.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ class Article
#[ORM\Column(type: 'text', nullable: true)]
public ?string $contenu = null;

#[ORM\Column(length: 255, nullable: true)]
public ?string $image = null;

#[ORM\Column(nullable: true, enumType: ArticleTheme::class)]
public ?ArticleTheme $theme = null;

Expand Down
47 changes: 35 additions & 12 deletions sources/AppBundle/Site/Form/ArticleType.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,21 @@
namespace AppBundle\Site\Form;

use AppBundle\Event\Model\Repository\EventRepository;
use AppBundle\Site\Entity\Article;
use AppBundle\Site\Entity\Rubrique;
use AppBundle\Site\Enum\ArticleEtat;
use AppBundle\Site\Enum\ArticleTheme;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\DataTransformer\DateTimeToTimestampTransformer;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
use Symfony\Component\Form\Extension\Core\Type\EnumType;
use Symfony\Component\Form\Extension\Core\Type\FileType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints as Assert;

class ArticleType extends AbstractType
Expand All @@ -28,6 +30,11 @@ public function __construct(private readonly EventRepository $eventRepository) {

public function buildForm(FormBuilderInterface $builder, array $options): void
{
$article = $builder->getData();
if (!$article instanceof Article) {
throw new \LogicException();
}

$positions = [];
for ($i = self::POSITIONS_RUBRIQUES; $i >= -(self::POSITIONS_RUBRIQUES); $i--) {
$positions[$i] = $i;
Expand Down Expand Up @@ -79,6 +86,22 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
new Assert\Type('string'),
],
])
->add('image', FileType::class, [
'label' => 'Image de couverture',
'required' => false,
'mapped' => false,
'data_class' => null,
'help' => 'JPEG, PNG, WebP ou GIF — 2 Mo maximum',
'attr' => [
'accept' => 'image/jpeg,image/png,image/webp,image/gif',
],
'constraints' => [
new Assert\Image(
maxSize: '2M',
mimeTypes: ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
),
],
])
->add('rubrique', EntityType::class, [
'required' => true,
'label' => 'Rubrique',
Expand Down Expand Up @@ -128,12 +151,11 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
'constraints' => [
new Assert\Type("integer"),
],
])
;
]);
$builder->get('datePublication')->addModelTransformer(new DateTimeToTimestampTransformer());

$raccourciOption = [
'required' => $options['is_new'] === false,
'required' => $article->id !== null,
'label' => 'Raccourci',
'attr' => [
'maxlength' => 255,
Expand All @@ -146,19 +168,20 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
],
];

if ($options['is_new'] === true) {
if ($article->id === null) {
$raccourciOption['help'] = 'Si ce champ est vide, la valeur sera générée automatiquement';
} else {
$raccourciOption['constraints'][] = new Assert\NotBlank();
}

$builder->add('raccourci', TextType::class, $raccourciOption);
}

public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'is_new' => true,
]);
if ($article->image !== null) {
$builder->add('supprimerImage', CheckboxType::class, [
'label' => 'Supprimer l\'image de couverture',
'required' => false,
'mapped' => false,
]);
}
}
}
Loading
Loading