bumped symfony to 5.3

This commit is contained in:
2021-06-01 18:48:20 +02:00
parent c39ac0d299
commit c18d5dc339
58 changed files with 7128 additions and 455 deletions

@ -22,6 +22,7 @@ class BlogCrudController extends AbstractCrudController
AssociationField::new('author')
->autocomplete(),
TextField::new('title'),
TextField::new('slug'),
TextEditorField::new('teaser'),
TextEditorField::new('content'),
DateTimeField::new('createdAt'),

@ -29,11 +29,11 @@ class BlogController extends AbstractController
*
* @return \Symfony\Component\HttpFoundation\Response
*/
#[Route('/blog/{id}', name: 'blog')]
public function show($id, BlogRepository $blogRepository): Response
#[Route('/blog/{slug}', name: 'blog')]
public function show($slug, BlogRepository $blogRepository): Response
{
return $this->render('blog/show.html.twig', [
'blog' => $blogRepository->findOneBy(['id' => $id])
'blog' => $blogRepository->findOneBy(['slug' => $slug])
]);
}
}

@ -0,0 +1,94 @@
<?php
namespace App\Controller;
use App\Entity\User;
use App\Form\RegistrationFormType;
use App\Security\EmailVerifier;
use App\Repository\UserRepository;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Address;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
class RegistrationController extends AbstractController
{
private $emailVerifier;
public function __construct(EmailVerifier $emailVerifier)
{
$this->emailVerifier = $emailVerifier;
}
#[Route('/register', name: 'app_register')]
public function register(Request $request, UserPasswordEncoderInterface $passwordEncoder): Response
{
$user = new User();
$form = $this->createForm(RegistrationFormType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
// encode the plain password
$user->setPassword(
$passwordEncoder->encodePassword(
$user,
$form->get('plainPassword')->getData()
)
);
$entityManager = $this->getDoctrine()->getManager();
$entityManager->persist($user);
$entityManager->flush();
// generate a signed url and email it to the user
$this->emailVerifier->sendEmailConfirmation('app_verify_email', $user,
(new TemplatedEmail())
->from(new Address('tracer@24unix.net', '24unix'))
->to($user->getEmail())
->subject('Please Confirm your Email')
->htmlTemplate('registration/confirmation_email.html.twig')
);
// do anything else you need here, like send an email
return $this->redirectToRoute('blogs');
}
return $this->render('registration/register.html.twig', [
'registrationForm' => $form->createView(),
]);
}
#[Route('/verify/email', name: 'app_verify_email')]
public function verifyUserEmail(Request $request, UserRepository $userRepository): Response
{
$id = $request->get('id');
if (null === $id) {
return $this->redirectToRoute('app_register');
}
$user = $userRepository->find($id);
if (null === $user) {
return $this->redirectToRoute('app_register');
}
// validate email confirmation link, sets User::isVerified=true and persists
try {
$this->emailVerifier->handleEmailConfirmation($request, $user);
} catch (VerifyEmailExceptionInterface $exception) {
$this->addFlash('verify_email_error', $exception->getReason());
return $this->redirectToRoute('app_register');
}
// @TODO Change the redirect on success and handle or remove the flash message in your templates
$this->addFlash('success', 'Your email address has been verified.');
return $this->redirectToRoute('app_register');
}
}

@ -6,9 +6,12 @@ use App\Repository\BlogRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JetBrains\PhpStorm\Pure;
use App\Repository\SectionRepository;
/**
* @ORM\Entity(repositoryClass=BlogRepository::class)
* @ORM\HasLifecycleCallbacks()
*/
class Blog
{
@ -22,28 +25,28 @@ class Blog
/**
* @ORM\Column(type="string", length=255)
*/
private $title;
private ?string $title;
/**
* @ORM\Column(type="text", nullable=true)
*/
private $teaser;
private ?string $teaser;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
private $teaserImage;
private ?string $teaserImage;
/**
* @ORM\Column(type="text")
*/
private $content;
private ?string $content;
/**
* @ORM\ManyToOne(targetEntity=User::class, inversedBy="blogs")
* @ORM\JoinColumn(nullable=false)
*/
private $author;
private ?User $author;
/**
* @ORM\ManyToMany(targetEntity=Section::class, inversedBy="blogs")
@ -53,17 +56,17 @@ class Blog
/**
* @ORM\Column(type="datetime")
*/
private $createdAt;
private ?\DateTimeInterface $createdAt;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
private $editedAt;
private ?\DateTimeInterface $editedAt;
/**
* @ORM\ManyToOne(targetEntity=User::class)
*/
private $editedBy;
private ?User $editedBy;
/**
* @ORM\Column(type="string", length=255, nullable=true)
@ -75,16 +78,25 @@ class Blog
*/
private $comments;
/**
* @ORM\Column(type="string", length=255)
*/
private $slug;
#[Pure]
public function __construct()
{
$this->section = new ArrayCollection();
$this->comments = new ArrayCollection();
}
public function __toString()
{
return $this->title;
}
/**
* @return null|string
*/
public function __toString()
{
return $this->title;
}
public function getId(): ?int
@ -253,4 +265,16 @@ class Blog
return $this;
}
public function getSlug(): ?string
{
return $this->slug;
}
public function setSlug(string $slug): self
{
$this->slug = $slug;
return $this;
}
}

