Browse Source

Ajout Crud Articles et commande création user

Sangfroid 5 months ago
parent
commit
b2e3f2e600

+ 1 - 0
composer.json

@@ -98,6 +98,7 @@
         }
     },
     "require-dev": {
+        "fakerphp/faker": "^1.23",
         "phpunit/phpunit": "^9.5",
         "symfony/browser-kit": "7.1.*",
         "symfony/css-selector": "7.1.*",

+ 64 - 1
composer.lock

@@ -4,7 +4,7 @@
         "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
         "This file is @generated automatically"
     ],
-    "content-hash": "0389f88db16a49c53c67166bc3068946",
+    "content-hash": "467141bbdc6343d94dba80817b4ecfdc",
     "packages": [
         {
             "name": "composer/semver",
@@ -7646,6 +7646,69 @@
         }
     ],
     "packages-dev": [
+        {
+            "name": "fakerphp/faker",
+            "version": "v1.23.1",
+            "source": {
+                "type": "git",
+                "url": "https://github.com/FakerPHP/Faker.git",
+                "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b"
+            },
+            "dist": {
+                "type": "zip",
+                "url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b",
+                "reference": "bfb4fe148adbf78eff521199619b93a52ae3554b",
+                "shasum": ""
+            },
+            "require": {
+                "php": "^7.4 || ^8.0",
+                "psr/container": "^1.0 || ^2.0",
+                "symfony/deprecation-contracts": "^2.2 || ^3.0"
+            },
+            "conflict": {
+                "fzaninotto/faker": "*"
+            },
+            "require-dev": {
+                "bamarni/composer-bin-plugin": "^1.4.1",
+                "doctrine/persistence": "^1.3 || ^2.0",
+                "ext-intl": "*",
+                "phpunit/phpunit": "^9.5.26",
+                "symfony/phpunit-bridge": "^5.4.16"
+            },
+            "suggest": {
+                "doctrine/orm": "Required to use Faker\\ORM\\Doctrine",
+                "ext-curl": "Required by Faker\\Provider\\Image to download images.",
+                "ext-dom": "Required by Faker\\Provider\\HtmlLorem for generating random HTML.",
+                "ext-iconv": "Required by Faker\\Provider\\ru_RU\\Text::realText() for generating real Russian text.",
+                "ext-mbstring": "Required for multibyte Unicode string functionality."
+            },
+            "type": "library",
+            "autoload": {
+                "psr-4": {
+                    "Faker\\": "src/Faker/"
+                }
+            },
+            "notification-url": "https://packagist.org/downloads/",
+            "license": [
+                "MIT"
+            ],
+            "authors": [
+                {
+                    "name": "François Zaninotto"
+                }
+            ],
+            "description": "Faker is a PHP library that generates fake data for you.",
+            "keywords": [
+                "data",
+                "faker",
+                "fixtures"
+            ],
+            "support": {
+                "issues": "https://github.com/FakerPHP/Faker/issues",
+                "source": "https://github.com/FakerPHP/Faker/tree/v1.23.1"
+            },
+            "time": "2024-01-02T13:46:09+00:00"
+        },
         {
             "name": "masterminds/html5",
             "version": "2.9.0",

+ 3 - 0
config/packages/security.yaml

@@ -37,6 +37,9 @@ security:
         # - { path: ^/admin, roles: ROLE_ADMIN }
         # - { path: ^/profile, roles: ROLE_USER }
 
+    role_hierarchy:
+        ROLE_ADMIN: ROLE_AUTHOR
+
 when@test:
     security:
         password_hashers:

+ 61 - 0
src/Command/BlogUserCreateCommand.php

@@ -0,0 +1,61 @@
+<?php
+
+namespace App\Command;
+
+use App\Entity\User;
+use App\Repository\UserRepository;
+use Symfony\Component\Console\Attribute\AsCommand;
+use Symfony\Component\Console\Command\Command;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+use Symfony\Component\Console\Style\SymfonyStyle;
+use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
+
+#[AsCommand(
+    name: 'blog:user:create',
+    description: 'Créer le premier utilisateur',
+)]
+class BlogUserCreateCommand extends Command
+{
+    public function __construct(
+        protected readonly UserRepository $userRepository,
+        protected UserPasswordHasherInterface $passwordHasher
+    )
+    {
+        parent::__construct();
+    }
+
+    protected function configure(): void
+    {
+        
+    }
+
+    protected function execute(InputInterface $input, OutputInterface $output): int
+    {
+        $io = new SymfonyStyle($input, $output);
+        
+        $user = new User();
+
+        $user->setUsername($io->ask('Username'));
+        $user->setEmail($io->ask('Email'));
+        do {
+            $password = $io->askHidden('Mot de passe');
+            $confirm = $io->askHidden('Confirmez le mot de passe');
+        } while ($password !== $confirm);
+        
+        $user->setPassword($this->passwordHasher->hashPassword($user, $password));
+
+        if ($io->confirm("Est-ce un administrateur", true)) {
+            $user->setRoles(['ROLE_ADMIN']);
+        } else 
+        {
+            $user->setRoles(['ROLE_AUTHOR']);
+        }
+
+        $this->userRepository->add($user);
+
+        $io->success('Utilisateur créé');
+
+        return Command::SUCCESS;
+    }
+}

