<?php

namespace PHPDish\Bundle\PostBundle\Controller;

use Carbon\Carbon;
use Doctrine\Common\Collections\Criteria;
use PHPDish\Bundle\CmsBundle\Utility\StringManipulator;
use PHPDish\Bundle\ResourceBundle\Controller\ResourceController;
use PHPDish\Bundle\PostBundle\Event\Events;
use PHPDish\Bundle\PostBundle\Event\PostEvent;
use PHPDish\Bundle\PostBundle\Form\Type\CommentType;
use PHPDish\Bundle\PostBundle\Form\Type\PostType;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use PHPDish\Bundle\UserBundle\Controller\ManagerTrait as UserManagerTrait;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Component\Translation\TranslatorInterface;

class PostController extends ResourceController
{
    use ManagerTrait;
    use UserManagerTrait;

    /**
     * @Route("/posts", name="post")
     *
     * @param Request $request
     *
     * @return Response
     */
    public function indexAction(Request $request)
    {
        $posts = $this->getPostManager()->findLatestEnabledPosts($request->query->getInt('page', 1));

        return $this->render($this->configuration->getTemplate('Post:index.html.twig'), [
            'posts' => $posts,
        ]);
    }

    /**
     * 创建文章.
     *
     * @Route("/posts/new", name="post_add")
     *
     * @param Request $request
     *
     * @return Response
     */
    public function createAction(Request $request)
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        /**@var TranslatorInterface*/
        $translator = $this->get('translator');
        //如果用户没有专栏，则跳转专栏创建页面
        if ($this->getCategoryManager()->getUserCategoriesNumber($this->getUser()) === 0) {
            $this->addFlash('warning', $translator->trans('post.add_category_first'));
            return $this->redirectToRoute('category_add');
        }

        $manager = $this->getPostManager();
        $post = $manager->createPost($this->getUser());
        $form = $this->createForm(PostType::class, $post, [
            'user' => $this->getUser(),
        ]);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            if ($manager->savePost($post)) {
                //触发事件
                $this->get('event_dispatcher')->dispatch(Events::POST_CREATED, new PostEvent($post));

                return $this->redirectToRoute('post_view', [
                    'id' => $post->getId(),
                ]);
            } else {
                $this->addFlash('error', $translator->trans('post.cannot_add'));
            }
        }

        return $this->render($this->configuration->getTemplate('Post:create.html.twig'), [
            'form' => $form->createView(),
        ]);
    }

    /**
     * 查看文章.
     *
     * @Route("/posts/{id}", name="post_view", requirements={"id": "\d+"}, methods={"GET"})
     *
     * @param int    $id
     * @param Request $request
     *
     * @return Response
     */
    public function viewAction($id, Request $request)
    {
        $post = $this->getPostManager()->findPostById($id);
        if (!$post->isEnabled()) {
            throw $this->createNotFoundException();
        }
        //如果是电子书则跳转到阅读页面
        if ($post->getCategory()->isBook()) {
            return $this->redirectToRoute('book_read', [
                'slug' => $post->getCategory()->getSlug(),
                'chapterId' => $post->getId()
            ], 301);
        }
        $form = $this->createForm(CommentType::class);
        $criteria = Criteria::create()->where(Criteria::expr()->eq('post', $post->getId()));
        $comments = $this->getPostCommentManager()->findCommentsPager($criteria, $request->query->getInt('page', 1));

        $this->getPostManager()->increasePostViews($post);

        //SEO
        $seoPage = $this->get('sonata.seo.page');
        $summary = StringManipulator::stripLineBreak($post->getSummary());
        $seoPage
            ->setTitle($post->getTitle())
            ->removeMeta('name', 'keywords')
            ->addMeta('name', 'description', $summary)
            ->addMeta('property', 'og:title', $post->getTitle())
            ->addMeta('property', 'og:type', 'article')
            ->addMeta('property', 'og:url',  $this->generateUrl('post_view', ['id' => $post->getId()], UrlGeneratorInterface::ABSOLUTE_URL))
            ->addMeta('property', 'og:description', $summary);

        return $this->render($this->configuration->getTemplate('Post:view.html.twig'), [
            'post' => $post,
            'comments' => $comments,
            'form' => $form->createView(),
        ]);
    }

    /**
     * 修改文章.
     *
     * @Route("/post/{id}/edit", name="post_edit", requirements={"id": "\d+"})
     *
     * @param int     $id
     * @param Request $request
     *
     * @return Response
     */
    public function editAction($id, Request $request)
    {
        $post = $this->getPostManager()->findPostById($id);
        if (!$post) {
            throw $this->createNotFoundException();
        }
        $this->denyAccessUnlessGranted('edit', $post);
        $form = $this->createForm(PostType::class, $post, [
            'user' => $this->getUser(),
        ]);
        $form->handleRequest($request);
        if ($form->isValid() && $form->isSubmitted()) {
            $this->getPostManager()->savePost($post);

            return $this->redirectToRoute('post_view', [
                'id' => $post->getId(),
            ]);
        }

        return $this->render($this->configuration->getTemplate('Post:create.html.twig'), [
            'form' => $form->createView(),
            'post' => $post,
        ]);
    }

    /**
     * 删除文章.
     *
     * @Route("/posts/{id}", name="post_delete", requirements={"id": "\d+"}, methods={"DELETE"})
     *
     * @param int $id
     *
     * @return Response
     */
    public function deleteAction($id)
    {
        $manager = $this->getPostManager();
        $post = $manager->findPostById($id);
        if (!$post) {
            throw $this->createNotFoundException();
        }
        $this->denyAccessUnlessGranted('edit', $post);
        $manager->blockPost($post);

        return $this->handleView($this->view([
            'result' => true,
        ]));
    }

    /**
     * 用户文章.
     *
     * @Route("/users/{username}/posts", name="user_posts")
     *
     * @param string  $username
     * @param Request $request
     *
     * @return Response
     */
    public function getUserPostsAction($username, Request $request)
    {
        $user = $this->getUserManager()->findUserByName($username);
        $posts = $this->getPostManager()->findUserEnabledPosts($user, $request->query->getInt('page', 1));

        return $this->render($this->configuration->getTemplate('Post:user_posts.html.twig'), [
            'user' => $user,
            'posts' => $posts,
        ]);
    }

    /**
     * 推荐的文章.
     *
     * @param int $limit
     *
     * @return Response
     */
    public function recommendPostsAction($limit)
    {
        $criteria = Criteria::create()->orderBy(['isRecommended' => 'desc', 'createdAt' => 'desc'])
            ->andWhere(Criteria::expr()->gte('createdAt', Carbon::parse('-100 days')))
            ->setMaxResults($limit);
        $posts = $this->getPostManager()->findPosts($criteria);

        return $this->render($this->configuration->getTemplate('Post:recommend_posts.html.twig'), [
            'posts' => $posts,
        ]);
    }

    /**
     * 切换点赞状态
     *
     * @Route("/posts/{id}/voters", name="post_toggle_voter", methods={"POST"})
     */
    public function toggleVoterAction($id)
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');

        $post = $this->getPostManager()->findPostById($id);
        if (!$post) {
            throw new \InvalidArgumentException($this->get('translator')->trans('post.not_exists'));
        }
        if ($isVoted = $post->isVotedBy($this->getUser())) {
            $this->getPostManager()->removeVoter($post, $this->getUser());
        } else {
            $this->getPostManager()->addVoter($post, $this->getUser());
        }
        return $this->json([
            'vote_count' => $post->getVoteCount(),
            'is_voted' => !$isVoted
        ]);
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CoreBundle\Application;

use PHPDish\Bundle\CoreBundle\Application\Plugin\Finder\CachedPluginFinder;
use PHPDish\Bundle\CoreBundle\Application\Plugin\Finder\PluginFinder;
use PHPDish\Bundle\CoreBundle\Application\Plugin\SimplePluginInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel as HttpKernel;

abstract class Kernel extends HttpKernel
{
    /**
     * @var SimplePluginInterface[]
     */
    protected $simplePlugins = [];

    /**
     * {@inheritdoc}
     */
    public function registerBundles()
    {
        $bundles = [
            new \Symfony\Bundle\FrameworkBundle\FrameworkBundle(),
            new \Symfony\Bundle\SecurityBundle\SecurityBundle(),
            new \Symfony\Bundle\TwigBundle\TwigBundle(),
            new \Symfony\Bundle\MonologBundle\MonologBundle(),
            new \Symfony\Bundle\SwiftmailerBundle\SwiftmailerBundle(),
            new \Doctrine\Bundle\DoctrineBundle\DoctrineBundle(),
            new \Sensio\Bundle\FrameworkExtraBundle\SensioFrameworkExtraBundle(),
            new \Symfony\Bundle\AsseticBundle\AsseticBundle(),
            new \Doctrine\Bundle\MigrationsBundle\DoctrineMigrationsBundle(),
            new \Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle(),
            new \Stof\DoctrineExtensionsBundle\StofDoctrineExtensionsBundle(),

            new \JMS\SerializerBundle\JMSSerializerBundle(),
            new \FOS\UserBundle\FOSUserBundle(),
            new \FOS\RestBundle\FOSRestBundle(),
            new \FOS\MessageBundle\FOSMessageBundle(),

            new \Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),
            new \Knp\Bundle\MarkdownBundle\KnpMarkdownBundle(),
            new \Knp\Bundle\GaufretteBundle\KnpGaufretteBundle(),
            new \Knp\Bundle\MenuBundle\KnpMenuBundle(),
            new \WhiteOctober\PagerfantaBundle\WhiteOctoberPagerfantaBundle(),
            new \Liip\ImagineBundle\LiipImagineBundle(),
            new \Http\HttplugBundle\HttplugBundle(),
            new \HWI\Bundle\OAuthBundle\HWIOAuthBundle(),
            new \Algolia\SearchBundle\AlgoliaSearchBundle(),
            new \Exercise\HTMLPurifierBundle\ExerciseHTMLPurifierBundle(),
//            new \Sylius\Bundle\ThemeBundle\SyliusThemeBundle(),
            new \Gregwar\CaptchaBundle\GregwarCaptchaBundle(),
            new \Bazinga\Bundle\JsTranslationBundle\BazingaJsTranslationBundle(),
            new \Oneup\UploaderBundle\OneupUploaderBundle(), //多文件上传

            //Sonata
            new \Sonata\SeoBundle\SonataSeoBundle(),
            new \Sonata\CoreBundle\SonataCoreBundle(),
            new \Sonata\BlockBundle\SonataBlockBundle(),

            //Data grid
//            new \APY\DataGridBundle\APYDataGridBundle(),

            //PHPDish bundles
            new \PHPDish\Bundle\ResourceBundle\PHPDishResourceBundle(),
            new \PHPDish\Bundle\ThemeBundle\PHPDishThemeBundle(),
            new \PHPDish\Bundle\CmsBundle\PHPDishCmsBundle(),
            new \PHPDish\Bundle\UserBundle\PHPDishUserBundle(),
            new \PHPDish\Bundle\PostBundle\PHPDishPostBundle(),
            new \PHPDish\Bundle\ForumBundle\PHPDishForumBundle(),
            new \PHPDish\Bundle\MediaBundle\PHPDishMediaBundle(),
            new \PHPDish\Bundle\NotificationBundle\PHPDishNotificationBundle(),
            new \PHPDish\Bundle\ChatBundle\PHPDishChatBundle(),
            new \PHPDish\Bundle\ResumeBundle\PHPDishResumeBundle(),
            new \PHPDish\Bundle\PaymentBundle\PHPDishPaymentBundle(),
            new \PHPDish\Bundle\PermissionBundle\PHPDishPermissionBundle(), //权限管理
            new \PHPDish\Bundle\CoreBundle\PHPDishCoreBundle(),
        ];