@ -7,10 +7,12 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use JetBrains\PhpStorm\Pure;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* @ORM\Entity(repositoryClass=UserRepository::class)
* @UniqueEntity(fields={"username"}, message="There is already an account with this username")
*/
class User implements UserInterface
{
@ -71,22 +73,27 @@ class User implements UserInterface
* @ORM\OneToMany(targetEntity=Comment::class, mappedBy="author")
*/
private $comments;
/**
* @ORM\Column(type="boolean")
*/
private $isVerified = false;
#[Pure] public function __construct()
{
$this->blogs = new ArrayCollection();
$this->comments = new ArrayCollection();
}
{
$this->blogs = new ArrayCollection();
$this->comments = new ArrayCollection();
}
public function __toString()
{
return $this->username;
}
{
return $this->username;
}
public function getId(): ?int
{
return $this->id;
}
{
return $this->id;
}
/**
* A visual identifier that represents this user.
@ -94,50 +101,50 @@ class User implements UserInterface
* @see UserInterface
*/
public function getUsername(): string
{
return (string)$this->username;
}
{
return (string)$this->username;
}
public function setUsername(string $username): self
{
$this->username = $username;
return $this;
}
{
$this->username = $username;
return $this;
}
/**
* @see UserInterface
*/
public function getRoles(): array
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
{
$roles = $this->roles;
// guarantee every user at least has ROLE_USER
$roles[] = 'ROLE_USER';
return array_unique($roles);
}
public function setRoles(array $roles): self
{
$this->roles = $roles;
return $this;
}
{
$this->roles = $roles;
return $this;
}
/**
* @see UserInterface
*/
public function getPassword(): string
{
return $this->password;
}
{
return $this->password;
}
public function setPassword(string $password): self
{
$this->password = $password;
return $this;
}
{
$this->password = $password;
return $this;
}
/**
* Returning a salt is only needed, if you are not using a modern
@ -146,136 +153,148 @@ class User implements UserInterface
* @see UserInterface
*/
public function getSalt(): ?string
{
return null;
}
{
return null;
}
/**
* @see UserInterface
*/
public function eraseCredentials()
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
{
// If you store any temporary, sensitive data on the user, clear it here
// $this->plainPassword = null;
}
public function getFirstName(): ?string
{
return $this->firstName;
}
{
return $this->firstName;
}
public function setFirstName(?string $firstName): self
{
$this->firstName = $firstName;
return $this;
}
{
$this->firstName = $firstName;
return $this;
}
public function getLastName(): ?string
{
return $this->lastName;
}
{
return $this->lastName;
}
public function setLastName(?string $lastName): self
{
$this->lastName = $lastName;
return $this;
}
{
$this->lastName = $lastName;
return $this;
}
public function getEmail(): ?string
{
return $this->email;
}
{
return $this->email;
}
public function setEmail(string $email): self
{
$this->email = $email;
return $this;
}
{
$this->email = $email;
return $this;
}
public function getCreatedAt(): ?\DateTimeInterface
{
return $this->createdAt;
}
{
return $this->createdAt;
}
public function setCreatedAt(\DateTimeInterface $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
{
$this->createdAt = $createdAt;
return $this;
}
public function getLastLoginAt(): ?\DateTimeInterface
{
return $this->lastLoginAt;
}
{
return $this->lastLoginAt;
}
public function setLastLoginAt(?\DateTimeInterface $lastLoginAt): self
{
$this->lastLoginAt = $lastLoginAt;
return $this;
}
{
$this->lastLoginAt = $lastLoginAt;
return $this;
}
/**
* @return Collection|Blog[]
*/
public function getBlogs(): Collection
{
return $this->blogs;
}
{
return $this->blogs;
}
public function addBlog(Blog $blog): self
{
if (!$this->blogs->contains($blog)) {
$this->blogs[] = $blog;
$blog->setAuthor($this);
}
return $this;
}
{
if (!$this->blogs->contains($blog)) {
$this->blogs[] = $blog;
$blog->setAuthor($this);
}
return $this;
}
public function removeBlog(Blog $blog): self
{
if ($this->blogs->removeElement($blog)) {
// set the owning side to null (unless already changed)
if ($blog->getAuthor() === $this) {
$blog->setAuthor(null);
}
}
return $this;
}
{
if ($this->blogs->removeElement($blog)) {
// set the owning side to null (unless already changed)
if ($blog->getAuthor() === $this) {
$blog->setAuthor(null);
}
}
return $this;
}
/**
* @return Collection|Comment[]
*/
public function getComments(): Collection
{
return $this->comments;
}
{
return $this->comments;
}
public function addComment(Comment $comment): self
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setAuthor($this);
}
return $this;
}
{
if (!$this->comments->contains($comment)) {
$this->comments[] = $comment;
$comment->setAuthor($this);
}
return $this;
}
public function removeComment(Comment $comment): self
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getAuthor() === $this) {
$comment->setAuthor(null);
}
}
return $this;
}
{
if ($this->comments->removeElement($comment)) {
// set the owning side to null (unless already changed)
if ($comment->getAuthor() === $this) {
$comment->setAuthor(null);
}
}
return $this;
}
public function isVerified(): bool
{
return $this->isVerified;
}
public function setIsVerified(bool $isVerified): self
{
$this->isVerified = $isVerified;
return $this;
}
}