+ 83 - 0
src/Controller/ArticleController.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Controller;
+
+use App\Entity\Article;
+use App\Form\ArticleType;
+use App\Repository\ArticleRepository;
+use Doctrine\ORM\EntityManagerInterface;
+use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
+use Symfony\Component\HttpFoundation\Request;
+use Symfony\Component\HttpFoundation\Response;
+use Symfony\Component\Routing\Attribute\Route;
+use Symfony\Component\Security\Http\Attribute\IsGranted;
+
+#[Route('/article')]
+#[IsGranted('ROLE_AUTHOR')]
+final class ArticleController extends AbstractController
+{
+    #[Route(name: 'app_article_index', methods: ['GET'])]
+    public function index(ArticleRepository $articleRepository): Response
+    {
+        return $this->render('article/index.html.twig', [
+            'articles' => $articleRepository->findAll(),
+        ]);
+    }
+
+    #[Route('/new', name: 'app_article_new', methods: ['GET', 'POST'])]
+    public function new(Request $request, EntityManagerInterface $entityManager): Response
+    {
+        $article = new Article($this->getUser());
+        $form = $this->createForm(ArticleType::class, $article);
+        $form->handleRequest($request);
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            $entityManager->persist($article);
+            $entityManager->flush();
+
+            return $this->redirectToRoute('app_article_index', [], Response::HTTP_SEE_OTHER);
+        }
+
+        return $this->render('article/new.html.twig', [
+            'article' => $article,
+            'form' => $form,
+        ]);
+    }
+
+    #[Route('/{id}', name: 'app_article_show', methods: ['GET'])]
+    public function show(Article $article): Response
+    {
+        return $this->render('article/show.html.twig', [
+            'article' => $article,
+        ]);
+    }
+
+    #[Route('/{id}/edit', name: 'app_article_edit', methods: ['GET', 'POST'])]
+    public function edit(Request $request, Article $article, EntityManagerInterface $entityManager): Response
+    {
+        $form = $this->createForm(ArticleType::class, $article);
+        $form->handleRequest($request);
+
+        if ($form->isSubmitted() && $form->isValid()) {
+            $entityManager->flush();
+
+            return $this->redirectToRoute('app_article_index', [], Response::HTTP_SEE_OTHER);
+        }
+
+        return $this->render('article/edit.html.twig', [
+            'article' => $article,
+            'form' => $form,
+        ]);
+    }
+
+    #[Route('/{id}', name: 'app_article_delete', methods: ['POST'])]
+    public function delete(Request $request, Article $article, EntityManagerInterface $entityManager): Response
+    {
+        if ($this->isCsrfTokenValid('delete'.$article->getId(), $request->getPayload()->getString('_token'))) {
+            $entityManager->remove($article);
+            $entityManager->flush();
+        }
+
+        return $this->redirectToRoute('app_article_index', [], Response::HTTP_SEE_OTHER);
+    }
+}

+ 3 - 3
src/Controller/IndexController.php

@@ -2,6 +2,7 @@
 
 namespace App\Controller;
 
+use App\Repository\ArticleRepository;
 use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
 use Symfony\Component\HttpFoundation\Response;
 use Symfony\Component\Routing\Attribute\Route;
