Spring cleaning

This commit is contained in:
Bob den Otter
2018-09-29 08:58:44 +02:00
parent ff2d867c7f
commit bbfb401b33
29 changed files with 12 additions and 2031 deletions

View File

@@ -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

View File

@@ -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: []

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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
{
/**

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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';
}

View File

@@ -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;

View File

@@ -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,
]);
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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;
}
}

View File

@@ -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,
]);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
});
}
}

View File

@@ -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);
}
}

View File

@@ -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)

View File

@@ -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();
}
}

View File

@@ -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;

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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']);
}
}

View File

@@ -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'];
}
}

View File

@@ -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;
}
}

View File

@@ -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'];
}
}

View File

@@ -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);
}
}