mirror of
https://github.com/jbcr/core.git
synced 2026-03-24 00:42:14 +01:00
Spring cleaning
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
Contributing
|
||||
============
|
||||
Contributing to Bolt
|
||||
====================
|
||||
|
||||
The Symfony Demo application is an open source project. Contributions made by
|
||||
the community are welcome. Send us your ideas, code reviews, pull requests and
|
||||
feature requests to help us improve this project. All contributions must follow
|
||||
the [usual Symfony contribution requirements](https://symfony.com/doc/current/contributing/index.html).
|
||||
Bolt is an open source, community-driven project. Whether you're a user of
|
||||
Bolt, a developer or both, contributing to Bolt is easy!
|
||||
|
||||
If you'd like to contribute, please read "[Contributing to Bolt][contributing]"
|
||||
on the documentation site.
|
||||
|
||||
[contributing]: https://docs.bolt.cm/other/contributing
|
||||
|
||||
@@ -31,11 +31,6 @@ services:
|
||||
resource: '../src/Controller'
|
||||
tags: ['controller.service_arguments']
|
||||
|
||||
# when the service definition only contains arguments, you can omit the
|
||||
# 'arguments' key and define the arguments just below the service class
|
||||
Bolt\EventSubscriber\CommentNotificationSubscriber:
|
||||
$sender: '%app.notifications.email_sender%'
|
||||
|
||||
doctrine.content_listener:
|
||||
class: Bolt\EventListener\ContentListener
|
||||
arguments: []
|
||||
|
||||
@@ -1,167 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Controller;
|
||||
|
||||
use Bolt\Entity\Comment;
|
||||
use Bolt\Entity\Content;
|
||||
use Bolt\Entity\Post;
|
||||
use Bolt\Events;
|
||||
use Bolt\Form\CommentType;
|
||||
use Bolt\Repository\PostRepository;
|
||||
use Bolt\Repository\TagRepository;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Cache;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\IsGranted;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\ParamConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\GenericEvent;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
|
||||
/**
|
||||
* Controller used to manage blog contents in the public part of the site.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
final class BlogController extends AbstractController
|
||||
{
|
||||
/**
|
||||
* xRoute("/", defaults={"page": "1", "_format"="html"}, methods={"GET"}, name="blog_index")
|
||||
* xRoute("/rss.xml", defaults={"page": "1", "_format"="xml"}, methods={"GET"}, name="blog_rss")
|
||||
* xRoute("/page/{page<[1-9]\d*>}", defaults={"_format"="html"}, methods={"GET"}, name="blog_index_paginated").
|
||||
*
|
||||
* @Cache(smaxage="10")
|
||||
*
|
||||
* NOTE: For standard formats, Symfony will also automatically choose the best
|
||||
* Content-Type header for the response.
|
||||
* See https://symfony.com/doc/current/quick_tour/the_controller.html#using-formats
|
||||
*/
|
||||
public function index(Request $request, int $page, string $_format, PostRepository $posts, TagRepository $tags): Response
|
||||
{
|
||||
$tag = null;
|
||||
if ($request->query->has('tag')) {
|
||||
$tag = $tags->findOneBy(['name' => $request->query->get('tag')]);
|
||||
}
|
||||
$latestPosts = $posts->findLatest($page, $tag);
|
||||
|
||||
// Every template name also has two extensions that specify the format and
|
||||
// engine for that template.
|
||||
// See https://symfony.com/doc/current/templating.html#template-suffix
|
||||
return $this->render('blog/index.' . $_format . '.twig', ['posts' => $latestPosts]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/posts/{slug}", methods={"GET"}, name="blog_post")
|
||||
*
|
||||
* NOTE: The $post controller argument is automatically injected by Symfony
|
||||
* after performing a database query looking for a Post with the 'slug'
|
||||
* value given in the route.
|
||||
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html
|
||||
*/
|
||||
public function postShow(Post $post): Response
|
||||
{
|
||||
// Symfony's 'dump()' function is an improved version of PHP's 'var_dump()' but
|
||||
// it's not available in the 'prod' environment to prevent leaking sensitive information.
|
||||
// It can be used both in PHP files and Twig templates, but it requires to
|
||||
// have enabled the DebugBundle. Uncomment the following line to see it in action:
|
||||
//
|
||||
// dump($post, $this->getUser(), new \DateTime());
|
||||
|
||||
return $this->render('blog/post_show.html.twig', ['post' => $post]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/comment/{postSlug}/new", methods={"POST"}, name="comment_new")
|
||||
* @IsGranted("IS_AUTHENTICATED_FULLY")
|
||||
* @ParamConverter("post", options={"mapping": {"postSlug": "slug"}})
|
||||
*
|
||||
* NOTE: The ParamConverter mapping is required because the route parameter
|
||||
* (postSlug) doesn't match any of the Doctrine entity properties (slug).
|
||||
* See https://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/annotations/converters.html#doctrine-converter
|
||||
*/
|
||||
public function commentNew(Request $request, Post $post, EventDispatcherInterface $eventDispatcher): Response
|
||||
{
|
||||
$comment = new Comment();
|
||||
$comment->setAuthor($this->getUser());
|
||||
$post->addComment($comment);
|
||||
|
||||
$form = $this->createForm(CommentType::class, $comment);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$em = $this->getDoctrine()->getManager();
|
||||
$em->persist($comment);
|
||||
$em->flush();
|
||||
|
||||
// When triggering an event, you can optionally pass some information.
|
||||
// For simple applications, use the GenericEvent object provided by Symfony
|
||||
// to pass some PHP variables. For more complex applications, define your
|
||||
// own event object classes.
|
||||
// See https://symfony.com/doc/current/components/event_dispatcher/generic_event.html
|
||||
$event = new GenericEvent($comment);
|
||||
|
||||
// When an event is dispatched, Symfony notifies it to all the listeners
|
||||
// and subscribers registered to it. Listeners can modify the information
|
||||
// passed in the event and they can even modify the execution flow, so
|
||||
// there's no guarantee that the rest of this controller will be executed.
|
||||
// See https://symfony.com/doc/current/components/event_dispatcher.html
|
||||
$eventDispatcher->dispatch(Events::COMMENT_CREATED, $event);
|
||||
|
||||
return $this->redirectToRoute('blog_post', ['slug' => $post->getSlug()]);
|
||||
}
|
||||
|
||||
return $this->render('blog/comment_form_error.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* This controller is called directly via the render() function in the
|
||||
* blog/post_show.html.twig template. That's why it's not needed to define
|
||||
* a route name for it.
|
||||
*
|
||||
* The "id" of the Post is passed in and then turned into a Post object
|
||||
* automatically by the ParamConverter.
|
||||
*/
|
||||
public function commentForm(Post $post): Response
|
||||
{
|
||||
$form = $this->createForm(CommentType::class);
|
||||
|
||||
return $this->render('blog/_comment_form.html.twig', [
|
||||
'post' => $post,
|
||||
'form' => $form->createView(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @Route("/search", methods={"GET"}, name="blog_search")
|
||||
*/
|
||||
public function search(Request $request, PostRepository $posts): Response
|
||||
{
|
||||
if (!$request->isXmlHttpRequest()) {
|
||||
return $this->render('blog/search.html.twig');
|
||||
}
|
||||
|
||||
$query = $request->query->get('q', '');
|
||||
$limit = $request->query->get('l', 10);
|
||||
$foundPosts = $posts->findBySearchQuery($query, $limit);
|
||||
|
||||
$results = [];
|
||||
foreach ($foundPosts as $post) {
|
||||
$results[] = [
|
||||
'title' => htmlspecialchars($post->getTitle(), ENT_COMPAT | ENT_HTML5),
|
||||
'date' => $post->getPublishedAt()->format('M d, Y'),
|
||||
'author' => htmlspecialchars($post->getAuthor()->getFullName(), ENT_COMPAT | ENT_HTML5),
|
||||
'summary' => htmlspecialchars($post->getSummary(), ENT_COMPAT | ENT_HTML5),
|
||||
'url' => $this->generateUrl('blog_post', ['slug' => $post->getSlug()]),
|
||||
];
|
||||
}
|
||||
|
||||
return $this->json($results);
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Controller\Bolt;
|
||||
|
||||
use Bolt\Form\Type\ChangePasswordType;
|
||||
use Bolt\Form\ChangePasswordType;
|
||||
use Bolt\Form\UserType;
|
||||
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
||||
@@ -9,13 +9,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Annotation\Route;
|
||||
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
|
||||
|
||||
/**
|
||||
* Controller used to manage the application security.
|
||||
* See https://symfony.com/doc/current/cookbook/security/form_login_setup.html.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
|
||||
class SecurityController extends AbstractController
|
||||
{
|
||||
/**
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="bolt_comment")
|
||||
*
|
||||
* Defines the properties of the Comment entity to represent the blog comments.
|
||||
* See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See https://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class Comment
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var Post
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Post", inversedBy="comments")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $post;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\NotBlank(message="comment.blank")
|
||||
* @Assert\Length(
|
||||
* min=5,
|
||||
* minMessage="comment.too_short",
|
||||
* max=10000,
|
||||
* maxMessage="comment.too_long"
|
||||
* )
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(type="datetime")
|
||||
* @Assert\DateTime
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Bolt\Entity\User")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $author;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publishedAt = new \DateTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* @Assert\IsTrue(message="comment.is_spam")
|
||||
*/
|
||||
public function isLegitComment(): bool
|
||||
{
|
||||
$containsInvalidCharacters = false !== mb_strpos($this->content, '@');
|
||||
|
||||
return !$containsInvalidCharacters;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getPublishedAt(): \DateTime
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
|
||||
public function setPublishedAt(\DateTime $publishedAt): void
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
}
|
||||
|
||||
public function getAuthor(): User
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(User $author): void
|
||||
{
|
||||
$this->author = $author;
|
||||
}
|
||||
|
||||
public function getPost(): ?Post
|
||||
{
|
||||
return $this->post;
|
||||
}
|
||||
|
||||
public function setPost(?Post $post): void
|
||||
{
|
||||
$this->post = $post;
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Entity;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* @ORM\Entity(repositoryClass="Bolt\Repository\PostRepository")
|
||||
* @ORM\Table(name="bolt_post")
|
||||
*
|
||||
* Defines the properties of the Post entity to represent the blog posts.
|
||||
*
|
||||
* See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* Tip: if you have an existing database, you can generate these entity class automatically.
|
||||
* See https://symfony.com/doc/current/cookbook/doctrine/reverse_engineering.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class Post
|
||||
{
|
||||
/**
|
||||
* Use constants to define configuration options that rarely change instead
|
||||
* of specifying them under parameters section in config/services.yaml file.
|
||||
*
|
||||
* See https://symfony.com/doc/current/best_practices/configuration.html#constants-vs-configuration-options
|
||||
*/
|
||||
public const NUM_ITEMS = 10;
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank
|
||||
*/
|
||||
private $title;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
*/
|
||||
private $slug;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string")
|
||||
* @Assert\NotBlank(message="post.blank_summary")
|
||||
* @Assert\Length(max=255)
|
||||
*/
|
||||
private $summary;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="text")
|
||||
* @Assert\NotBlank(message="post.blank_content")
|
||||
* @Assert\Length(min=10, minMessage="post.too_short_content")
|
||||
*/
|
||||
private $content;
|
||||
|
||||
/**
|
||||
* @var \DateTime
|
||||
*
|
||||
* @ORM\Column(type="datetime")
|
||||
* @Assert\DateTime
|
||||
*/
|
||||
private $publishedAt;
|
||||
|
||||
/**
|
||||
* @var User
|
||||
*
|
||||
* @ORM\ManyToOne(targetEntity="Bolt\Entity\User")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*/
|
||||
private $author;
|
||||
|
||||
/**
|
||||
* @var Comment[]|ArrayCollection
|
||||
*
|
||||
* @ORM\OneToMany(
|
||||
* targetEntity="Comment",
|
||||
* mappedBy="post",
|
||||
* orphanRemoval=true,
|
||||
* cascade={"persist"}
|
||||
* )
|
||||
* @ORM\OrderBy({"publishedAt": "DESC"})
|
||||
*/
|
||||
private $comments;
|
||||
|
||||
/**
|
||||
* @var Tag[]|ArrayCollection
|
||||
*
|
||||
* @ORM\ManyToMany(targetEntity="Bolt\Entity\Tag", cascade={"persist"})
|
||||
* @ORM\JoinTable(name="bolt_post_tag")
|
||||
* @ORM\OrderBy({"name": "ASC"})
|
||||
* @Assert\Count(max="4", maxMessage="post.too_many_tags")
|
||||
*/
|
||||
private $tags;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->publishedAt = new \DateTime();
|
||||
$this->comments = new ArrayCollection();
|
||||
$this->tags = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getTitle(): ?string
|
||||
{
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
public function setTitle(?string $title): void
|
||||
{
|
||||
$this->title = $title;
|
||||
}
|
||||
|
||||
public function getSlug(): ?string
|
||||
{
|
||||
return $this->slug;
|
||||
}
|
||||
|
||||
public function setSlug(?string $slug): void
|
||||
{
|
||||
$this->slug = $slug;
|
||||
}
|
||||
|
||||
public function getContent(): ?string
|
||||
{
|
||||
return $this->content;
|
||||
}
|
||||
|
||||
public function setContent(?string $content): void
|
||||
{
|
||||
$this->content = $content;
|
||||
}
|
||||
|
||||
public function getPublishedAt(): \DateTime
|
||||
{
|
||||
return $this->publishedAt;
|
||||
}
|
||||
|
||||
public function setPublishedAt(?\DateTime $publishedAt): void
|
||||
{
|
||||
$this->publishedAt = $publishedAt;
|
||||
}
|
||||
|
||||
public function getAuthor(): User
|
||||
{
|
||||
return $this->author;
|
||||
}
|
||||
|
||||
public function setAuthor(?User $author): void
|
||||
{
|
||||
$this->author = $author;
|
||||
}
|
||||
|
||||
public function getComments(): Collection
|
||||
{
|
||||
return $this->comments;
|
||||
}
|
||||
|
||||
public function addComment(?Comment $comment): void
|
||||
{
|
||||
$comment->setPost($this);
|
||||
if (!$this->comments->contains($comment)) {
|
||||
$this->comments->add($comment);
|
||||
}
|
||||
}
|
||||
|
||||
public function removeComment(Comment $comment): void
|
||||
{
|
||||
$comment->setPost(null);
|
||||
$this->comments->removeElement($comment);
|
||||
}
|
||||
|
||||
public function getSummary(): ?string
|
||||
{
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
public function setSummary(?string $summary): void
|
||||
{
|
||||
$this->summary = $summary;
|
||||
}
|
||||
|
||||
public function addTag(?Tag ...$tags): void
|
||||
{
|
||||
foreach ($tags as $tag) {
|
||||
if (!$this->tags->contains($tag)) {
|
||||
$this->tags->add($tag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function removeTag(Tag $tag): void
|
||||
{
|
||||
$this->tags->removeElement($tag);
|
||||
}
|
||||
|
||||
public function getTags(): Collection
|
||||
{
|
||||
return $this->tags;
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\Table(name="bolt_tag")
|
||||
*
|
||||
* Defines the properties of the Tag entity to represent the post tags.
|
||||
*
|
||||
* See https://symfony.com/doc/current/book/doctrine.html#creating-an-entity-class
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class Tag implements \JsonSerializable
|
||||
{
|
||||
/**
|
||||
* @var int
|
||||
*
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*
|
||||
* @ORM\Column(type="string", unique=true, length=190)
|
||||
*/
|
||||
private $name;
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function jsonSerialize(): string
|
||||
{
|
||||
// This entity implements JsonSerializable (http://php.net/manual/en/class.jsonserializable.php)
|
||||
// so this method is used to customize its JSON representation when json_encode()
|
||||
// is called, for example in tags|json_encode (app/Resources/views/form/fields.html.twig)
|
||||
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\EventSubscriber;
|
||||
|
||||
use Bolt\Entity\Comment;
|
||||
use Bolt\Events;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\EventDispatcher\GenericEvent;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Component\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Notifies post's author about new comments.
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
class CommentNotificationSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
private $mailer;
|
||||
private $translator;
|
||||
private $urlGenerator;
|
||||
private $sender;
|
||||
|
||||
public function __construct(\Swift_Mailer $mailer, UrlGeneratorInterface $urlGenerator, TranslatorInterface $translator, $sender)
|
||||
{
|
||||
$this->mailer = $mailer;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->translator = $translator;
|
||||
$this->sender = $sender;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
{
|
||||
return [
|
||||
Events::COMMENT_CREATED => 'onCommentCreated',
|
||||
];
|
||||
}
|
||||
|
||||
public function onCommentCreated(GenericEvent $event): void
|
||||
{
|
||||
/** @var Comment $comment */
|
||||
$comment = $event->getSubject();
|
||||
$post = $comment->getPost();
|
||||
|
||||
$linkToPost = $this->urlGenerator->generate('blog_post', [
|
||||
'slug' => $post->getSlug(),
|
||||
'_fragment' => 'comment_' . $comment->getId(),
|
||||
], UrlGeneratorInterface::ABSOLUTE_URL);
|
||||
|
||||
$subject = $this->translator->trans('notification.comment_created');
|
||||
$body = $this->translator->trans('notification.comment_created.description', [
|
||||
'%title%' => $post->getTitle(),
|
||||
'%link%' => $linkToPost,
|
||||
]);
|
||||
|
||||
// Symfony uses a library called SwiftMailer to send emails. That's why
|
||||
// email messages are created instantiating a Swift_Message class.
|
||||
// See https://symfony.com/doc/current/email.html#sending-emails
|
||||
$message = (new \Swift_Message())
|
||||
->setSubject($subject)
|
||||
->setTo($post->getAuthor()->getEmail())
|
||||
->setFrom($this->sender)
|
||||
->setBody($body, 'text/html')
|
||||
;
|
||||
|
||||
// In config/packages/dev/swiftmailer.yaml the 'disable_delivery' option is set to 'true'.
|
||||
// That's why in the development environment you won't actually receive any email.
|
||||
// However, you can inspect the contents of those unsent emails using the debug toolbar.
|
||||
// See https://symfony.com/doc/current/email/dev_environment.html#viewing-from-the-web-debug-toolbar
|
||||
$this->mailer->send($message);
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt;
|
||||
|
||||
/**
|
||||
* This class defines the names of all the events dispatched in
|
||||
* the Symfony Demo application. It's not mandatory to create a
|
||||
* class like this, but it's considered a good practice.
|
||||
*
|
||||
* @author Oleg Voronkovich <oleg-voronkovich@yandex.ru>
|
||||
*/
|
||||
final class Events
|
||||
{
|
||||
/**
|
||||
* For the event naming conventions, see:
|
||||
* https://symfony.com/doc/current/components/event_dispatcher.html#naming-conventions.
|
||||
*
|
||||
* @Event("Symfony\Component\EventDispatcher\GenericEvent")
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public const COMMENT_CREATED = 'comment.created';
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form\Type;
|
||||
namespace Bolt\Form;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
|
||||
@@ -1,52 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form;
|
||||
|
||||
use Bolt\Entity\Comment;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used to create and manipulate blog comments. Although in this
|
||||
* case the form is trivial and we could build it inside the controller, a good
|
||||
* practice is to always define your forms as classes.
|
||||
*
|
||||
* See https://symfony.com/doc/current/book/forms.html#creating-form-classes
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class CommentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
// $builder->add('content', null, ['required' => false]);
|
||||
|
||||
$builder
|
||||
->add('content', TextareaType::class, [
|
||||
'help' => 'Comments not complying with our Code of Conduct will be moderated.',
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Comment::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,65 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form;
|
||||
|
||||
use Bolt\Entity\Content;
|
||||
use Bolt\Form\Type\DateTimePickerType;
|
||||
use Bolt\Form\Type\TagsInputType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class ContentType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
// For the full reference of options defined by each form field type
|
||||
// see https://symfony.com/doc/current/reference/forms/types.html
|
||||
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
// $builder->add('title', null, ['required' => false, ...]);
|
||||
|
||||
$builder
|
||||
->add('title', null, [
|
||||
'attr' => ['autofocus' => true],
|
||||
'label' => 'label.title',
|
||||
])
|
||||
->add('summary', TextareaType::class, [
|
||||
'help' => 'Summaries can\'t contain Markdown or HTML contents; only plain text.',
|
||||
'label' => 'label.summary',
|
||||
])
|
||||
->add('content', null, [
|
||||
'attr' => ['rows' => 20],
|
||||
'help' => 'Use Markdown to format the blog post contents. HTML is allowed too.',
|
||||
'label' => 'label.content',
|
||||
])
|
||||
->add('publishedAt', DateTimePickerType::class, [
|
||||
'label' => 'label.published_at',
|
||||
'help' => 'Set the date in the future to schedule the blog post publication.',
|
||||
])
|
||||
->add('tags', TagsInputType::class, [
|
||||
'label' => 'label.tags',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Content::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form\DataTransformer;
|
||||
|
||||
use Bolt\Entity\Tag;
|
||||
use Bolt\Repository\TagRepository;
|
||||
use Symfony\Component\Form\DataTransformerInterface;
|
||||
|
||||
/**
|
||||
* This data transformer is used to translate the array of tags into a comma separated format
|
||||
* that can be displayed and managed by Bootstrap-tagsinput js plugin (and back on submit).
|
||||
*
|
||||
* See https://symfony.com/doc/current/form/data_transformers.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
* @author Jonathan Boyer <contact@grafikart.fr>
|
||||
*/
|
||||
class TagArrayToStringTransformer implements DataTransformerInterface
|
||||
{
|
||||
private $tags;
|
||||
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function transform($tags): string
|
||||
{
|
||||
// The value received is an array of Tag objects generated with
|
||||
// Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::transform()
|
||||
// The value returned is a string that concatenates the string representation of those objects
|
||||
|
||||
/* @var Tag[] $tags */
|
||||
return implode(',', $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function reverseTransform($string): array
|
||||
{
|
||||
if ('' === $string || null === $string) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$names = array_filter(array_unique(array_map('trim', explode(',', $string))));
|
||||
|
||||
// Get the current tags and find the new ones that should be created.
|
||||
$tags = $this->tags->findBy([
|
||||
'name' => $names,
|
||||
]);
|
||||
$newNames = array_diff($names, $tags);
|
||||
foreach ($newNames as $name) {
|
||||
$tag = new Tag();
|
||||
$tag->setName($name);
|
||||
$tags[] = $tag;
|
||||
|
||||
// There's no need to persist these new tags because Doctrine does that automatically
|
||||
// thanks to the cascade={"persist"} option in the Bolt\Entity\Post::$tags property.
|
||||
}
|
||||
|
||||
// Return an array of tags to transform them back into a Doctrine Collection.
|
||||
// See Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer::reverseTransform()
|
||||
return $tags;
|
||||
}
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form;
|
||||
|
||||
use Bolt\Entity\Post;
|
||||
use Bolt\Form\Type\DateTimePickerType;
|
||||
use Bolt\Form\Type\TagsInputType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the form used to create and manipulate blog posts.
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class PostType extends AbstractType
|
||||
{
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
// For the full reference of options defined by each form field type
|
||||
// see https://symfony.com/doc/current/reference/forms/types.html
|
||||
|
||||
// By default, form fields include the 'required' attribute, which enables
|
||||
// the client-side form validation. This means that you can't test the
|
||||
// server-side validation errors from the browser. To temporarily disable
|
||||
// this validation, set the 'required' attribute to 'false':
|
||||
// $builder->add('title', null, ['required' => false, ...]);
|
||||
|
||||
$builder
|
||||
->add('title', null, [
|
||||
'attr' => ['autofocus' => true],
|
||||
'label' => 'label.title',
|
||||
])
|
||||
->add('summary', TextareaType::class, [
|
||||
'help' => 'Summaries can\'t contain Markdown or HTML contents; only plain text.',
|
||||
'label' => 'label.summary',
|
||||
])
|
||||
->add('content', null, [
|
||||
'attr' => ['rows' => 20],
|
||||
'help' => 'Use Markdown to format the blog post contents. HTML is allowed too.',
|
||||
'label' => 'label.content',
|
||||
])
|
||||
->add('publishedAt', DateTimePickerType::class, [
|
||||
'label' => 'label.published_at',
|
||||
'help' => 'Set the date in the future to schedule the blog post publication.',
|
||||
])
|
||||
->add('tags', TagsInputType::class, [
|
||||
'label' => 'label.tags',
|
||||
'required' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'data_class' => Post::class,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form\Type;
|
||||
|
||||
use Bolt\Utils\MomentFormatConverter;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\DateTimeType;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
* Defines the custom form field type used to manipulate datetime values across
|
||||
* Bootstrap Date\Time Picker javascript plugin.
|
||||
*
|
||||
* See https://symfony.com/doc/current/cookbook/form/create_custom_field_type.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class DateTimePickerType extends AbstractType
|
||||
{
|
||||
private $formatConverter;
|
||||
|
||||
public function __construct(MomentFormatConverter $converter)
|
||||
{
|
||||
$this->formatConverter = $converter;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['attr']['data-date-format'] = $this->formatConverter->convert($options['format']);
|
||||
$view->vars['attr']['data-date-locale'] = mb_strtolower(str_replace('_', '-', \Locale::getDefault()));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'widget' => 'single_text',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return DateTimeType::class;
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Form\Type;
|
||||
|
||||
use Bolt\Form\DataTransformer\TagArrayToStringTransformer;
|
||||
use Bolt\Repository\TagRepository;
|
||||
use Symfony\Bridge\Doctrine\Form\DataTransformer\CollectionToArrayTransformer;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
|
||||
/**
|
||||
* Defines the custom form field type used to manipulate tags values across
|
||||
* Bootstrap-tagsinput javascript plugin.
|
||||
*
|
||||
* See https://symfony.com/doc/current/cookbook/form/create_custom_field_type.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class TagsInputType extends AbstractType
|
||||
{
|
||||
private $tags;
|
||||
|
||||
public function __construct(TagRepository $tags)
|
||||
{
|
||||
$this->tags = $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder
|
||||
// The Tag collection must be transformed into a comma separated string.
|
||||
// We could create a custom transformer to do Collection <-> string in one step,
|
||||
// but here we're doing the transformation in two steps (Collection <-> array <-> string)
|
||||
// and reuse the existing CollectionToArrayTransformer.
|
||||
->addModelTransformer(new CollectionToArrayTransformer(), true)
|
||||
->addModelTransformer(new TagArrayToStringTransformer($this->tags), true)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildView(FormView $view, FormInterface $form, array $options)
|
||||
{
|
||||
$view->vars['tags'] = $this->tags->findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getParent()
|
||||
{
|
||||
return TextType::class;
|
||||
}
|
||||
}
|
||||
@@ -1,106 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Repository;
|
||||
|
||||
use Bolt\Entity\Post;
|
||||
use Bolt\Entity\Tag;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||
use Doctrine\ORM\Query;
|
||||
use Pagerfanta\Adapter\DoctrineORMAdapter;
|
||||
use Pagerfanta\Pagerfanta;
|
||||
|
||||
/**
|
||||
* This custom Doctrine repository contains some methods which are useful when
|
||||
* querying for blog post information.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine/repository.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class PostRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Post::class);
|
||||
}
|
||||
|
||||
public function findLatest(int $page = 1, Tag $tag = null): Pagerfanta
|
||||
{
|
||||
$qb = $this->createQueryBuilder('p')
|
||||
->addSelect('a', 't')
|
||||
->innerJoin('p.author', 'a')
|
||||
->leftJoin('p.tags', 't')
|
||||
->where('p.publishedAt <= :now')
|
||||
->orderBy('p.publishedAt', 'DESC')
|
||||
->setParameter('now', new \DateTime());
|
||||
|
||||
if (null !== $tag) {
|
||||
$qb->andWhere(':tag MEMBER OF p.tags')
|
||||
->setParameter('tag', $tag);
|
||||
}
|
||||
|
||||
return $this->createPaginator($qb->getQuery(), $page);
|
||||
}
|
||||
|
||||
private function createPaginator(Query $query, int $page): Pagerfanta
|
||||
{
|
||||
$paginator = new Pagerfanta(new DoctrineORMAdapter($query));
|
||||
$paginator->setMaxPerPage(Post::NUM_ITEMS);
|
||||
$paginator->setCurrentPage($page);
|
||||
|
||||
return $paginator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Post[]
|
||||
*/
|
||||
public function findBySearchQuery(string $rawQuery, int $limit = Post::NUM_ITEMS): array
|
||||
{
|
||||
$query = $this->sanitizeSearchQuery($rawQuery);
|
||||
$searchTerms = $this->extractSearchTerms($query);
|
||||
|
||||
if (0 === \count($searchTerms)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$queryBuilder = $this->createQueryBuilder('p');
|
||||
|
||||
foreach ($searchTerms as $key => $term) {
|
||||
$queryBuilder
|
||||
->orWhere('p.title LIKE :t_' . $key)
|
||||
->setParameter('t_' . $key, '%' . $term . '%')
|
||||
;
|
||||
}
|
||||
|
||||
return $queryBuilder
|
||||
->orderBy('p.publishedAt', 'DESC')
|
||||
->setMaxResults($limit)
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all non-alphanumeric characters except whitespaces.
|
||||
*/
|
||||
private function sanitizeSearchQuery(string $query): string
|
||||
{
|
||||
return trim(preg_replace('/[[:space:]]+/', ' ', $query));
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits the search query into terms and removes the ones which are irrelevant.
|
||||
*/
|
||||
private function extractSearchTerms(string $searchQuery): array
|
||||
{
|
||||
$terms = array_unique(explode(' ', $searchQuery));
|
||||
|
||||
return array_filter($terms, function ($term) {
|
||||
return 2 <= mb_strlen($term);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Repository;
|
||||
|
||||
use Bolt\Entity\Tag;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* This custom Doctrine repository is empty because so far we don't need any custom
|
||||
* method to query for application user information. But it's always a good practice
|
||||
* to define a custom repository that will be used when the application grows.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine/repository.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class TagRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Tag::class);
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,6 @@ use Bolt\Entity\User;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\Common\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* This custom Doctrine repository is empty because so far we don't need any custom
|
||||
* method to query for application user information. But it's always a good practice
|
||||
* to define a custom repository that will be used when the application grows.
|
||||
*
|
||||
* See https://symfony.com/doc/current/doctrine/repository.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
*/
|
||||
class UserRepository extends ServiceEntityRepository
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Security;
|
||||
|
||||
use Bolt\Entity\Post;
|
||||
use Bolt\Entity\User;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* It grants or denies permissions for actions related to blog posts (such as
|
||||
* showing, editing and deleting posts).
|
||||
*
|
||||
* See https://symfony.com/doc/current/security/voters.html
|
||||
*
|
||||
* @author Yonel Ceruto <yonelceruto@gmail.com>
|
||||
*/
|
||||
class PostVoter extends Voter
|
||||
{
|
||||
// Defining these constants is overkill for this simple application, but for real
|
||||
// applications, it's a recommended practice to avoid relying on "magic strings"
|
||||
private const SHOW = 'show';
|
||||
private const EDIT = 'edit';
|
||||
private const DELETE = 'delete';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function supports($attribute, $subject): bool
|
||||
{
|
||||
// this voter is only executed for three specific permissions on Post objects
|
||||
return $subject instanceof Post && \in_array($attribute, [self::SHOW, self::EDIT, self::DELETE], true);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function voteOnAttribute($attribute, $post, TokenInterface $token): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
// the user must be logged in; if not, deny permission
|
||||
if (!$user instanceof User) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// the logic of this voter is pretty simple: if the logged user is the
|
||||
// author of the given blog post, grant permission; otherwise, deny it.
|
||||
// (the supports() method guarantees that $post is a Post object)
|
||||
return $user === $post->getAuthor();
|
||||
}
|
||||
}
|
||||
@@ -11,16 +11,6 @@ use Twig\Extension\AbstractExtension;
|
||||
use Twig\TwigFilter;
|
||||
use Twig\TwigFunction;
|
||||
|
||||
/**
|
||||
* This Twig extension adds a new 'md2html' filter to easily transform Markdown
|
||||
* contents into HTML contents inside Twig templates.
|
||||
*
|
||||
* See https://symfony.com/doc/current/cookbook/templating/twig_extension.html
|
||||
*
|
||||
* @author Ryan Weaver <weaverryan@gmail.com>
|
||||
* @author Javier Eguiluz <javier.eguiluz@gmail.com>
|
||||
* @author Julien ITARD <julienitard@gmail.com>
|
||||
*/
|
||||
class AppExtension extends AbstractExtension
|
||||
{
|
||||
private $parser;
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Command;
|
||||
|
||||
use Bolt\Command\AddUserCommand;
|
||||
use Bolt\Entity\User;
|
||||
use Bolt\Utils\Validator;
|
||||
use Doctrine\Bundle\DoctrineBundle\Registry;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
class AddUserCommandTest extends KernelTestCase
|
||||
{
|
||||
private $userData = [
|
||||
'username' => 'chuck_norris',
|
||||
'password' => 'foobar',
|
||||
'email' => 'chuck@norris.com',
|
||||
'full-name' => 'Chuck Norris',
|
||||
];
|
||||
|
||||
protected function setUp()
|
||||
{
|
||||
exec('stty 2>&1', $output, $exitcode);
|
||||
$isSttySupported = 0 === $exitcode;
|
||||
|
||||
if ('Windows' === \PHP_OS_FAMILY || !$isSttySupported) {
|
||||
$this->markTestSkipped('`stty` is required to test this command.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider isAdminDataProvider
|
||||
*
|
||||
* This test provides all the arguments required by the command, so the
|
||||
* command runs non-interactively and it won't ask for any argument.
|
||||
*/
|
||||
public function testCreateUserNonInteractive(bool $isAdmin)
|
||||
{
|
||||
$input = $this->userData;
|
||||
if ($isAdmin) {
|
||||
$input['--admin'] = 1;
|
||||
}
|
||||
$this->executeCommand($input);
|
||||
|
||||
$this->assertUserCreated($isAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider isAdminDataProvider
|
||||
*
|
||||
* This test doesn't provide all the arguments required by the command, so
|
||||
* the command runs interactively and it will ask for the value of the missing
|
||||
* arguments.
|
||||
* See https://symfony.com/doc/current/components/console/helpers/questionhelper.html#testing-a-command-that-expects-input
|
||||
*/
|
||||
public function testCreateUserInteractive(bool $isAdmin)
|
||||
{
|
||||
$this->executeCommand(
|
||||
// these are the arguments (only 1 is passed, the rest are missing)
|
||||
$isAdmin ? ['--admin' => 1] : [],
|
||||
// these are the responses given to the questions asked by the command
|
||||
// to get the value of the missing required arguments
|
||||
array_values($this->userData)
|
||||
);
|
||||
|
||||
$this->assertUserCreated($isAdmin);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to execute the same test twice: first for normal users
|
||||
* (isAdmin = false) and then for admin users (isAdmin = true).
|
||||
*/
|
||||
public function isAdminDataProvider()
|
||||
{
|
||||
yield [false];
|
||||
yield [true];
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper method checks that the user was correctly created and saved
|
||||
* in the database.
|
||||
*/
|
||||
private function assertUserCreated(bool $isAdmin)
|
||||
{
|
||||
$container = self::$kernel->getContainer();
|
||||
|
||||
/** @var User $user */
|
||||
$user = $container->get('doctrine')->getRepository(User::class)->findOneByEmail($this->userData['email']);
|
||||
$this->assertNotNull($user);
|
||||
|
||||
$this->assertSame($this->userData['full-name'], $user->getFullName());
|
||||
$this->assertSame($this->userData['username'], $user->getUsername());
|
||||
$this->assertTrue($container->get('security.password_encoder')->isPasswordValid($user, $this->userData['password']));
|
||||
$this->assertSame($isAdmin ? ['ROLE_ADMIN'] : ['ROLE_USER'], $user->getRoles());
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper method abstracts the boilerplate code needed to test the
|
||||
* execution of a command.
|
||||
*
|
||||
* @param array $arguments All the arguments passed when executing the command
|
||||
* @param array $inputs The (optional) answers given to the command when it asks for the value of the missing arguments
|
||||
*/
|
||||
private function executeCommand(array $arguments, array $inputs = [])
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
$container = self::$kernel->getContainer();
|
||||
/** @var Registry $doctrine */
|
||||
$doctrine = $container->get('doctrine');
|
||||
$command = new AddUserCommand($doctrine->getManager(), $container->get('security.password_encoder'), new Validator(), $doctrine->getRepository(User::class));
|
||||
$command->setApplication(new Application(self::$kernel));
|
||||
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs($inputs);
|
||||
$commandTester->execute($arguments);
|
||||
}
|
||||
}
|
||||
@@ -1,166 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Controller\Admin;
|
||||
|
||||
use Bolt\Entity\Post;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Functional test for the controllers defined inside the BlogController used
|
||||
* for managing the blog in the backend.
|
||||
*
|
||||
* See https://symfony.com/doc/current/book/testing.html#functional-tests
|
||||
*
|
||||
* Whenever you test resources protected by a firewall, consider using the
|
||||
* technique explained in:
|
||||
* https://symfony.com/doc/current/cookbook/testing/http_authentication.html
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ ./vendor/bin/phpunit
|
||||
*/
|
||||
class BlogControllerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getUrlsForRegularUsers
|
||||
*/
|
||||
public function testAccessDeniedForRegularUsers(string $httpMethod, string $url)
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'john_user',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
|
||||
$client->request($httpMethod, $url);
|
||||
$this->assertSame(Response::HTTP_FORBIDDEN, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
public function getUrlsForRegularUsers()
|
||||
{
|
||||
yield ['GET', '/en/admin/post/'];
|
||||
yield ['GET', '/en/admin/post/1'];
|
||||
yield ['GET', '/en/admin/post/1/edit'];
|
||||
yield ['POST', '/en/admin/post/1/delete'];
|
||||
}
|
||||
|
||||
public function testAdminBackendHomePage()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'jane_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
|
||||
$crawler = $client->request('GET', '/en/admin/post/');
|
||||
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
|
||||
|
||||
$this->assertGreaterThanOrEqual(
|
||||
1,
|
||||
$crawler->filter('body#admin_post_index #main tbody tr')->count(),
|
||||
'The backend homepage displays all the available posts.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test changes the database contents by creating a new blog post. However,
|
||||
* thanks to the DAMADoctrineTestBundle and its PHPUnit listener, all changes
|
||||
* to the database are rolled back when this test completes. This means that
|
||||
* all the application tests begin with the same database contents.
|
||||
*/
|
||||
public function testAdminNewPost()
|
||||
{
|
||||
$postTitle = 'Blog Post Title ' . mt_rand();
|
||||
$postSummary = $this->generateRandomString(255);
|
||||
$postContent = $this->generateRandomString(1024);
|
||||
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'jane_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
$crawler = $client->request('GET', '/en/admin/post/new');
|
||||
$form = $crawler->selectButton('Create post')->form([
|
||||
'post[title]' => $postTitle,
|
||||
'post[summary]' => $postSummary,
|
||||
'post[content]' => $postContent,
|
||||
]);
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode());
|
||||
|
||||
$post = $client->getContainer()->get('doctrine')->getRepository(Post::class)->findOneBy([
|
||||
'title' => $postTitle,
|
||||
]);
|
||||
$this->assertNotNull($post);
|
||||
$this->assertSame($postSummary, $post->getSummary());
|
||||
$this->assertSame($postContent, $post->getContent());
|
||||
}
|
||||
|
||||
public function testAdminShowPost()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'jane_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
$client->request('GET', '/en/admin/post/1');
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test changes the database contents by editing a blog post. However,
|
||||
* thanks to the DAMADoctrineTestBundle and its PHPUnit listener, all changes
|
||||
* to the database are rolled back when this test completes. This means that
|
||||
* all the application tests begin with the same database contents.
|
||||
*/
|
||||
public function testAdminEditPost()
|
||||
{
|
||||
$newBlogPostTitle = 'Blog Post Title ' . mt_rand();
|
||||
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'jane_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
$crawler = $client->request('GET', '/en/admin/post/1/edit');
|
||||
$form = $crawler->selectButton('Save changes')->form([
|
||||
'post[title]' => $newBlogPostTitle,
|
||||
]);
|
||||
$client->submit($form);
|
||||
|
||||
$this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode());
|
||||
|
||||
/** @var Post $post */
|
||||
$post = $client->getContainer()->get('doctrine')->getRepository(Post::class)->find(1);
|
||||
$this->assertSame($newBlogPostTitle, $post->getTitle());
|
||||
}
|
||||
|
||||
/**
|
||||
* This test changes the database contents by deleting a blog post. However,
|
||||
* thanks to the DAMADoctrineTestBundle and its PHPUnit listener, all changes
|
||||
* to the database are rolled back when this test completes. This means that
|
||||
* all the application tests begin with the same database contents.
|
||||
*/
|
||||
public function testAdminDeletePost()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'jane_admin',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
$crawler = $client->request('GET', '/en/admin/post/1');
|
||||
$client->submit($crawler->filter('#delete-form')->form());
|
||||
|
||||
$this->assertSame(Response::HTTP_FOUND, $client->getResponse()->getStatusCode());
|
||||
|
||||
$post = $client->getContainer()->get('doctrine')->getRepository(Post::class)->find(1);
|
||||
$this->assertNull($post);
|
||||
}
|
||||
|
||||
private function generateRandomString(int $length): string
|
||||
{
|
||||
$chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||
|
||||
return mb_substr(str_shuffle(str_repeat($chars, ceil($length / mb_strlen($chars)))), 1, $length);
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Controller;
|
||||
|
||||
use Bolt\Entity\Post;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
|
||||
/**
|
||||
* Functional test for the controllers defined inside BlogController.
|
||||
*
|
||||
* See https://symfony.com/doc/current/book/testing.html#functional-tests
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ ./vendor/bin/phpunit
|
||||
*/
|
||||
class BlogControllerTest extends WebTestCase
|
||||
{
|
||||
public function testIndex()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$crawler = $client->request('GET', '/en/blog/');
|
||||
|
||||
$this->assertCount(
|
||||
Post::NUM_ITEMS,
|
||||
$crawler->filter('article.post'),
|
||||
'The homepage displays the right number of posts.'
|
||||
);
|
||||
}
|
||||
|
||||
public function testRss()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$crawler = $client->request('GET', '/en/blog/rss.xml');
|
||||
|
||||
$this->assertSame(
|
||||
'text/xml; charset=UTF-8',
|
||||
$client->getResponse()->headers->get('Content-Type')
|
||||
);
|
||||
|
||||
$this->assertCount(
|
||||
Post::NUM_ITEMS,
|
||||
$crawler->filter('item'),
|
||||
'The xml file displays the right number of posts.'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* This test changes the database contents by creating a new comment. However,
|
||||
* thanks to the DAMADoctrineTestBundle and its PHPUnit listener, all changes
|
||||
* to the database are rolled back when this test completes. This means that
|
||||
* all the application tests begin with the same database contents.
|
||||
*/
|
||||
public function testNewComment()
|
||||
{
|
||||
$client = static::createClient([], [
|
||||
'PHP_AUTH_USER' => 'john_user',
|
||||
'PHP_AUTH_PW' => 'kitten',
|
||||
]);
|
||||
$client->followRedirects();
|
||||
|
||||
// Find first blog post
|
||||
$crawler = $client->request('GET', '/en/blog/');
|
||||
$postLink = $crawler->filter('article.post > h2 a')->link();
|
||||
|
||||
$crawler = $client->click($postLink);
|
||||
|
||||
$form = $crawler->selectButton('Publish comment')->form([
|
||||
'comment[content]' => 'Hi, Symfony!',
|
||||
]);
|
||||
$crawler = $client->submit($form);
|
||||
|
||||
$newComment = $crawler->filter('.post-comment')->first()->filter('div > p')->text();
|
||||
|
||||
$this->assertSame('Hi, Symfony!', $newComment);
|
||||
}
|
||||
|
||||
public function testAjaxSearch()
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->xmlHttpRequest('GET', '/en/blog/search', ['q' => 'lorem']);
|
||||
|
||||
$results = json_decode($client->getResponse()->getContent(), true);
|
||||
|
||||
$this->assertSame('application/json', $client->getResponse()->headers->get('Content-Type'));
|
||||
$this->assertCount(1, $results);
|
||||
$this->assertSame('Lorem ipsum dolor sit amet consectetur adipiscing elit', $results[0]['title']);
|
||||
$this->assertSame('Jane Doe', $results[0]['author']);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Controller;
|
||||
|
||||
use Bolt\Entity\Post;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
|
||||
/**
|
||||
* Functional test that implements a "smoke test" of all the public and secure
|
||||
* URLs of the application.
|
||||
* See https://symfony.com/doc/current/best_practices/tests.html#functional-tests.
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ ./vendor/bin/phpunit
|
||||
*/
|
||||
class DefaultControllerTest extends WebTestCase
|
||||
{
|
||||
/**
|
||||
* PHPUnit's data providers allow to execute the same tests repeated times
|
||||
* using a different set of data each time.
|
||||
* See https://symfony.com/doc/current/cookbook/form/unit_testing.html#testing-against-different-sets-of-data.
|
||||
*
|
||||
* @dataProvider getPublicUrls
|
||||
*/
|
||||
public function testPublicUrls(string $url)
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', $url);
|
||||
|
||||
$this->assertSame(
|
||||
Response::HTTP_OK,
|
||||
$client->getResponse()->getStatusCode(),
|
||||
sprintf('The %s public URL loads correctly.', $url)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* A good practice for tests is to not use the service container, to make
|
||||
* them more robust. However, in this example we must access to the container
|
||||
* to get the entity manager and make a database query. The reason is that
|
||||
* blog post fixtures are randomly generated and there's no guarantee that
|
||||
* a given blog post slug will be available.
|
||||
*/
|
||||
public function testPublicBlogPost()
|
||||
{
|
||||
$client = static::createClient();
|
||||
// the service container is always available via the test client
|
||||
$blogPost = $client->getContainer()->get('doctrine')->getRepository(Post::class)->find(1);
|
||||
$client->request('GET', sprintf('/en/blog/posts/%s', $blogPost->getSlug()));
|
||||
|
||||
$this->assertSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* The application contains a lot of secure URLs which shouldn't be
|
||||
* publicly accessible. This tests ensures that whenever a user tries to
|
||||
* access one of those pages, a redirection to the login form is performed.
|
||||
*
|
||||
* @dataProvider getSecureUrls
|
||||
*/
|
||||
public function testSecureUrls(string $url)
|
||||
{
|
||||
$client = static::createClient();
|
||||
$client->request('GET', $url);
|
||||
|
||||
$response = $client->getResponse();
|
||||
$this->assertSame(Response::HTTP_FOUND, $response->getStatusCode());
|
||||
$this->assertSame(
|
||||
'http://localhost/en/login',
|
||||
$response->getTargetUrl(),
|
||||
sprintf('The %s secure URL redirects to the login form.', $url)
|
||||
);
|
||||
}
|
||||
|
||||
public function getPublicUrls()
|
||||
{
|
||||
yield ['/'];
|
||||
yield ['/en/blog/'];
|
||||
yield ['/en/login'];
|
||||
}
|
||||
|
||||
public function getSecureUrls()
|
||||
{
|
||||
yield ['/en/admin/post/'];
|
||||
yield ['/en/admin/post/new'];
|
||||
yield ['/en/admin/post/1'];
|
||||
yield ['/en/admin/post/1/edit'];
|
||||
}
|
||||
}
|
||||
@@ -1,121 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Form\DataTransformer;
|
||||
|
||||
use Bolt\Entity\Tag;
|
||||
use Bolt\Form\DataTransformer\TagArrayToStringTransformer;
|
||||
use Bolt\Repository\TagRepository;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests that tags are transformed correctly using the data transformer.
|
||||
*
|
||||
* See https://symfony.com/doc/current/testing/database.html
|
||||
*/
|
||||
class TagArrayToStringTransformerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* Ensures that tags are created correctly.
|
||||
*/
|
||||
public function testCreateTheRightAmountOfTags()
|
||||
{
|
||||
$tags = $this->getMockedTransformer()->reverseTransform('Hello, Demo, How');
|
||||
|
||||
$this->assertCount(3, $tags);
|
||||
$this->assertSame('Hello', $tags[0]->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that empty tags and errors in the number of commas are
|
||||
* dealt correctly.
|
||||
*/
|
||||
public function testCreateTheRightAmountOfTagsWithTooManyCommas()
|
||||
{
|
||||
$transformer = $this->getMockedTransformer();
|
||||
|
||||
$this->assertCount(3, $transformer->reverseTransform('Hello, Demo,, How'));
|
||||
$this->assertCount(3, $transformer->reverseTransform('Hello, Demo, How,'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that leading/trailing spaces are ignored for tag names.
|
||||
*/
|
||||
public function testTrimNames()
|
||||
{
|
||||
$tags = $this->getMockedTransformer()->reverseTransform(' Hello ');
|
||||
|
||||
$this->assertSame('Hello', $tags[0]->getName());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that duplicated tag names are ignored.
|
||||
*/
|
||||
public function testDuplicateNames()
|
||||
{
|
||||
$tags = $this->getMockedTransformer()->reverseTransform('Hello, Hello, Hello');
|
||||
|
||||
$this->assertCount(1, $tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the transformer uses tags already persisted in the database.
|
||||
*/
|
||||
public function testUsesAlreadyDefinedTags()
|
||||
{
|
||||
$persistedTags = [
|
||||
$this->createTag('Hello'),
|
||||
$this->createTag('World'),
|
||||
];
|
||||
$tags = $this->getMockedTransformer($persistedTags)->reverseTransform('Hello, World, How, Are, You');
|
||||
|
||||
$this->assertCount(5, $tags);
|
||||
$this->assertSame($persistedTags[0], $tags[0]);
|
||||
$this->assertSame($persistedTags[1], $tags[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensures that the transformation from Tag instances to a simple string
|
||||
* works as expected.
|
||||
*/
|
||||
public function testTransform()
|
||||
{
|
||||
$persistedTags = [
|
||||
$this->createTag('Hello'),
|
||||
$this->createTag('World'),
|
||||
];
|
||||
$transformed = $this->getMockedTransformer()->transform($persistedTags);
|
||||
|
||||
$this->assertSame('Hello,World', $transformed);
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper method mocks the real TagArrayToStringTransformer class to
|
||||
* simplify the tests. See https://phpunit.de/manual/current/en/test-doubles.html.
|
||||
*
|
||||
* @param array $findByReturnValues The values returned when calling to the findBy() method
|
||||
*/
|
||||
private function getMockedTransformer(array $findByReturnValues = []): TagArrayToStringTransformer
|
||||
{
|
||||
$tagRepository = $this->getMockBuilder(TagRepository::class)
|
||||
->disableOriginalConstructor()
|
||||
->getMock();
|
||||
$tagRepository->expects($this->any())
|
||||
->method('findBy')
|
||||
->will($this->returnValue($findByReturnValues));
|
||||
|
||||
return new TagArrayToStringTransformer($tagRepository);
|
||||
}
|
||||
|
||||
/**
|
||||
* This helper method creates a Tag instance for the given tag name.
|
||||
*/
|
||||
private function createTag(string $name): Tag
|
||||
{
|
||||
$tag = new Tag();
|
||||
$tag->setName($name);
|
||||
|
||||
return $tag;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Utils;
|
||||
|
||||
use Bolt\Utils\Slugger;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Unit test for the application utils.
|
||||
*
|
||||
* See https://symfony.com/doc/current/book/testing.html#unit-tests
|
||||
*
|
||||
* Execute the application tests using this command (requires PHPUnit to be installed):
|
||||
*
|
||||
* $ cd your-symfony-project/
|
||||
* $ ./vendor/bin/phpunit
|
||||
*/
|
||||
class SluggerTest extends TestCase
|
||||
{
|
||||
/**
|
||||
* @dataProvider getSlugs
|
||||
*/
|
||||
public function testSlugify(string $string, string $slug)
|
||||
{
|
||||
$this->assertSame($slug, Slugger::slugify($string));
|
||||
}
|
||||
|
||||
public function getSlugs()
|
||||
{
|
||||
yield ['Lorem Ipsum', 'lorem-ipsum'];
|
||||
yield [' Lorem Ipsum ', 'lorem-ipsum'];
|
||||
yield [' lOrEm iPsUm ', 'lorem-ipsum'];
|
||||
yield ['!Lorem Ipsum!', '!lorem-ipsum!'];
|
||||
yield ['lorem-ipsum', 'lorem-ipsum'];
|
||||
yield ['lorem 日本語 ipsum', 'lorem-日本語-ipsum'];
|
||||
yield ['lorem русский язык ipsum', 'lorem-русский-язык-ipsum'];
|
||||
yield ['lorem العَرَبِيَّة ipsum', 'lorem-العَرَبِيَّة-ipsum'];
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Bolt\Tests\Utils;
|
||||
|
||||
use Bolt\Utils\Validator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ValidatorTest extends TestCase
|
||||
{
|
||||
private $object;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
parent::__construct();
|
||||
|
||||
$this->object = new Validator();
|
||||
}
|
||||
|
||||
public function testValidateUsername()
|
||||
{
|
||||
$test = 'username';
|
||||
|
||||
$this->assertSame($test, $this->object->validateUsername($test));
|
||||
}
|
||||
|
||||
public function testValidateUsernameEmpty()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The username can not be empty.');
|
||||
$this->object->validateUsername(null);
|
||||
}
|
||||
|
||||
public function testValidateUsernameInvalid()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The username must contain only lowercase latin characters and underscores.');
|
||||
$this->object->validateUsername('INVALID');
|
||||
}
|
||||
|
||||
public function testValidatePassword()
|
||||
{
|
||||
$test = 'password';
|
||||
|
||||
$this->assertSame($test, $this->object->validatePassword($test));
|
||||
}
|
||||
|
||||
public function testValidatePasswordEmpty()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The password can not be empty.');
|
||||
$this->object->validatePassword(null);
|
||||
}
|
||||
|
||||
public function testValidatePasswordInvalid()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The password must be at least 6 characters long.');
|
||||
$this->object->validatePassword('12345');
|
||||
}
|
||||
|
||||
public function testValidateEmail()
|
||||
{
|
||||
$test = '@';
|
||||
|
||||
$this->assertSame($test, $this->object->validateEmail($test));
|
||||
}
|
||||
|
||||
public function testValidateEmailEmpty()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The email can not be empty.');
|
||||
$this->object->validateEmail(null);
|
||||
}
|
||||
|
||||
public function testValidateEmailInvalid()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The email should look like a real email.');
|
||||
$this->object->validateEmail('invalid');
|
||||
}
|
||||
|
||||
public function testValidateFullName()
|
||||
{
|
||||
$test = 'Full Name';
|
||||
|
||||
$this->assertSame($test, $this->object->validateFullName($test));
|
||||
}
|
||||
|
||||
public function testValidateFullNameEmpty()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('The full name can not be empty.');
|
||||
$this->object->validateFullName(null);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user