@@ -9,10 +10,9 @@ use Symfony\Component\Routing\Attribute\Route;
 class IndexController extends AbstractController
 {
     #[Route('/', name: 'app_index')]
-    public function index(): Response
+    public function index(ArticleRepository $articleRepository): Response
     {
-        
-        $articles = [];
+        $articles = $articleRepository->findAll();
 
         return $this->render('index/index.html.twig', [
             'articles'  => $articles

+ 5 - 0
src/Entity/User.php

@@ -47,6 +47,11 @@ class User implements UserInterface, PasswordAuthenticatedUserInterface
         $this->articles = new ArrayCollection();
     }
 
+    public function __toString(): string
+    {
+        return $this->getUserIdentifier();
+    }
+
     public function getId(): ?int
     {
         return $this->id;

+ 8 - 1
src/Form/ArticleType.php

@@ -3,6 +3,9 @@
 namespace App\Form;
 
 use App\Entity\Article;
+use App\Entity\User;
+use FOS\CKEditorBundle\Form\Type\CKEditorType;
+use Symfony\Bridge\Doctrine\Form\Type\EntityType;
 use Symfony\Component\Form\AbstractType;
 use Symfony\Component\Form\FormBuilderInterface;
 use Symfony\Component\OptionsResolver\OptionsResolver;
@@ -13,10 +16,14 @@ class ArticleType extends AbstractType
     {
         $builder
             ->add('title')
-            ->add('content')
+            ->add('content', CKEditorType::class)
             ->add('publicationDate', null, [
                 'widget' => 'single_text',
             ])
+            ->add('author', EntityType::class, [
+                'class' => User::class,
+                'choice_label' => 'userIdentifier',
+            ])
         ;
     }
 

+ 5 - 0
src/Repository/UserRepository.php

@@ -33,6 +33,11 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader
         $this->getEntityManager()->flush();
     }
 
+    public function add(User $user) {
+        $this->getEntityManager()->persist($user);
+        $this->getEntityManager()->flush();
+    }
+
     //    /**
     //     * @return User[] Returns an array of User objects
     //     */

+ 4 - 0
templates/article/_delete_form.html.twig

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

+ 4 - 0
templates/article/_form.html.twig

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

+ 13 - 0
templates/article/edit.html.twig

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

+ 39 - 0
templates/article/index.html.twig

@@ -0,0 +1,39 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Article index{% endblock %}
+
+{% block body %}
+    <h1>Article index</h1>
+
+    <table class="table">
+        <thead>
+            <tr>
+                <th>Id</th>
+                <th>Title</th>
+                <th>PublicationDate</th>
+                <th>Content</th>
+                <th>actions</th>
+            </tr>
+        </thead>
+        <tbody>
+        {% for article in articles %}
+            <tr>
+                <td>{{ article.id }}</td>
+                <td>{{ article.title }}</td>
+                <td>{{ article.publicationDate ? article.publicationDate|date('Y-m-d H:i:s') : '' }}</td>
+                <td>{{ article.content }}</td>
+                <td>
+                    <a href="{{ path('app_article_show', {'id': article.id}) }}">show</a>
+                    <a href="{{ path('app_article_edit', {'id': article.id}) }}">edit</a>
+                </td>
+            </tr>
+        {% else %}
+            <tr>
+                <td colspan="5">no records found</td>
+            </tr>
+        {% endfor %}
+        </tbody>
+    </table>
+
+    <a href="{{ path('app_article_new') }}">Create new</a>
+{% endblock %}

+ 11 - 0
templates/article/new.html.twig

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

+ 34 - 0
templates/article/show.html.twig

@@ -0,0 +1,34 @@
+{% extends 'base.html.twig' %}
+
+{% block title %}Article{% endblock %}
+
+{% block body %}
+    <h1>Article</h1>
+
+    <table class="table">
+        <tbody>
+            <tr>
+                <th>Id</th>
+                <td>{{ article.id }}</td>
+            </tr>
+            <tr>
+                <th>Title</th>
+                <td>{{ article.title }}</td>
+            </tr>
+            <tr>
+                <th>PublicationDate</th>
+                <td>{{ article.publicationDate ? article.publicationDate|date('Y-m-d H:i:s') : '' }}</td>
+            </tr>
+            <tr>
+                <th>Content</th>
+                <td>{{ article.content }}</td>
+            </tr>
+        </tbody>
+    </table>
+
+    <a href="{{ path('app_article_index') }}">back to list</a>
+
+    <a href="{{ path('app_article_edit', {'id': article.id}) }}">edit</a>
+
+    {{ include('article/_delete_form.html.twig') }}
+{% endblock %}

+ 15 - 11
templates/index/index.html.twig

@@ -10,17 +10,21 @@
     Le blog de benj
 
     <h2>Derniers articles</h2>
-    {% for article in articles %}
-        <article>
-            <h2>{{ article.title}}</h2>
-            <div class="article-by">
-                {{ article.author }} - {{ article.publicationDate }}
-            </div>
-            {{ article.content | raw }}
-        </article>
-    {% else %}
-        <p class="remarque">Rien pour le moment</p>
-    {% endfor %}
+    <section id="section-articles">
+        {% for article in articles %}
+            <article>
+                <h2>{{ article.title}}</h2>
+                <div class="article-by">
+                    {{ article.author }}, le {{ article.publicationDate | date('d-m-Y')}}
+                </div>
+                <div class="contenu">
+                    {{ article.content | raw }}
+                </div>
+            </article>
+        {% else %}
+            <p class="remarque">Rien pour le moment</p>
+        {% endfor %}
+    </section>
 
 </div>
 {% endblock %}

+ 131 - 0
tests/Controller/ArticleControllerTest.php

@@ -0,0 +1,131 @@
+<?php
+
+namespace App\Tests\Controller;
+
+use App\Entity\Article;
+use Doctrine\ORM\EntityManagerInterface;
+use Doctrine\ORM\EntityRepository;
+use Symfony\Bundle\FrameworkBundle\KernelBrowser;
+use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
+
+final class ArticleControllerTest extends WebTestCase
+{
+    private KernelBrowser $client;
+    private EntityManagerInterface $manager;
+    private EntityRepository $repository;
+    private string $path = '/article/';
+
+    protected function setUp(): void
+    {
+        $this->client = static::createClient();
+        $this->manager = static::getContainer()->get('doctrine')->getManager();
+        $this->repository = $this->manager->getRepository(Article::class);
+
+        foreach ($this->repository->findAll() as $object) {
+            $this->manager->remove($object);
+        }
+
+        $this->manager->flush();
+    }
+
+    public function testIndex(): void
+    {
+        $this->client->followRedirects();
+        $crawler = $this->client->request('GET', $this->path);
+
+        self::assertResponseStatusCodeSame(200);
+        self::assertPageTitleContains('Article index');
+
+        // Use the $crawler to perform additional assertions e.g.
+        // self::assertSame('Some text on the page', $crawler->filter('.p')->first());
+    }
+
+    public function testNew(): void
+    {
+        $this->markTestIncomplete();
+        $this->client->request('GET', sprintf('%snew', $this->path));
+
+        self::assertResponseStatusCodeSame(200);
+
+        $this->client->submitForm('Save', [
+            'article[title]' => 'Testing',
+            'article[publicationDate]' => 'Testing',
+            'article[content]' => 'Testing',
+            'article[author]' => 'Testing',
+        ]);
+
+        self::assertResponseRedirects($this->path);
+
+        self::assertSame(1, $this->repository->count([]));
+    }
+
+    public function testShow(): void
+    {
+        $this->markTestIncomplete();
+        $fixture = new Article();
+        $fixture->setTitle('My Title');
+        $fixture->setPublicationDate('My Title');
+        $fixture->setContent('My Title');
+        $fixture->setAuthor('My Title');
+
+        $this->manager->persist($fixture);
+        $this->manager->flush();
+
+        $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId()));
+
+        self::assertResponseStatusCodeSame(200);
+        self::assertPageTitleContains('Article');
+
+        // Use assertions to check that the properties are properly displayed.
+    }
+
+    public function testEdit(): void
+    {
+        $this->markTestIncomplete();
+        $fixture = new Article();
+        $fixture->setTitle('Value');
+        $fixture->setPublicationDate('Value');
+        $fixture->setContent('Value');
+        $fixture->setAuthor('Value');
+
+        $this->manager->persist($fixture);
+        $this->manager->flush();
+
+        $this->client->request('GET', sprintf('%s%s/edit', $this->path, $fixture->getId()));
+
+        $this->client->submitForm('Update', [
+            'article[title]' => 'Something New',
+            'article[publicationDate]' => 'Something New',
+            'article[content]' => 'Something New',
+            'article[author]' => 'Something New',
+        ]);
+
+        self::assertResponseRedirects('/article/');
+
+        $fixture = $this->repository->findAll();
+
+        self::assertSame('Something New', $fixture[0]->getTitle());
+        self::assertSame('Something New', $fixture[0]->getPublicationDate());
+        self::assertSame('Something New', $fixture[0]->getContent());
+        self::assertSame('Something New', $fixture[0]->getAuthor());
+    }
+
+    public function testRemove(): void
+    {
+        $this->markTestIncomplete();
+        $fixture = new Article();
+        $fixture->setTitle('Value');
+        $fixture->setPublicationDate('Value');
+        $fixture->setContent('Value');
+        $fixture->setAuthor('Value');
+
+        $this->manager->persist($fixture);
+        $this->manager->flush();
+
+        $this->client->request('GET', sprintf('%s%s', $this->path, $fixture->getId()));
+        $this->client->submitForm('Delete');
+
+        self::assertResponseRedirects('/article/');
+        self::assertSame(0, $this->repository->count([]));
+    }
+}