@ -0,0 +1,55 @@
<?php
namespace App\Form;
use App\Entity\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Validator\Constraints\IsTrue;
use Symfony\Component\Validator\Constraints\Length;
use Symfony\Component\Validator\Constraints\NotBlank;
class RegistrationFormType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('username')
->add('agreeTerms', CheckboxType::class, [
'mapped' => false,
'constraints' => [
new IsTrue([
'message' => 'You should agree to our terms.',
]),
],
])
->add('plainPassword', PasswordType::class, [
// instead of being set onto the object directly,
// this is read and encoded in the controller
'mapped' => false,
'attr' => ['autocomplete' => 'new-password'],
'constraints' => [
new NotBlank([
'message' => 'Please enter a password',
]),
new Length([
'min' => 6,
'minMessage' => 'Your password should be at least {{ limit }} characters',
// max length allowed by Symfony for security reasons
'max' => 4096,
]),
],
])
;
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => User::class,
]);
}
}

@ -0,0 +1,66 @@
<?php
namespace App\Security;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Mailer\Exception\TransportExceptionInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Security\Core\User\UserInterface;
use SymfonyCasts\Bundle\VerifyEmail\Exception\VerifyEmailExceptionInterface;
use SymfonyCasts\Bundle\VerifyEmail\VerifyEmailHelperInterface;
/**
* Class EmailVerifier
* @package App\Security
*/
class EmailVerifier
{
private VerifyEmailHelperInterface $verifyEmailHelper;
private MailerInterface $mailer;
private EntityManagerInterface $entityManager;
public function __construct(VerifyEmailHelperInterface $helper, MailerInterface $mailer, EntityManagerInterface $manager)
{
$this->verifyEmailHelper = $helper;
$this->mailer = $mailer;
$this->entityManager = $manager;
}
public function sendEmailConfirmation(string $verifyEmailRouteName, UserInterface $user, TemplatedEmail $email): void
{
$signatureComponents = $this->verifyEmailHelper->generateSignature(
$verifyEmailRouteName,
$user->getId(),
$user->getEmail(),
['id' => $user->getId()]
);
$context = $email->getContext();
$context['signedUrl'] = $signatureComponents->getSignedUrl();
$context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
$context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
$email->context($context);
try {
$this->mailer->send($email);
} catch (TransportExceptionInterface $e) {
die("Error: " . $e->getMessage());
}
}
/**
* @throws VerifyEmailExceptionInterface
*/
public function handleEmailConfirmation(Request $request, UserInterface $user): void
{
$this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail());
$user->setIsVerified(true);
$this->entityManager->persist($user);
$this->entityManager->flush();
}
}