        if (in_array($this->getEnvironment(), ['dev', 'test'], true)) {
            $bundles[] = new \Symfony\Bundle\DebugBundle\DebugBundle();
            $bundles[] = new \Symfony\Bundle\WebProfilerBundle\WebProfilerBundle();
            $bundles[] = new \Sensio\Bundle\DistributionBundle\SensioDistributionBundle();

            if ('dev' === $this->getEnvironment()) {
                $bundles[] = new \Sensio\Bundle\GeneratorBundle\SensioGeneratorBundle();
                $bundles[] = new \Symfony\Bundle\WebServerBundle\WebServerBundle();
            }
        }

        //动态查找
        $this->simplePlugins = $this->findSimplePlugins();
        $bundles = array_merge($bundles, $this->simplePlugins);
        return $bundles;
    }

    /**
     * {@inheritdoc}
     */
    protected function getKernelParameters()
    {
        $simplePlugins = [];
        $simplePluginMetas = [];
        foreach ($this->simplePlugins as $simplePlugin) {
            $simplePlugins[$simplePlugin->getName()] = get_class($simplePlugin);
            $simplePluginMetas[$simplePlugin->getName()] = [
                'path' => $simplePlugin->getPath(),
                'routerSource' => $simplePlugin->getRouterResource(),
                'servicesSource' => $simplePlugin->getServicesSource(),
            ];
        }
        return array_merge(parent::getKernelParameters(), [
            'kernel.simple_plugins' => $simplePlugins,
            'kernel.simple_plugins_metadata' => $simplePluginMetas
        ]);
    }

    /**
     * 获取全部的插件
     *
     * @return SimplePluginInterface[]
     */
    public function getSimplePlugins()
    {
        return $this->simplePlugins;
    }

    /**
     * 初始化插件
     */
    protected function findSimplePlugins()
    {
        $finder = new CachedPluginFinder($this->getCacheDir() . '/phpdish_plugins.php',
            new PluginFinder($this->getProjectDir())
        );
        return $finder->findAll();
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\AdminBundle\Grid\Filter;

class RangeFilter extends AbstractFilter
{
    protected $from;

    protected $to;

    public function __construct($from, $to)
    {
        $this->from = $from;
        $this->to = $to;
    }

    /**
     * {@inheritdoc}
     */
    public function getOperator()
    {
        return 'between';
    }

    /**
     * {@inheritdoc}
     */
    public function getComparison()
    {
        return $this->column->getName() . ' BETWEEN ' . $this->from . ' AND ' . $this->to;
    }

    /**
     * {@inheritdoc}
     */
    public function shouldSkip()
    {
        return $this->from === null && $this->to === null;
    }

    /**
     * @return mixed
     */
    public function getFrom()
    {
        return $this->from;
    }

    /**
     * @param mixed $from
     */
    public function setFrom($from)
    {
        $this->from = $from;
    }

    /**
     * @return mixed
     */
    public function getTo()
    {
        return $this->to;
    }

    /**
     * @param mixed $to
     */
    public function setTo($to)
    {
        $this->to = $to;
    }

    /**
     * {@inheritdoc}
     */
    public function initialize($data)
    {
        isset($data['from']) && $this->from = $data['from'];
        isset($data['to']) && $this->from = $data['to'];
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ResourceBundle\EventListener;

use Doctrine\Common\EventSubscriber;
use Doctrine\Common\Persistence\Mapping\RuntimeReflectionService;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\Event\LoadClassMetadataEventArgs;
use Doctrine\ORM\Events;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataInfo;
use PHPDish\Bundle\ResourceBundle\Metadata\ResourceRegistry;
use PHPDish\Bundle\ResourceBundle\Model\IdentifiableInterface;

final class ORMMappedSuperClassSubscriber implements EventSubscriber
{
    /**
     * @var ResourceRegistry
     */
    protected $resourceRegistry;

    /**
     * @var RuntimeReflectionService
     */
    private $reflectionService;

    public function __construct(ResourceRegistry $resourceRegistry)
    {
        $this->resourceRegistry = $resourceRegistry;
    }

    /**
     * @return array
     */
    public function getSubscribedEvents(): array
    {
        return [
            Events::loadClassMetadata,
        ];
    }

    /**
     * @param LoadClassMetadataEventArgs $eventArgs
     */
    public function loadClassMetadata(LoadClassMetadataEventArgs $eventArgs): void
    {
        $metadata = $eventArgs->getClassMetadata();

        $this->convertToEntityIfNeeded($metadata);

        if (!$metadata->isMappedSuperclass) {
            $this->setAssociationMappings($metadata, $eventArgs->getEntityManager()->getConfiguration());
        } else {
            $this->unsetAssociationMappings($metadata);
        }
        if ($metadata->getName() === 'PHPDish\Bundle\UserBundle\Model\User') {
        }

    }

    /**
     * @param ClassMetadataInfo $metadata
     */
    private function convertToEntityIfNeeded(ClassMetadataInfo $metadata): void
    {
        if (false === $metadata->isMappedSuperclass) {
            return;
        }

        $resourceMetadata = $this->resourceRegistry->getResourceItemByModel($metadata->getName());

        if ($resourceMetadata === null) {
            return;
        }

        if ($metadata->getName() === $resourceMetadata['model']) {
            $metadata->isMappedSuperclass = false;
        }
    }

    /**
     * @param ClassMetadataInfo $metadata
     * @param Configuration $configuration
     */
    private function setAssociationMappings(ClassMetadataInfo $metadata, Configuration $configuration): void
    {
        $class = $metadata->getName();
        if (!class_exists($class)) {
            return;
        }
        foreach (class_parents($class) as $parent) {
            if (false === in_array($parent, $configuration->getMetadataDriverImpl()->getAllClassNames(), true)) {
                continue;
            }

            $parentMetadata = new ClassMetadata(
                $parent,
                $configuration->getNamingStrategy()
            );

            // Wakeup Reflection
            $parentMetadata->wakeupReflection($this->getReflectionService());

            // Load Metadata
            $configuration->getMetadataDriverImpl()->loadMetadataForClass($parent, $parentMetadata);

            if (false === $this->isResource($parentMetadata)) {
                continue;
            }

            if ($parentMetadata->isMappedSuperclass) {
                foreach ($parentMetadata->getAssociationMappings() as $key => $value) {
                    if ($this->isRelation($value['type']) && !isset($metadata->associationMappings[$key])) {
                        $metadata->associationMappings[$key] = $value;
                    }
                }
            }
        }
    }

    /**
     * @param ClassMetadata $metadata
     */
    private function unsetAssociationMappings(ClassMetadata $metadata): void
    {
        if (false === $this->isResource($metadata)) {
            return;
        }

        foreach ($metadata->getAssociationMappings() as $key => $value) {
            if ($this->isRelation($value['type'])) {
                unset($metadata->associationMappings[$key]);
            }
        }
    }

    /**
     * @param int $type
     *
     * @return bool
     */
    private function isRelation(int $type): bool
    {
        return in_array(
            $type,
            [
                ClassMetadataInfo::MANY_TO_MANY,
                ClassMetadataInfo::ONE_TO_MANY,
                ClassMetadataInfo::ONE_TO_ONE,
            ],
            true
        );
    }

    /**
     * @param ClassMetadata $metadata
     *
     * @return bool
     */
    protected function isResource(ClassMetadata $metadata): bool
    {
        if (!$reflectionClass = $metadata->getReflectionClass()) {
            return false;
        }

        return $reflectionClass->implementsInterface(IdentifiableInterface::class);
    }

    protected function getReflectionService()
    {
        if ($this->reflectionService === null) {
            $this->reflectionService = new RuntimeReflectionService();
        }

        return $this->reflectionService;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

declare(strict_types=1);

namespace PHPDish\Component\Media\Manager;

use GuzzleHttp\Client as HttpClient;
use PHPDish\Component\Media\ExtensionFinder;
use Psr\Http\Message\ResponseInterface;
use Symfony\Component\HttpFoundation\File\MimeType\ExtensionGuesser;

class FileDownloader implements FileDownloaderInterface
{
    /**
     * @var FileFactoryInterface
     */
    protected $fileFactory;

    /**
     * @var FileManagerInterface
     */
    protected $fileManager;

    /**
     * @var HttpClient
     */
    protected $httpClient;

    public function __construct(
        HttpClient $httpClient,
        FileManagerInterface $fileManager,
        FileFactoryInterface $fileFactory
    ) {
        $this->httpClient = $httpClient;
        $this->fileManager = $fileManager;
        $this->fileFactory = $fileFactory;
    }

    /**
     * {@inheritdoc}
     */
    public function download($mediaUrl)
    {
        try {
            $response = $this->httpClient->get($mediaUrl);
        } catch(\Exception $exception) {
            throw new \RuntimeException(sprintf('Fail to donwload the resource "%s"', $mediaUrl));
        }
        //查找扩展名
        $extension = $this->resolveExtension($mediaUrl, $response);

        $file = $this->fileFactory->createFileWithExtension($extension);
        $file->setContent($response->getBody());
        $this->fileManager->upload($file);

        return $file;
    }

    protected function resolveExtension($mediaUrl, ResponseInterface $response)
    {
        $extension = ExtensionFinder::find($mediaUrl);
        if ($extension === false) {
            $contentType = $response->getHeaderLine('content-type');
            $extension = $contentType ? ExtensionGuesser::getInstance()->guess($contentType) : 'jpg';
        }
        return $extension;
    }
}


<?php

namespace PHPDish\Bundle\UserBundle\Form\Type;

use PHPDish\Bundle\CoreBundle\Locale\LocaleManager;
use PHPDish\Bundle\UserBundle\Model\User;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;

class ChangeUserProfileType extends AbstractType
{
    /**
     * @var LocaleManager
     */
    protected $localeManager;

    public function __construct(LocaleManager $localeManager)
    {
        $this->localeManager = $localeManager;
    }

    /**
     * {@inheritdoc}
     */
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $supportLocales = $this->localeManager->all();
        $builder
            ->add('username', TextType::class, [
                'label' => 'form.user.username',
            ])
            ->add('gender', ChoiceType::class, [
                'choices' => [
                     'form.user.gender.male' => User::GENDER_MEN,
                     'form.user.gender.female' => User::GENDER_WOMEN,
                ],
                'placeholder' => 'form.user.gender.placeholder',
                'label' => 'form.user.gender.gender',
            ])
            ->add('email', TextType::class, [
                'label' => 'form.user.email',
            ])
            ->add('description', TextareaType::class, [
                'label' => 'form.user.description',
            ])
            ->add('profile', ProfileType::class)
            ->add('avatar', HiddenType::class, [
                'label' => 'form.user.avatar',
            ])
            ->add('locale', ChoiceType::class, [
                'choices' => $this->wrapLocaleChoices(),
                'label' => 'form.user.locale.locale',
            ]);
    }

    /**
     * @return array
     */
    protected function wrapLocaleChoices()
    {
        $choices = [];
        foreach ($this->localeManager->all() as $locale) {
            $choices['form.user.locale.' . $locale] = $locale;
        }
        return $choices;
    }

    /**
     * {@inheritdoc}
     */
    public function configureOptions(OptionsResolver $resolver)
    {
        $resolver->setDefaults([
            'data_class' => User::class,
            'validation_groups' => ['Profile']
        ]);
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CmsBundle\Model;

use PHPDish\Bundle\CmsBundle\Utility\MarkdownHelper;
use PHPDish\Bundle\ResourceBundle\Model\DateTimeTrait;
use PHPDish\Bundle\ResourceBundle\Model\EnabledTrait;
use PHPDish\Bundle\UserBundle\Model\UserAwareTrait;
use PHPDish\Bundle\UserBundle\Model\UserInterface;

class AbstractPost implements PostInterface
{
    use UserAwareTrait, ContentTrait,
        CommentableTrait, DateTimeTrait, EnabledTrait, VotableTrait;

    /**
     * @var string
     */
    protected $title;

    /**
     * @var int
     */
    protected $commentCount = 0;

    /**
     * @var int
     */
    protected $viewCount = 0;

    /**
     * 文章插图.
     * @var array
     */
    protected $images;

    /**
     * @var \DateTimeInterface
     */
    protected $lastCommentAt;

    /**
     * @var UserInterface
     */
    protected $lastCommentUser;

    /**
     * 评论
     * @var CommentInterface[]
     */
    protected $comments;

    public function __toString()
    {
        return $this->getTitle();
    }

    /**
     * {@inheritdoc}
     */
    public function setTitle($title)
    {
        $this->title = $title;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getTitle()
    {
        return $this->title;
    }

    /**
     * {@inheritdoc}
     */
    public function setViewCount($viewCount)
    {
        $this->viewCount = $viewCount;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function addViewCount($viewCount)
    {
        $this->viewCount += $viewCount;
        $this->viewCount < 0 && $this->viewCount = 0;
        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getViewCount()
    {
        return $this->viewCount;
    }

    /**
     * {@inheritdoc}
     */
    public function setCommentCount($commentCount)
    {
        $this->commentCount = $commentCount;

        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getCommentCount()
    {
        return $this->commentCount;
    }

    /**
     * {@inheritdoc}
     */
    public function addCommentCount($count = 1)
    {
        $this->commentCount += $count;

        return $this;
    }

    /**
     * Gets the summary of the post.
     *
     * @return string
     */
    public function getSummary()
    {
        return strip_tags(mb_substr($this->body, 0, 250));
    }

    /**
     * {@inheritdoc}
     */
    public function getWordCount()
    {
        return mb_strlen($this->getBody(), 'UTF-8');
    }

    /**
     * 检查文章是否是属于指定用户.
     *
     * @param UserInterface $user
     *
     * @return bool
     */
    public function isBelongsTo(UserInterface $user)
    {
        return $this->getUser() === $user;
    }

    /**
     * {@inheritdoc}
     */
    public function getImages()
    {
        if (!is_null($this->images)) {
            return $this->images;
        }

        return $this->images = MarkdownHelper::extractImages($this->getOriginalBody());
    }

    /**
     * {@inheritdoc}
     */
    public function getLastCommentAt()
    {
        return $this->lastCommentAt;
    }

    /**
     * {@inheritdoc}
     */
    public function getLastCommentUser()
    {
        return $this->lastCommentUser;
    }

    /**
     * @param \DateTimeInterface $lastCommentAt
     * @return self
     */
    public function setLastCommentAt(\DateTimeInterface $lastCommentAt)
    {
        $this->lastCommentAt = $lastCommentAt;
        return $this;
    }

    /**
     * @param UserInterface $lastCommentUser
     * @return self
     */
    public function setLastCommentUser(UserInterface $lastCommentUser)
    {
        $this->lastCommentUser = $lastCommentUser;
        return $this;
    }

    /**
     * {@inheritdoc}
     */
    public function getComments()
    {
        return $this->comments;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ChatBundle\Message;

use FOS\MessageBundle\Provider\Provider as BaseProvider;
use Pagerfanta\Pagerfanta;
use PHPDish\Bundle\CoreBundle\Service\PaginatorTrait;

class Provider extends BaseProvider
{
    use PaginatorTrait;

    /**
     * 获取已读消息
     * @param int $page
     * @param null $limit
     * @return Pagerfanta
     */
    public function getInboxThreadsPager($page, $limit = null)
    {
        $participant = $this->getAuthenticatedParticipant();

        $query = $this->threadManager
            ->getParticipantInboxThreadsQueryBuilder($participant)
            ->getQuery();
        return $this->createPaginator($query, $page, $limit);
    }

    /**
     * 获取已发送消息
     *
     * @param int $page
     * @param null $limit
     * @return Pagerfanta
     */
    public function getSentThreadsPager($page, $limit = null)
    {
        $participant = $this->getAuthenticatedParticipant();

        $query = $this->threadManager
            ->getParticipantSentThreadsQueryBuilder($participant)
            ->getQuery();

        return $this->createPaginator($query, $page, $limit);
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ForumBundle\Twig;

use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use PHPDish\Bundle\ForumBundle\Model\ReplyInterface;
use PHPDish\Bundle\ForumBundle\Service\ReplyManagerInterface;
use PHPDish\Bundle\ForumBundle\Service\ThreadManagerInterface;
use PHPDish\Bundle\ForumBundle\Service\TopicManagerInterface;

class TopicExtension extends \Twig_Extension
{
    protected $threadManager;

    /**
     * @var TopicManagerInterface
     */
    protected $topicManager;

    /**
     * @var ReplyManagerInterface
     */
    protected $replyManager;

    public function __construct(
        ThreadManagerInterface $threadManager,
        TopicManagerInterface $topicManager,
        ReplyManagerInterface $replyManager
    )
    {
        $this->threadManager = $threadManager;
        $this->topicManager = $topicManager;
        $this->replyManager = $replyManager;
    }

    /**
     * {@inheritdoc}
     */
    public function getFunctions(): array
    {
        return [
            new \Twig_SimpleFunction('get_thread', [$this->threadManager, 'findThreadById']),
            new \Twig_SimpleFunction('get_thread_by_slug', [$this->threadManager, 'findThreadBySlug']),
            new \Twig_SimpleFunction('get_threads', [$this, 'getThreads']),
            new \Twig_SimpleFunction('get_threads_pager', [$this->threadManager, 'findThreadsPager']),

            new \Twig_SimpleFunction('get_topic', [$this->topicManager, 'findTopicById']),
            new \Twig_SimpleFunction('get_topics', [$this, 'getTopics']),
            new \Twig_SimpleFunction('get_topics_pager', [$this->topicManager, 'findTopicsPager']),
            new \Twig_SimpleFunction('get_topics_pager_by_thread', [$this->topicManager, 'findThreadTopics']),

            new \Twig_SimpleFunction('get_reply', [$this->replyManager, 'findReplyById']),
            new \Twig_SimpleFunction('get_replies', [$this, 'getReplies']),
            new \Twig_SimpleFunction('get_replies_pager', [$this->replyManager, 'findRepliesPager']),
        ];
    }

    /**
     * 指定条件查找节点
     *
     * @param array| $criteria
     * @param array $orderBy
     * @param int $limit
     * @return array
     */
    public function getThreads($criteria, array $orderBy = [], ?int $limit = null)
    {
        if ($criteria instanceof Criteria) {
            return $this->threadManager->findThreads($criteria);
        } else {
            return $this->threadManager->getThreadRepository()->findBy($criteria, $orderBy, $limit);
        }
    }

    /**
     * 指定条件查找话题
     *
     * @param array| $criteria
     * @param array $orderBy
     * @param int $limit
     * @return array
     */
    public function getTopics($criteria, array $orderBy = [], ?int $limit = null)
    {
        if ($criteria instanceof Criteria) {
            return $this->topicManager->findTopics($criteria);
        } else {
            return $this->topicManager->getTopicRepository()->findBy($criteria, $orderBy, $limit);
        }
    }

    /**
     * 查找回复
     *
     * @param array|Criteria $criteria
     * @param array $orderBy
     * @param int|null $limit
     * @return ReplyInterface[]|Collection
     */
    public function getReplies($criteria, array $orderBy = [], ?int $limit = null)
    {
        if ($criteria instanceof Criteria) {
            return $this->replyManager->findReplies($criteria);
        } else {
            return $this->replyManager->getReplyRepository()->findBy($criteria, $orderBy, $limit);
        }
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

declare(strict_types=1);

namespace PHPDish\Component\Media\Manager;

use Symfony\Component\HttpFoundation\File\UploadedFile;

class FileUploader implements FileUploaderInterface
{
    /**
     * @var FileFactoryInterface
     */
    protected $fileFactory;

    /**
     * @var FileManagerInterface
     */
    protected $fileManager;

    public function __construct(FileFactoryInterface $fileFactory, FileManagerInterface $fileManager)
    {
        $this->fileFactory = $fileFactory;
        $this->fileManager = $fileManager;
    }

    /**
     * {@inheritdoc}
     */
    public function upload(UploadedFile $uploadedFile)
    {
        $file = $this->fileFactory->createFileFromUploadedFile($uploadedFile);
        $this->fileManager->upload($file, true);

        return $file;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\PostBundle\Service;

use Carbon\Carbon;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\EntityRepository;
use PHPDish\Bundle\CoreBundle\Service\PaginatorTrait;
use PHPDish\Bundle\PostBundle\Event\CommentMentionUserEvent;
use PHPDish\Bundle\PostBundle\Event\Events;
use PHPDish\Bundle\PostBundle\Event\VoteCommentEvent;
use PHPDish\Bundle\PostBundle\Model\CommentInterface;
use PHPDish\Bundle\PostBundle\Model\PostInterface;
use PHPDish\Bundle\ResourceBundle\Service\ServiceManagerInterface;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use PHPDish\Bundle\CmsBundle\BodyProcessor\BodyProcessorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class CommentManager implements CommentManagerInterface, ServiceManagerInterface
{
    use PaginatorTrait;

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @var EntityRepository
     */
    protected $commentRepository;

    /**
     * @var EventDispatcherInterface
     */
    protected $eventDispatcher;

    /**
     * @var BodyProcessorInterface
     */
    protected $bodyProcessor;

    protected $commentEntity;

    public function __construct(
        $commentEntity,
        EntityManagerInterface $entityManager,
        EventDispatcherInterface $eventDispatcher,
        BodyProcessorInterface $bodyProcessor
    ) {
        $this->commentEntity = $commentEntity;
        $this->entityManager = $entityManager;
        $this->eventDispatcher = $eventDispatcher;
        $this->bodyProcessor = $bodyProcessor;
    }

    /**
     * {@inheritdoc}
     */
    public function findCommentById($id)
    {
        return $this->getCommentRepository()->find($id);
    }

    /**
     * {@inheritdoc}
     */
    public function findCommentsPager(Criteria $criteria, $page = 1, $limit = null)
    {
        $query = $this->getCommentRepository()->createQueryBuilder('c')
            ->join('c.user', 'cu')->addSelect('cu')
            ->addCriteria($criteria)
            ->getQuery();

        return $this->createPaginator($query, $page, $limit);
    }

    /**
     * {@inheritdoc}
     */
    public function findComments(Criteria $criteria)
    {
        return $this->getCommentRepository()->createQueryBuilder('c')
            ->join('c.user', 'cu')->addSelect('cu')
            ->addCriteria($criteria)
            ->getQuery()
            ->getResult();
    }

    /**
     * {@inheritdoc}
     */
    public function createComment(PostInterface $post, UserInterface $user)
    {
        $comment = new $this->commentEntity;
        $comment->setPost($post)->setUser($user);
        $post->addCommentCount();
        return $comment;
    }

    /**
     * {@inheritdoc}
     */
    public function saveComment(CommentInterface $comment)
    {
        $new = !$comment->getId();
        $parsedBody = $this->bodyProcessor->process($comment->getOriginalBody());

        $comment->setUpdatedAt(Carbon::now())
            ->setBody($parsedBody);

        //新增评论，维护post的数据
        if ($new) {
            $comment->getPost()->addCommentCount(1)
                ->setLastCommentAt(Carbon::now())
                ->setLastCommentUser($comment->getUser());
        }

        $this->entityManager->persist($comment);
        $this->entityManager->flush();

        //如果评论中有艾特用户则触发事件
        $mentionParser = $this->bodyProcessor->getMentionParser();
        if ($new && $mentionParser->getMentionedUsers()) {
            $this->eventDispatcher->dispatch(Events::USER_MENTIONED_COMMENT, new CommentMentionUserEvent(
                $comment,
                $mentionParser->getMentionedUsers()
            ));
        }

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function blockComment(CommentInterface $comment)
    {
        $comment->disable();
        $this->saveComment($comment);
    }

    /**
     * {@inheritdoc}
     */
    public function addVoter(CommentInterface $comment, UserInterface $user)
    {
        $comment->addVoter($user)
            ->addVoteCount();
        $this->entityManager->persist($comment);
        $this->entityManager->flush();

        //触发事件
        $this->eventDispatcher->dispatch(Events::COMMENT_VOTED,
            new VoteCommentEvent($comment->getPost(), $comment, $user)
        );
    }

    /**
     * {@inheritdoc}
     */
    public function removeVoter(CommentInterface $comment, UserInterface $user)
    {
        $comment->removeVoter($user)
            ->addVoteCount(-1);
        $this->entityManager->persist($comment);
        $this->entityManager->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function getCommentRepository()
    {
        if ($this->commentRepository) {
            return $this->commentRepository;
        }
        return $this->entityManager->getRepository($this->commentEntity);
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEntities()
    {
        return [
            'commentEntity' => CommentInterface::class
        ];
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

declare(strict_types=1);

namespace PHPDish\Bundle\CoreBundle\Application\Plugin\Routing;

use Symfony\Component\Config\Loader\Loader;
use Symfony\Component\Routing\RouteCollection;

class RoutingLoader extends Loader
{
    /**
     * @var array
     */
    protected $routerResources = [];

    /**
     * {@inheritdoc}
     */
    public function load($resource, $type = null)
    {
        $routes = new RouteCollection();

        foreach ($this->routerResources as $resource) {
            $type = pathinfo($resource, PATHINFO_EXTENSION);
            if ($type === 'yml') {
                $type = 'yaml';
            }
            $importedRoutes = $this->import($resource, $type);
            $routes->addCollection($importedRoutes);
        }

        return $routes;
    }

    public function addResource($resource)
    {
        $this->routerResources[] = $resource;
    }

    /**
     * {@inheritdoc}
     */
    public function supports($resource, $type = null)
    {
        return $type === 'phpdish_plugin';
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\WebBundle\Controller;

use Carbon\Carbon;
use PHPDish\Bundle\PostBundle\Controller\ManagerTrait;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;

class DefaultController extends Controller
{
    use ManagerTrait;

    use \PHPDish\Bundle\ForumBundle\Controller\ManagerTrait;

    /**
     * 首页（暂时不启用）.
     *
     * @param Request $request
     *
     * @return Response
     */
    public function indexAction(Request $request)
    {
        $postManager = $this->getPostManager();
        $posts = $postManager->findLatestPosts($request->query->getInt('page', 1));
        $topics = $this->getTopicManager()->findHotTopics(Carbon::now()->addDays(-10), 15);

        return $this->render('PHPDishWebBundle:Default:index.html.twig', [
            'posts' => $posts,
            'topics' => $topics,
        ]);
    }

    /**
     * 当前用户的通知数量.
     *
     * @Route("/notifications/count", name="notification_count")
     */
    public function countNotificationAction()
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
        $count = $this->get('phpdish_notification.manager.notification')->getNotificationCount($this->getUser(), false);
        $nbMessageCount = $this->get('fos_message.provider')->getNbUnreadMessages();
        return $this->json([
            'count' => $count + $nbMessageCount,
        ]);
    }
}


<?php

namespace PHPDish\Bundle\CoreBundle\Search\Algolia;

use Algolia\SearchBundle\IndexManagerInterface;
use Pagerfanta\Adapter\AdapterInterface;

class AlgoliaAdapter implements AdapterInterface
{
    protected $query;

    protected $entityClass;

    protected $entityManager;

    /**
     * @var IndexManagerInterface
     */
    protected $indexManager;

    protected $options;

    public function __construct($query, $entityClass, $entityManager, $indexManager, array $options)
    {
        $this->query = $query;
        $this->entityClass = $entityClass;
        $this->entityManager = $entityManager;
        $this->indexManager = $indexManager;
        $this->options = $options;
    }

    /**
     * {@inheritdoc}
     */
    public function getNbResults()
    {
        return $this->indexManager->count(
            $this->query,
            $this->entityClass,
            $this->options
        );
    }

    /**
     * {@inheritdoc}
     */
    public function getSlice($offset, $length)
    {
        return array_filter($this->indexManager->search(
            $this->query,
            $this->entityClass,
            $this->entityManager,
            $offset / $length + 1,
            $length,
            $this->options
        ));
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ForumBundle\Controller;

use Doctrine\Common\Collections\Criteria;
use PHPDish\Bundle\ForumBundle\Form\Type\ThreadType;
use PHPDish\Bundle\ResourceBundle\Controller\ResourceController;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class ThreadController extends ResourceController
{
    use ManagerTrait;

    /**
     * 自动完成获取数据
     * @Route("/threads/autocomplete", name="thread_autocomplete")
     * @param Request $request
     * @return Response
     */
    public function autoCompleteAction(Request $request)
    {
        $query = $request->query->get('query');
        $threads = $this->getThreadManager()->searchThreads($query);
        return $this->json([
            'threads' => $threads
        ]);
    }

    /**
     * 热点threads （tag）
     * @return Response
     */
    public function hotThreadsAction()
    {
        $threads = $this->getThreadManager()->findEnabledThreads(15);
        return $this->render($this->configuration->getTemplate('Thread:_hot_threads.html.twig'), [
            'threads' => $threads
        ]);
    }

    /**
     * threads 探索
     *
     * @Route("/threads", name="threads")
     * @param Request $request
     * @return Response
     */
    public function indexAction(Request $request)
    {
        $criteria =  Criteria::create()->where(Criteria::expr()->eq('enabled', true))
            ->orderBy([
                'followerCount' => 'desc',
                'createdAt' => 'desc',
            ]);
        if ($request->query->get('tab') === 'following') {
            $threads = $this->getThreadManager()->findUserFollowingThreads(
                $this->getUser(),
                $request->query->getInt('page', 1),
                null,
                $criteria
            );
        } else {
            $threads = $this->getThreadManager()->findThreadsPager($criteria,
                $request->query->getInt('page', 1)
            );
        }

        $translator = $this->get('translator');
        $seoPage = $this->get('sonata.seo.page');
        $seoPage
            ->setTitle($translator->trans('thread.explore'))
            ->removeMeta('name', 'keywords')
            ->addMeta('name', 'description', $translator->trans('thread.explore'))
            ->addMeta('property', 'og:title', $translator->trans('thread.explore'))
            ->addMeta('property', 'og:url',  $this->generateUrl('threads', [],UrlGeneratorInterface::ABSOLUTE_URL))
            ->addMeta('property', 'og:description', $translator->trans('thread.explore'));

        return $this->render($this->configuration->getTemplate('Thread:index.html.twig'), [
            'threads' => $threads
        ]);
    }

    /**
     * @Route("/threads/{slug}", name="thread_view")
     *
     * @param string  $slug
     * @param Request $request
     *
     * @return Response
     */
    public function viewAction($slug, Request $request)
    {
        $thread = $this->getThreadManager()->findThreadBySlug($slug);
        if (!$thread) {
            throw $this->createNotFoundException();
        }

        $criteria = Criteria::create();
        $criteria->orderBy(['lastCommentAt' => 'desc'])
            ->where(Criteria::expr()->eq('enabled', true));

        $tab = $request->query->get('tab');
        if ($tab && $tab === 'recommend') {
            $criteria->andWhere(Criteria::expr()->eq('recommended', true));
        }

        $topics = $this->getTopicManager()->findThreadTopics($thread, $request->query->getInt('page', 1), null, $criteria);

        //SEO
        $seoPage = $this->get('sonata.seo.page');
        $seoPage
            ->setTitle($thread->getName())
            ->removeMeta('name', 'keywords')
            ->addMeta('name', 'description', $thread->getDescription())
            ->addMeta('property', 'og:title', $thread->getName())
            ->addMeta('property', 'og:url',  $this->generateUrl('thread_view', ['slug' => $thread->getSlug()], UrlGeneratorInterface::ABSOLUTE_URL))
            ->addMeta('property', 'og:description', $thread->getDescription());

        return $this->render($this->configuration->getTemplate('Thread:view.html.twig'), [
            'thread' => $thread,
            'topics' => $topics,
        ]);
    }

    /**
     * @Route("/threads/{slug}/edit", name="thread_edit")
     *
     * @param string  $slug
     * @param Request $request
     *
     * @return Response
     */
    public function editAction($slug, Request $request)
    {
        $thread = $this->getThreadManager()->findThreadBySlug($slug);
        if (!$thread) {
            throw $this->createNotFoundException();
        }
        $this->denyAccessUnlessGranted('edit', $thread);

        $form = $this->createForm(ThreadType::class, $thread);
        $form->handleRequest($request);
        if ($form->isValid() && $form->isSubmitted()) {

            $this->getThreadManager()->saveThread($thread);
            $this->addFlash('success', $this->get('translator')->trans('thread.edit_success'));

            return $this->redirectToRoute('thread_view', [
                'slug' => $thread->getSlug(),
            ]);
        }
        return $this->render($this->configuration->getTemplate('Thread:create.html.twig'), [
            'thread' => $thread,
            'form' => $form->createView()
        ]);
    }

    /**
     * 关注节点.
     *
     * @Route("/threads/{slug}/followers", name="thread_follow")
     * @Method("POST")
     *
     * @param string $slug
     *
     * @return Response
     */
    public function followAction($slug)
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
        $thread = $this->getThreadManager()->findThreadBySlug($slug);
        $this->getThreadManager()->followThread($thread, $this->getUser());
        return $this->json([
            'follower_count' => $thread->getFollowerCount(),
        ]);
    }

    /**
     * 取消关注节点.
     *
     * @Route("/threads/{slug}/followers", name="thread_unfollow")
     * @Method("DELETE")
     *
     * @param string $slug
     *
     * @return Response
     */
    public function unFollowAction($slug)
    {
        $this->denyAccessUnlessGranted('IS_AUTHENTICATED_REMEMBERED');
        $thread = $this->getThreadManager()->findThreadBySlug($slug);
        $this->getThreadManager()->unFollowThread($thread, $this->getUser());
        return $this->json([
            'follower_count' => $thread->getFollowerCount(),
        ]);
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\PermissionBundle\DependencyInjection;

use PHPDish\Bundle\PermissionBundle\Model\Permission;
use PHPDish\Bundle\PermissionBundle\Model\PermissionInterface;
use PHPDish\Bundle\PermissionBundle\Model\PrivilegerInterface;
use PHPDish\Bundle\PermissionBundle\Model\Role;
use PHPDish\Bundle\PermissionBundle\Model\RoleInterface;
use PHPDish\Bundle\ResourceBundle\DependencyInjection\AbstractConfiguration;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration extends AbstractConfiguration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('phpdish_permission');

        $rootNode
            ->children()
                ->arrayNode('resources')
                    ->children()
                        ->arrayNode('privileger')
                            ->children()
                                ->scalarNode('interface')->defaultValue(PrivilegerInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                        ->arrayNode('role')
                            ->children()
                                ->scalarNode('interface')->defaultValue(RoleInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Role::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                        ->arrayNode('permission')
                            ->children()
                                ->scalarNode('interface')->defaultValue(PermissionInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Permission::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end();

        return $treeBuilder;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\UserBundle\DependencyInjection\Compiler;

use PHPDish\Bundle\UserBundle\Controller\ResettingController;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

class FOSCompatiblePass implements CompilerPassInterface
{
    /**
     * {@inheritdoc}
     */
    public function process(ContainerBuilder $container)
    {
        if ($changePassword = $container->findDefinition('fos_user.resetting.controller')) {
            $changePassword->setClass(ResettingController::class)
                ->addMethodCall('setResourceConfiguration', [
                    new Reference('phpdish_resource.configuration.phpdish_user')
                ]);
        }
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\PostBundle\Service;

use Carbon\Carbon;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use PHPDish\Bundle\CoreBundle\Service\PaginatorTrait;
use PHPDish\Bundle\PaymentBundle\Model\PaymentInterface;
use PHPDish\Bundle\PaymentBundle\Service\PaymentManagerInterface;
use PHPDish\Bundle\PaymentBundle\Service\WalletManagerInterface;
use PHPDish\Bundle\PostBundle\Event\CategoryFollowedEvent;
use PHPDish\Bundle\PostBundle\Event\CategoryPersistEvent;
use PHPDish\Bundle\PostBundle\Event\Events;
use PHPDish\Bundle\PostBundle\Model\CategoryInterface;
use PHPDish\Bundle\ResourceBundle\Service\ServiceManagerInterface;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\Routing\Router;
use Symfony\Component\Translation\TranslatorInterface;

class CategoryManager implements CategoryManagerInterface, ServiceManagerInterface
{
    use PaginatorTrait;

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @var EventDispatcherInterface
     */
    protected $eventDispatcher;

    /**
     * @var Router
     */
    protected $router;

    /**
     * @var TranslatorInterface
     */
    protected $translator;

    /**
     * @var PaymentManagerInterface
     */
    protected $paymentManager;

    /**
     * @var WalletManagerInterface
     */
    protected $walletManager;

    protected $categoryEntity;

    public function __construct(
        $categoryEntity,
        EntityManagerInterface $entityManager,
        EventDispatcherInterface $eventDispatcher,
        Router $router,
        TranslatorInterface $translator,
        PaymentManagerInterface $paymentManager,
        WalletManagerInterface $walletManager
    )
    {
        $this->categoryEntity = $categoryEntity;
        $this->entityManager = $entityManager;
        $this->eventDispatcher = $eventDispatcher;
        $this->router = $router;
        $this->translator = $translator;
        $this->paymentManager = $paymentManager;
        $this->walletManager = $walletManager;
    }

    /**
     * {@inheritdoc}
     */
    public function findCategories(Criteria $criteria)
    {
        return $this->getCategoryRepository()->createQueryBuilder('c')
            ->addCriteria($criteria)
            ->getQuery()
            ->getResult();
    }

    /**
     * {@inheritdoc}
     */
    public function findCategoriesPager(Criteria $criteria, $page, $limit = null)
    {
        $query = $this->getCategoryRepository()->createQueryBuilder('c')
            ->addCriteria($criteria)
            ->getQuery();
        return $this->createPaginator($query, $page, $limit);
    }

    /**
     * {@inheritdoc}
     */
    public function findAllEnabledCategories()
    {
        $criteria = Criteria::create()->where(Criteria::expr()->eq('enabled', true));
        return $this->findCategories($criteria);
    }

    /**
     * {@inheritdoc}
     */
    public function findUserCategories(UserInterface $user)
    {
        return $this->createGetUserCategoriesQueryBuilder($user)
            ->andWhere('c.isBook = :isBook')->setParameter('isBook', false)
            ->getQuery()
            ->getResult();
    }

    /**
     * {@inheritdoc}
     */
    public function getUserCategoriesNumber(UserInterface $user)
    {
        $qb = $this->createGetUserCategoriesQueryBuilder($user)
            ->andWhere('c.isBook = :isBook')->setParameter('isBook', false);

        return (int)$qb->select($qb->expr()->count('c'))
            ->getQuery()
            ->getSingleScalarResult();
    }

    /**
     * 创建用户专栏查询的query builder.
     *
     * @param UserInterface $user
     *
     * @return \Doctrine\ORM\QueryBuilder
     */
    public function createGetUserCategoriesQueryBuilder(UserInterface $user)
    {
        return $this->getCategoryRepository()->createQueryBuilder('c')
            ->where('c.creator = :userId')->setParameter('userId', $user->getId())
            ->orderBy('c.createdAt', 'desc');
    }

    /**
     * {@inheritdoc}
     */
    public function findCategoryBySlug($slug)
    {
        return $this->getCategoryRepository()->findOneBy([
            'slug' => $slug,
        ]);
    }

    /**
     * {@inheritdoc}
     */
    public function findCategoryById($id)
    {
        return $this->getCategoryRepository()->find($id);
    }

    /**
     * {@inheritdoc}
     */
    public function addManagerForCategory(CategoryInterface $category, UserInterface $user)
    {
        $category->addManager($user);

        return $this->saveCategory($category);
    }

    /**
     * {@inheritdoc}
     */
    public function addCategoryIncome(UserInterface $user, CategoryInterface $category, UserInterface $follower, $amount = null)
    {
        $wallet = $this->walletManager->getUserWallet($user);
        $history = $this->walletManager->createHistory();

        if ($category->isBook()) {
            $description = $this->translator->trans('payment.buy_your_book', [
                '%username%' => sprintf('%s',
                    $this->router->generate('user_view', ['username' => $follower->getUsername()]),
                    $follower->getUsername()
                ),
                '%book%' => sprintf('《%s》',
                    $this->router->generate('book_view', ['slug' => $category->getSlug()]),
                    $category->getName()
                ),
            ]);

        } else {
            $description = $this->translator->trans('payment.subscribe_your_category', [
                '%username%' => sprintf('%s',
                    $this->router->generate('user_view', ['username' => $follower->getUsername()]),
                    $follower->getUsername()
                ),
                '%category%' => sprintf('《%s》',
                    $this->router->generate('category_view', ['slug' => $category->getSlug()]),
                    $category->getName()
                ),
            ]);
        }

        //设置历史
        $history->setAmount($amount ?: $category->getCharge())
            ->setType($category->isBook() ? PaymentInterface::TYPE_BOOK_INCOME : PaymentInterface::TYPE_CATEGORY_INCOME)
            ->setDescription($description)
            ->setStatus(PaymentInterface::STATUS_OK)
            ->setUser($wallet->getUser());

        $this->walletManager->addHistory($wallet, $history);
    }

    /**
     * {@inheritdoc}
     */
    public function payForCategory(CategoryInterface $category, UserInterface $user)
    {
        if ($category->isCharging()) {
            //发起付费
            $message = $category->isBook() ?
                $this->translator->trans('book.buy_book', [
                    '%book%' => sprintf('%s',
                        $this->router->generate('book_view', ['slug' => $category->getSlug()]),
                        $category->getName()
                    )
                ]):
                $this->translator->trans('category.subscribe_category', [
                    '%category%' => sprintf('%s',
                        $this->router->generate('category_view', ['slug' => $category->getSlug()]),
                        $category->getName()
                    )
                ]);

            //创建交易
            $payment = $this->paymentManager->createPayment($user)
                ->setAmount($category->getCharge())
                ->setDescription($message)
                ->setType($category->isBook() ? PaymentInterface::TYPE_BUY_BOOK
                    : PaymentInterface::TYPE_FOLLOW_CATEGORY)
                ->setPayableId($category->getId());

            $qrCode = $this->paymentManager->charge($payment);
        } else {
            throw new \LogicException($this->translator->trans('category.is_free'));
        }
        return $qrCode;
    }

    /**
     * {@inheritdoc}
     */
    public function followCategory(CategoryInterface $category, UserInterface $user)
    {
        $category->addFollower($user);
        $category->setFollowerCount($category->getFollowerCount() + 1);
        $result = $this->saveCategory($category);
        //触发订阅事件
        $this->eventDispatcher->dispatch(Events::CATEGORY_FOLLOWED, new CategoryFollowedEvent($category, $user));

        return $result;
    }

    /**
     * {@inheritdoc}
     */
    public function unFollowCategory(CategoryInterface $category, UserInterface $user)
    {
        $category->removeFollower($user);
        $category->setFollowerCount($category->getFollowerCount() - 1 ?: 0);

        return $this->saveCategory($category);
    }

    /**
     * {@inheritdoc}
     */
    public function createCategory(UserInterface $user)
    {
        $category = new $this->categoryEntity;
        $category->setCreator($user)
            ->setCreatedAt(Carbon::now());

        return $category;
    }

    /**
     * {@inheritdoc}
     */
    public function saveCategory(CategoryInterface $category)
    {
        $event = new CategoryPersistEvent($category);
        $this->eventDispatcher->dispatch(Events::CATEGORY_PRE_PERSIST, $event);
        if ($event->isPersistenceAborted()) {
            return false;
        }

        $category->setUpdatedAt(Carbon::now());
        $this->entityManager->persist($category);
        $this->entityManager->flush();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function getCategoryRepository()
    {
        return $this->entityManager->getRepository($this->categoryEntity);
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEntities()
    {
        return [
            'categoryEntity' => CategoryInterface::class
        ];
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ForumBundle\Service;

use Carbon\Carbon;
use Doctrine\Common\Collections\Criteria;
use Doctrine\ORM\EntityManagerInterface;
use PHPDish\Bundle\CoreBundle\Service\PaginatorTrait;
use PHPDish\Bundle\ForumBundle\Event\Events;
use PHPDish\Bundle\ForumBundle\Event\VoteTopicEvent;
use PHPDish\Bundle\ForumBundle\Model\ThreadInterface;
use PHPDish\Bundle\ForumBundle\Model\TopicInterface;
use PHPDish\Bundle\ResourceBundle\Service\ServiceManagerInterface;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use PHPDish\Bundle\CmsBundle\BodyProcessor\BodyProcessorInterface;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;

class TopicManager implements TopicManagerInterface, ServiceManagerInterface
{
    use PaginatorTrait;

    /**
     * @var EntityManagerInterface
     */
    protected $entityManager;

    /**
     * @var BodyProcessorInterface
     */
    protected $bodyProcessor;

    /**
     * @var EventDispatcherInterface
     */
    protected $eventDispatcher;

    /**
     * @var string
     */
    protected $topicEntity;

    public function __construct(
        $topicEntity,
        EntityManagerInterface $entityManager,
        EventDispatcherInterface $eventDispatcher,
        BodyProcessorInterface $bodyProcessor
    ) {
        $this->topicEntity = $topicEntity;
        $this->entityManager = $entityManager;
        $this->eventDispatcher = $eventDispatcher;
        $this->bodyProcessor = $bodyProcessor;
    }

    /**
     * {@inheritdoc}
     */
    public function createTopic(UserInterface $user)
    {
        $topic = new $this->topicEntity;
        $now = Carbon::now();
        $topic->setUser($user)
            ->setLastCommentAt($now)
            ->setCreatedAt($now)
            ->setLastCommentUser($user);

        return $topic;
    }

    /**
     * {@inheritdoc}
     */
    public function saveTopic(TopicInterface $topic)
    {
        $parsedBody = $this->bodyProcessor->process($topic->getOriginalBody());
        $topic->setUpdatedAt(Carbon::now())
            ->setBody($parsedBody);

        //给节点增加话题数
        if (!$topic->getId()) {
            foreach ($topic->getThreads() as $thread) {
                $thread->addPostCount(1);
            }
        }
        $this->entityManager->persist($topic);
        $this->entityManager->flush();

        return true;
    }

    /**
     * {@inheritdoc}
     */
    public function blockTopic(TopicInterface $topic)
    {
        $topic->disable();
        $this->entityManager->persist($topic);
        $this->entityManager->flush();
    }

    /**
     * {@inheritdoc}
     */
    public function findTopicById($id)
    {
        return $this->getTopicRepository()->find($id);
    }

    /**
     * {@inheritdoc}
     */
    public function findThreadTopics(ThreadInterface $thread, $page, $limit = null, Criteria $criteria = null)
    {
        $qb = $this->getTopicRepository()->createQueryBuilder('t')
            ->innerJoin('t.threads', 'th')
            ->where('th.id = :threadId')->setParameter('threadId', $thread);
        $criteria && $qb->addCriteria($criteria);

        return $this->createPaginator($qb->getQuery(), $page, $limit);
    }

    /**
     * {@inheritdoc}
     */
    public function findUserTopics(UserInterface $user, $page, $limit = null)
    {
        $criteria = Criteria::create()->where(Criteria::expr()->eq('user', $user->getId()))
            ->orderBy([
                'createdAt' => 'DESC',
            ]);

        return $this->findTopicsPager($criteria, $page, $limit);
    }

    /**
     * {@inheritdoc}
     */
    public function findTopicsPager(Criteria $criteria, $page, $limit = null)
    {
        $query = $this->getTopicRepository()->createQueryBuilder('t')
            ->addCriteria($criteria)
            ->getQuery();

        return $this->createPaginator($query, $page, $limit);
    }

    /**
     * {@inheritdoc}
     */
    public function findTopics(Criteria $criteria)
    {
        return $this->getTopicRepository()->createQueryBuilder('t')
            ->addCriteria($criteria)
            ->getQuery()
            ->getResult();
    }

    /**
     * {@inheritdoc}
     */
    public function findHotTopics(\DateTime $date, $limit)
    {
        $criteria = Criteria::create()->where(Criteria::expr()->gt('createdAt', $date))
            ->andWhere(Criteria::expr()->eq('enabled', true))
            ->orderBy(['commentCount' => 'desc', 'createdAt' => 'desc'])
            ->setMaxResults($limit);

        return $this->findTopics($criteria);
    }

    /**
     * 创建查询关注话题下的帖子query
     * @param UserInterface $user
     * @param Criteria|null $criteria
     * @return \Doctrine\ORM\Query
     * @throws \Doctrine\ORM\Query\QueryException
     */
    public function findFollowingThreadsTopicsQuery(UserInterface $user, Criteria $criteria = null)
    {
        $qb = $this->getTopicRepository()->createQueryBuilder('t')
            ->leftJoin('t.threads', 'tt')
            ->leftJoin('tt.followers', 'f')
            ->where('f.id = :userId')
            ->setParameter('userId', $user)
            ->orderBy('t.createdAt', 'desc');
        if ($criteria) {
            $qb->addCriteria($criteria);
        }
        return $qb->getQuery();
    }

    /**
     * {@inheritdoc}
     */
    public function findFollowingThreadsTopics(UserInterface $user, $page, $limit = null)
    {
        $query = $this->findFollowingThreadsTopicsQuery($user, Criteria::create()
            ->where(Criteria::expr()->eq('enabled', true))
        );
        return $this->createPaginator($query, $page, $limit);
    }

    /**
     * {@inheritdoc}
     */
    public function getUserTopicCount(UserInterface $user)
    {
        $qb = $this->getTopicRepository()->createQueryBuilder('p');
        $qb->select($qb->expr()->count('p'))
            ->where('p.enabled = :enabled')->setParameter('enabled', true)
            ->andWhere('p.user = :user')->setParameter('user', $user);
        return $qb->getQuery()->getSingleScalarResult();
    }

    /**
     * {@inheritdoc}
     */
    public function addVoter(TopicInterface $topic, UserInterface $user)
    {
        $topic->addVoter($user)
            ->addVoteCount();
        $this->saveTopic($topic);

        $event = new VoteTopicEvent($topic, $user);
        $this->eventDispatcher->dispatch(Events::TOPIC_VOTED, $event);

    }

    /**
     * {@inheritdoc}
     */
    public function removeVoter(TopicInterface $topic, UserInterface $user)
    {
        $topic->removeVoter($user)
            ->addVoteCount(-1);
        $this->saveTopic($topic);
    }

    /**
     * {@inheritdoc}
     */
    public function getTopicRepository()
    {
        return $this->entityManager->getRepository($this->topicEntity);
    }

    /**
     * {@inheritdoc}
     */
    public function getMaxPerPage()
    {
        return 20;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEntities()
    {
        return [
            'topicEntity' => TopicInterface::class
        ];
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CoreBundle\EventListener;

use Doctrine\ORM\Event\LifecycleEventArgs;
use PHPDish\Bundle\PostBundle\Model\PostInterface;
use PHPDish\Bundle\PostBundle\Service\PostManagerInterface;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use PHPDish\Bundle\UserBundle\Service\UserManagerInterface;

final class PostListener
{
    /**
     * @var PostManagerInterface
     */
    protected $postManager;

    /**
     * @var UserManagerInterface
     */
    protected $userManager;

    public function __construct(
        PostManagerInterface $postManager,
        UserManagerInterface $userManager
    )
    {
        $this->postManager = $postManager;
        $this->userManager = $userManager;
    }

    /**
     * 生成新数据之前
     * @param PostInterface $post
     * @param LifecycleEventArgs $event
     */
    public function postPersist(PostInterface $post, LifecycleEventArgs $event)
    {
        $user = $post->getUser();
        $this->handleUserPostCount($user);
    }

    /**
     * 处理用户的文章数量
     * @param UserInterface $user
     */
    protected function handleUserPostCount(UserInterface $user)
    {
        $count = $this->postManager->getUserPostCount($user);
        $user->setPostCount($count);
        $this->userManager->saveUser($user);
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\UserBundle\Controller;

use FOS\UserBundle\Controller\ResettingController as FOSResettingController;
use FOS\UserBundle\Event\GetResponseNullableUserEvent;
use FOS\UserBundle\FOSUserEvents;
use PHPDish\Bundle\ResourceBundle\Controller\ResourceConfigurationInterface;
use PHPDish\Bundle\UserBundle\Form\Type\ResettingRequestType;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Translation\TranslatorInterface;

class ResettingController extends FOSResettingController
{
    /**
     * @var ResourceConfigurationInterface
     */
    protected $configuration;

    /**
     * @param ResourceConfigurationInterface $configuration
     */
    public function setResourceConfiguration(ResourceConfigurationInterface $configuration)
    {
        $this->configuration = $configuration;
    }

    /**
     * 上一个用户名
     * @var string
     */
    const LAST_USERNAME_SESSION_KEY = '_resetting_last_username';

    /**
     * 重置密码
     * {@inheritdoc}
     */
    public function requestAction()
    {
        $form = $this->createForm(ResettingRequestType::class, null, [
            'action' => $this->generateUrl('fos_user_resetting_send_email')
        ]);
        return $this->render($this->configuration->getTemplate('Resetting:request.html.twig'), [
            'form' => $form->createView(),
            'lastUsername' => $this->get('session')->get(static::LAST_USERNAME_SESSION_KEY) ?: ''
        ]);
    }

    /**
     * 重置密码
     * {@inheritdoc}
     */
    public function sendEmailAction(Request $request)
    {
        $form = $this->createForm(ResettingRequestType::class);
        $form->handleRequest($request);
        if ($form->isSubmitted() && $form->isValid()) {
            $this->getEventDispatcher()->addListener(FOSUserEvents::RESETTING_SEND_EMAIL_INITIALIZE,
                [$this, 'onSendEmailInitialize']
            );
            return parent::sendEmailAction($request);
        }
        //处理失败调回请求页面
        foreach ($form->getErrors(true) as $error) {
            $this->addFlash('danger', $error->getMessage());
        }
        $this->get('session')->set(static::LAST_USERNAME_SESSION_KEY, $form->getData()['username'] ?? '');
        return $this->redirectToRoute('fos_user_resetting_request');
    }

    /**
     * @return EventDispatcher
     */
    protected function getEventDispatcher()
    {
        return $this->get('event_dispatcher');
    }

    /**
     * 发送邮件初始化
     * @param GetResponseNullableUserEvent $event
     */
    public function onSendEmailInitialize(GetResponseNullableUserEvent $event)
    {
        $response = $this->redirectToRoute('fos_user_resetting_request');
        /** @var TranslatorInterface */
        $translator = $this->get('translator');
        if ($event->getUser() === null) {
            $this->addFlash('danger', $translator->trans('resetting.username_or_email_not_exists'));
            $event->setResponse($response);
        } elseif (!$event->getUser()->getEmail()) {
            $this->addFlash('warning', $translator->trans('resetting.user_missing_email'));
            $event->setResponse($response);
        }
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\NotificationBundle\DependencyInjection;

use PHPDish\Bundle\NotificationBundle\Model\Notification;
use PHPDish\Bundle\NotificationBundle\Model\NotificationInterface;
use PHPDish\Bundle\NotificationBundle\Model\NotificationMetadata;
use PHPDish\Bundle\NotificationBundle\Model\NotificationMetadataInterface;
use PHPDish\Bundle\ResourceBundle\DependencyInjection\AbstractConfiguration;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration extends AbstractConfiguration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('phpdish_notification');

        $rootNode
            ->children()
                ->arrayNode('resources')
                    ->children()
                        ->arrayNode('notification')
                            ->children()
                                ->scalarNode('interface')->defaultValue(NotificationInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Notification::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()

                        ->arrayNode('notification_metadata')
                            ->children()
                                ->scalarNode('interface')->defaultValue(NotificationMetadataInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(NotificationMetadata::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end();

        $this->addTemplatesSection($rootNode);
        return $treeBuilder;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Component\Mention;

class MentionParser implements MentionParserInterface
{
    /**
     * @var AdapterInterface
     */
    protected $adapter;

    /**
     * @var string
     */
    protected $parsedBody;

    /**
     * @var array
     */
    protected $mentionedNames;

    /**
     * @var array
     */
    protected $mentionedUsers;

    public function __construct(AdapterInterface $adapter)
    {
        $this->adapter = $adapter;
    }

    /**
     * {@inheritdoc}
     */
    public function getMentionedNames()
    {
        return $this->mentionedNames;
    }

    /**
     * {@inheritdoc}
     */
    public function getMentionedUsers()
    {
        return $this->mentionedUsers;
    }

    /**
     * {@inheritdoc}
     */
    public function getParsedBody()
    {
        return $this->parsedBody;
    }

    /**
     * {@inheritdoc}
     */
    public function parse($body)
    {
        $this->parsedBody = $body;
        $this->mentionedNames = static::extractUserNames($body);
        if ($this->mentionedNames) {
            $this->mentionedUsers = $this->adapter->findUsers($this->mentionedNames);
            foreach ($this->mentionedUsers as $user) {
                $search = '@'.$user->getUsername();
                $replace = $this->adapter->createUserLink($user);
                $this->parsedBody = str_replace($search, $replace, $this->parsedBody);
            }
        }

        return $this;
    }

    protected static function extractUserNames($body)
    {
        preg_match_all("/@([\x{4e00}-\x{9fa5}\w\-]+)/iu", $body, $mentionItems);

        return array_unique($mentionItems[1]);
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\ForumBundle\DependencyInjection;

use PHPDish\Bundle\ForumBundle\Model\Reply;
use PHPDish\Bundle\ForumBundle\Model\ReplyInterface;
use PHPDish\Bundle\ForumBundle\Model\Thread;
use PHPDish\Bundle\ForumBundle\Model\ThreadInterface;
use PHPDish\Bundle\ForumBundle\Model\Topic;
use PHPDish\Bundle\ForumBundle\Model\TopicInterface;
use PHPDish\Bundle\ResourceBundle\DependencyInjection\AbstractConfiguration;
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
use Symfony\Component\Config\Definition\ConfigurationInterface;

class Configuration extends AbstractConfiguration implements ConfigurationInterface
{
    /**
     * {@inheritdoc}
     */
    public function getConfigTreeBuilder()
    {
        $treeBuilder = new TreeBuilder();
        $rootNode = $treeBuilder->root('phpdish_forum');

        $rootNode
            ->children()
                ->arrayNode('resources')
                    ->children()
                        ->arrayNode('topic')
                            ->children()
                                ->scalarNode('interface')->defaultValue(TopicInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Topic::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                        ->arrayNode('reply')
                            ->children()
                                ->scalarNode('interface')->defaultValue(ReplyInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Reply::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                        ->arrayNode('thread')
                            ->children()
                                ->scalarNode('interface')->defaultValue(ThreadInterface::class)->cannotBeEmpty()->end()
                                ->scalarNode('model')->defaultValue(Thread::class)->cannotBeEmpty()->end()
                            ->end()
                        ->end()
                    ->end()
                ->end()
            ->end();

        $this->addTemplatesSection($rootNode);
        return $treeBuilder;
    }
}


<?php

/*
 * This file is part of the phpdish/phpdish
 *
 * (c) Slince <taosikai@yeah.net>
 *
 * This source file is subject to the MIT license that is bundled
 * with this source code in the file LICENSE.
 */

namespace PHPDish\Bundle\CoreBundle\Util;

use PHPDish\Bundle\CoreBundle\Model\Notification;
use PHPDish\Bundle\ForumBundle\Model\ReplyInterface;
use PHPDish\Bundle\ForumBundle\Model\TopicInterface;
use PHPDish\Bundle\NotificationBundle\Service\NotificationManagerInterface;
use PHPDish\Bundle\PaymentBundle\Model\PaymentInterface;
use PHPDish\Bundle\PostBundle\Model\CategoryInterface;
use PHPDish\Bundle\PostBundle\Model\CommentInterface;
use PHPDish\Bundle\PostBundle\Model\PostInterface;
use PHPDish\Bundle\UserBundle\Model\UserInterface;
use Symfony\Component\Routing\RouterInterface;
use Symfony\Component\Translation\TranslatorInterface;

final class NotificationHelper
{
    /**
     * @var NotificationManagerInterface
     */
    protected $notificationManager;

    /**
     * @var TranslatorInterface
     */
    protected $translator;

    /**
     * @var RouterInterface
     */
    protected $router;

    public function __construct(
        NotificationManagerInterface $notificationManager,
        TranslatorInterface $translator,
        RouterInterface $router
    ) {
        $this->notificationManager = $notificationManager;
        $this->translator = $translator;
        $this->router = $router;
    }

    /**
     * {@inheritdoc}
     */
    public function createFollowUserNotification(UserInterface $follower)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.follow_user.subject', [
                '%username%' => $follower->getUsername(),
                '%url%' => $this->router->generate('user_view', ['username' => $follower->getUsername()])
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_FOLLOW_USER,
            'follower_id' => $follower->getId(),
            'follower_username' => $follower->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createReplyTopicNotification(TopicInterface $topic, ReplyInterface $reply)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.reply_topic.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $reply->getUser()->getUsername()]),
                '%username%' =>$reply->getUser()->getUsername(),
                '%topicUrl%' => $this->router->generate('topic_view', ['id' => $topic->getId()]),
                '%topicTitle%' => $topic->getTitle()
            ]),
            $reply->getBody()
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_REPLY_TOPIC,
            'topic_id' => $topic->getId(),
            'reply_id' => $reply->getId(),
            'reply_user_id' => $reply->getUser()->getId(),
            'reply_user_username' => $reply->getUser()->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createCommentPostNotification(PostInterface $post, CommentInterface $comment)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.comment_post.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $comment->getUser()->getUsername()]),
                '%username%' =>$comment->getUser()->getUsername(),
                '%postUrl%' => $this->router->generate('post_view', ['id' => $post->getId()]),
                '%postTitle%' => $post->getTitle()
            ]),
            $comment->getBody()
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_COMMENT_POST,
            'post_id' => $post->getId(),
            'comment_id' => $comment->getId(),
            'comment_user_id' => $comment->getUser()->getId(),
            'comment_user_username' => $comment->getUser()->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createMentionUserInPostNotification(CommentInterface $comment)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.mention_user_post.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $comment->getUser()->getUsername()]),
                '%username%' => $comment->getUser()->getUsername(),
                '%postUrl%' => $this->router->generate('post_view', ['id' => $comment->getPost()->getId()]),
                '%postTitle%' => $comment->getPost()->getTitle()
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_MENTION_USER_IN_POST,
            'post_id' => $comment->getPost()->getId(),
            'comment_id' => $comment->getId(),
            'comment_user_id' => $comment->getUser()->getId(),
            'comment_user_username' => $comment->getUser()->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createMentionUserInTopicNotification(ReplyInterface $reply)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.mention_user_topic.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $reply->getUser()->getUsername()]),
                '%username%' => $reply->getUser()->getUsername(),
                '%topicUrl%' => $this->router->generate('post_view', ['id' => $reply->getTopic()->getId()]),
                '%topicTitle%' => $reply->getTopic()->getTitle()
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_MENTION_USER_IN_TOPIC,
            'topic_id' => $reply->getTopic()->getId(),
            'reply_id' => $reply->getId(),
            'reply_user_id' => $reply->getUser()->getId(),
            'reply_user_username' => $reply->getUser()->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createFollowCategoryNotification(CategoryInterface $category, UserInterface $follower)
    {
        if ($category->isBook()) {
            $notification = $this->notificationManager->createNotification(
                $this->translator->trans('notification.follow_book.subject', [
                    '%userUrl%' => $this->router->generate('user_view', ['username' => $follower->getUsername()]),
                    '%username%' => $follower->getUsername(),
                    '%bookUrl%' => $this->router->generate('book_view', ['slug' => $category->getSlug()]),
                    '%bookTitle%' => $category->getName()
                ])
            );
        } else {
            $notification = $this->notificationManager->createNotification(
                $this->translator->trans('notification.follow_category.subject', [
                    '%userUrl%' => $this->router->generate('user_view', ['username' => $follower->getUsername()]),
                    '%username%' => $follower->getUsername(),
                    '%categoryUrl%' => $this->router->generate('category_view', ['slug' => $category->getSlug()]),
                    '%categoryTitle%' => $category->getName()
                ])
            );
        }
        $notification->addParameters([
            'subject' => Notification::SUBJECT_FOLLOW_CATEGORY,
            'category_id' => $category->getId(),
            'follower_id' => $follower->getId(),
            'follower_username' => $follower->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createWithdrawNotification(PaymentInterface $payment)
    {
        $message = $payment->getStatus() === PaymentInterface::STATUS_OK
            ? $this->translator->trans('notification.withdraw.subject.your_withdraw_was_approved', ['%payment%' => $payment->getDescription()])
            : $this->translator->trans('notification.withdraw.subject.your_withdraw_was_declined', ['%payment%' => $payment->getDescription()]);

        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.withdraw.subject'),
            $message
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_HANDLE_WITHDRAW,
            'payment_id' => $payment->getId(),
            'payment_serial_no' => $payment->getSerialNo(),
        ]);

        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createVoteTopicNotification(TopicInterface $topic, UserInterface $user)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.vote_topic.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $user->getUsername()]),
                '%username%' => $user->getUsername(),
                '%topicUrl%' => $this->router->generate('topic_view', ['id'=>$topic->getId()]),
                '%topicTitle%' => $topic->getTitle()
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_VOTE_TOPIC,
            'topic_id' => $topic->getId(),
            'voter_id' => $user->getId(),
            'voter_username' => $user->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createVoteReplyNotification(TopicInterface $topic, ReplyInterface $reply, UserInterface $user)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.vote_reply.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $user->getUsername()]),
                '%username%' => $user->getUsername(),
                '%topicUrl%' => $this->router->generate('topic_view', ['id'=>$topic->getId()]),
                '%topicTitle%' => $topic->getTitle()
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_VOTE_REPLY,
            'topic_id' => $topic->getId(),
            'reply_id' => $reply->getId(),
            'voter_id' => $user->getId(),
            'voter_username' => $user->getUsername(),
        ]);
        return $notification;
    }

    /**
     * {@inheritdoc}
     */
    public function createVotePostNotification(PostInterface $post, UserInterface $user)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.vote_post.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $user->getUsername()]),
                '%username%' => $user->getUsername(),
                '%postUrl%' => $this->router->generate('post_view', ['id'=>$post->getId()]),
                '%postTitle%' => $post->getTitle()
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_VOTE_POST,
            'post_id' => $post->getId(),
            'voter_id' => $user->getId(),
            'voter_username' => $user->getUsername(),
        ]);

        return $notification;
    }
    /**
     * {@inheritdoc}
     */
    public function createVoteCommentNotification(PostInterface $post, CommentInterface $comment, UserInterface $user)
    {
        $notification = $this->notificationManager->createNotification(
            $this->translator->trans('notification.vote_comment.subject', [
                '%userUrl%' => $this->router->generate('user_view', ['username' => $user->getUsername()]),
                '%username%' => $user->getUsername(),
                '%postUrl%' => $this->router->generate('post_view', ['id'=>$post->getId()]),
                '%postTitle%' => $post->getTitle()
            ])
        );
        $notification->addParameters([
            'subject' => Notification::SUBJECT_VOTE_COMMENT,
            'post_id' => $post->getId(),
            'comment_id' => $comment->getId(),
            'voter_id' => $user->getId(),
            'voter_username' => $user->getUsername(),
        ]);
        return $notification;
    }

    /**
     * 发送消息
     *
     * @param UserInterface[]|UserInterface $participant
     * @param $notification
     */
    public function sendNotification($participant, $notification)
    {
        $participants = is_array($participant) ? $participant : [$participant];
        $this->notificationManager->sendNotification($participants, $notification, true);
    }

    public function __destruct()
    {
        $this->notificationManager->flush();
    }
}