diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ac1f865 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +composer.phar +phpunit.xml +vendor/ +data/schema.graphql +composer.lock diff --git a/CommentsBundle.php b/CommentsBundle.php new file mode 100644 index 0000000..1e2da7d --- /dev/null +++ b/CommentsBundle.php @@ -0,0 +1,20 @@ +addCompilerPass(new CommentsCompilerPass()); + + } + +} \ No newline at end of file diff --git a/DependencyInjection/CommentsExtension.php b/DependencyInjection/CommentsExtension.php new file mode 100644 index 0000000..0f1294f --- /dev/null +++ b/DependencyInjection/CommentsExtension.php @@ -0,0 +1,34 @@ +processConfiguration($configuration, $configs); + +// $container->setParameter('graphql_extensions.files', $config['files']); + $this->setContainerParam($container, 'platform', $config['platform']); + $this->setContainerParam($container, 'host', null); + $this->setContainerParam($container, 'scheme', null); + $this->setContainerParam($container, 'allow_anonymous', $config['allow_anonymous']); + $this->setContainerParam($container, 'max_depth', $config['max_depth']); + + $loader = new Loader\YamlFileLoader($container, new FileLocator(__DIR__ . '/../Resources/config')); + $loader->load('services.yml'); + } + + private function setContainerParam(ContainerBuilder $container, $parameter, $value) + { + $container->setParameter(sprintf('comments.config.%s', $parameter), $value); + } + +} \ No newline at end of file diff --git a/DependencyInjection/CompilerPass/CommentsCompilerPass.php b/DependencyInjection/CompilerPass/CommentsCompilerPass.php new file mode 100644 index 0000000..da3c83b --- /dev/null +++ b/DependencyInjection/CompilerPass/CommentsCompilerPass.php @@ -0,0 +1,32 @@ +getParameter('comments.config.platform'); + switch ($platform) { + case 'orm': + $container->setAlias('comments.om', 'doctrine.orm.entity_manager'); + $models['file'] = 'Youshido\GraphQLExtensionsBundle\Entity\File'; + break; + + case 'odm': + $container->setAlias('comments.om', 'doctrine_mongodb.odm.document_manager'); + $models['file'] = 'Youshido\GraphQLExtensionsBundle\Document\File'; + break; + } + $container->setParameter('comments.models', $models); + } + + +} \ No newline at end of file diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php new file mode 100644 index 0000000..a29c82c --- /dev/null +++ b/DependencyInjection/Configuration.php @@ -0,0 +1,44 @@ + + * created: 2/21/17 11:28 PM + */ +class Configuration implements ConfigurationInterface +{ + public function getConfigTreeBuilder() + { + $treeBuilder = new TreeBuilder(); + $rootNode = $treeBuilder->root('graphql_extensions'); + + $rootNode + ->children() + ->enumNode('storage') + ->values(['s3', 'filesystem']) + ->cannotBeEmpty() + ->defaultValue('filesystem') + ->end() + ->enumNode('platform') + ->values(['odm', 'orm']) + ->cannotBeEmpty() + ->defaultValue('orm') + ->end() + ->scalarNode('max_depth') + ->defaultValue(1) + ->cannotBeEmpty() + ->end() + ->booleanNode('allow_anonymous') + ->defaultValue(false) + ->end() + ->end(); + return $treeBuilder; + } + + +} \ No newline at end of file diff --git a/Document/Comment.php b/Document/Comment.php new file mode 100644 index 0000000..0177964 --- /dev/null +++ b/Document/Comment.php @@ -0,0 +1,296 @@ +content = $content; + $this->votes = new ArrayCollection(); + } + + /** + * @return mixed + */ + public function getId() + { + return $this->id; + } + + /** + * @param mixed $id + * @return Comment + */ + public function setId($id) + { + $this->id = $id; + return $this; + } + + /** + * @return mixed + */ + public function getContent() + { + return $this->content; + } + + /** + * @param mixed $content + * @return Comment + */ + public function setContent($content) + { + $this->content = $content; + return $this; + } + + /** + * @return mixed + */ + public function getParentId() + { + return $this->parentId; + } + + /** + * @param mixed $parentId + * @return Comment + */ + public function setParentId($parentId) + { + $this->parentId = $parentId; + return $this; + } + + /** + * @return mixed + */ + public function getCreatedAt() + { + return $this->createdAt; + } + + /** + * @param mixed $createdAt + * @return Comment + */ + public function setCreatedAt($createdAt) + { + $this->createdAt = $createdAt; + return $this; + } + + /** + * @return mixed + */ + public function getStatus() + { + return $this->status; + } + + /** + * @param mixed $status + * @return Comment + */ + public function setStatus($status) + { + $this->status = $status; + return $this; + } + + /** + * @return mixed + */ + public function getVotes() + { + return $this->votes; + } + + /** + * @param mixed $votes + * @return Comment + */ + public function setVotes($votes) + { + $this->votes = $votes; + return $this; + } + + + /** + * Add vote + * + * @param CommentVote $vote + */ + public function addVote($vote) + { + $this->votes[] = $vote; + } + + /** + * Remove vote + * + * @param CommentVote $vote + */ + public function removeVote($vote) + { + $this->votes->removeElement($vote); + } + + /** + * @return mixed + */ + public function getUpvotesCount() + { + return $this->upvotesCount; + } + + /** + * @param mixed $upvotesCount + * @return Comment + */ + public function setUpvotesCount($upvotesCount) + { + $this->upvotesCount = $upvotesCount; + return $this; + } + + /** + * @return mixed + */ + public function getDownvotesCount() + { + return $this->downvotesCount; + } + + /** + * @param mixed $downvotesCount + * @return Comment + */ + public function setDownvotesCount($downvotesCount) + { + $this->downvotesCount = $downvotesCount; + return $this; + } + + /** + * @return mixed + */ + public function getSlug() + { + return $this->slug; + } + + /** + * @param mixed $slug + */ + public function setSlug($slug) + { + $this->slug = $slug; + } + + /** + * @return int + */ + public function getLevel(): int + { + return $this->level; + } + + /** + * @param int $level + */ + public function setLevel(int $level) + { + $this->level = $level; + } + + /** + * @return mixed + */ + public function getUserReference() + { + return $this->userReference; + } + + /** + * @param mixed $userReference + * @return Comment + */ + public function setUserReference($userReference) + { + $this->userReference = $userReference; + return $this; + } + + /** + * @return mixed + */ + public function getModelId() + { + return $this->modelId; + } + + /** + * @param mixed $modelId + * @return Comment + */ + public function setModelId($modelId) + { + $this->modelId = $modelId; + return $this; + } + + + public function getAuthor() + { + return $this->userReference; + } + +} \ No newline at end of file diff --git a/Document/CommentInterface.php b/Document/CommentInterface.php new file mode 100644 index 0000000..2553a2a --- /dev/null +++ b/Document/CommentInterface.php @@ -0,0 +1,134 @@ + + * created: 3/15/17 8:24 PM + */ + +namespace Youshido\CommentsBundle\Document; + + +/** + * Class Comment + * @package Youshido\CommentsBundle\Document + * @ODM\Document(collection="comments") + */ +interface CommentInterface +{ + /** + * @return mixed + */ + public function getId(); + + /** + * @param mixed $id + * @return Comment + */ + public function setId($id); + + /** + * @return mixed + */ + public function getContent(); + + /** + * @param mixed $content + * @return Comment + */ + public function setContent($content); + + /** + * @return mixed + */ + public function getParentId(); + + /** + * @param mixed $parentId + * @return Comment + */ + public function setParentId($parentId); + + /** + * @return mixed + */ + public function getCreatedAt(); + + /** + * @param mixed $createdAt + * @return Comment + */ + public function setCreatedAt($createdAt); + + /** + * @return mixed + */ + public function getStatus(); + + /** + * @param mixed $status + * @return Comment + */ + public function setStatus($status); + + /** + * @return mixed + */ + public function getVotes(); + + /** + * @param mixed $votes + * @return Comment + */ + public function setVotes($votes); + + /** + * @return mixed + */ + public function getUpvotesCount(); + + /** + * @param mixed $upvotesCount + * @return Comment + */ + public function setUpvotesCount($upvotesCount); + + /** + * @return mixed + */ + public function getDownvotesCount(); + + /** + * @param mixed $downvotesCount + * @return Comment + */ + public function setDownvotesCount($downvotesCount); + + /** + * @return mixed + */ + public function getUserReference(); + + public function addVote($vote); + + public function removeVote($vote); + + public function getSlug(); + + /** + * @param mixed $userReference + * @return Comment + */ + public function setUserReference($userReference); + + /** + * @return mixed + */ + public function getModelId(); + + /** + * @param mixed $modelId + * @return Comment + */ + public function setModelId($modelId); +} \ No newline at end of file diff --git a/Document/CommentStatus.php b/Document/CommentStatus.php new file mode 100644 index 0000000..268eab2 --- /dev/null +++ b/Document/CommentStatus.php @@ -0,0 +1,13 @@ + + * created: 4/27/17 7:50 PM + */ + +namespace Youshido\CommentsBundle\Document; + + +interface CommentUserInterface +{ + + public function getId(); + + /** @return EmbeddedPath */ + public function getAvatar(); + +} \ No newline at end of file diff --git a/Document/CommentVote.php b/Document/CommentVote.php new file mode 100644 index 0000000..02514ad --- /dev/null +++ b/Document/CommentVote.php @@ -0,0 +1,58 @@ + + * created: 3/16/17 9:55 AM + */ + +namespace Youshido\CommentsBundle\Document; + +use Doctrine\ODM\MongoDB\Mapping\Annotations as ODM; + +/** + * Class CommentVote + * @package Youshido\CommentsBundle\Document + * @ODM\EmbeddedDocument() + */ +class CommentVote +{ + /** @ODM\ObjectId() */ + private $userId; + + /** @ODM\Field(type="int") */ + private $value; + + /** + * @return mixed + */ + public function getUserId() + { + return $this->userId; + } + + /** + * @param mixed $userId + */ + public function setUserId($userId) + { + $this->userId = $userId; + } + + /** + * @return mixed + */ + public function getValue() + { + return $this->value; + } + + /** + * @param mixed $value + */ + public function setValue($value) + { + $this->value = $value; + } + +} \ No newline at end of file diff --git a/Document/CommentableInterface.php b/Document/CommentableInterface.php new file mode 100644 index 0000000..97f80ed --- /dev/null +++ b/Document/CommentableInterface.php @@ -0,0 +1,15 @@ + + * created: 3/15/17 8:11 PM + */ + +namespace Youshido\CommentsBundle\Document; + + +interface CommentableInterface +{ + public function getId(); +} \ No newline at end of file diff --git a/Document/EmbeddedPath.php b/Document/EmbeddedPath.php new file mode 100644 index 0000000..1fca8d7 --- /dev/null +++ b/Document/EmbeddedPath.php @@ -0,0 +1,56 @@ +id; + } + + /** + * @param mixed $id + */ + public function setId($id) + { + $this->id = $id; + } + + + + /** + * @return mixed + */ + public function getPath() + { + return $this->path; + } + + /** + * @param mixed $path + */ + public function setPath($path) + { + $this->path = $path; + } + +} \ No newline at end of file diff --git a/Document/Repository/CommentRepository.php b/Document/Repository/CommentRepository.php new file mode 100644 index 0000000..2f7b81c --- /dev/null +++ b/Document/Repository/CommentRepository.php @@ -0,0 +1,46 @@ + 'upvotesCount', + 'order' => 1, + ]; + break; + + case CommentSortModeEnumType::COMMENT_SORT_TYPE_NEWEST: + $args['sort'] = [ + 'field' => 'slug', + 'order' => 1, + ]; + break; + } + } + return parent::getCursoredList($args, $filters); + } + + + public function createQueryForFilters($filters) + { + $qb = $this->createQueryBuilder(); + + if (!empty($filters['showdownId'])) { + $qb->addAnd(['modelId' => new \MongoId($filters['showdownId'])]); + } + + $qb->sort('slug', 'ASC'); + + return $qb; + } +} \ No newline at end of file diff --git a/Document/UserReference.php b/Document/UserReference.php new file mode 100644 index 0000000..4301e1f --- /dev/null +++ b/Document/UserReference.php @@ -0,0 +1,83 @@ +userId = $user->getId(); + $this->name = (string)$user; + $this->avatar = new EmbeddedPath(); + $this->avatar->setPath($user->getAvatar() ? $user->getAvatar()->getPath() : null); + } + + /** + * @return mixed + */ + public function getName() + { + return $this->name; + } + + /** + * @param mixed $name + */ + public function setName($name) + { + $this->name = $name; + } + + /** + * @return mixed + */ + public function getUserId() + { + return $this->userId; + } + + /** + * @param mixed $userId + */ + public function setUserId($userId) + { + $this->userId = $userId; + } + + /** + * @return EmbeddedPath + */ + public function getAvatar() + { + return $this->avatar; + } + + /** + * @param EmbeddedPath $avatar + */ + public function setAvatar($avatar) + { + $this->avatar = $avatar; + } + + + +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bf39183 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Youshido + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 4486181..42b88e9 100644 --- a/README.md +++ b/README.md @@ -1 +1,12 @@ -# CommentsBundle \ No newline at end of file +# CommentsBundle +Provides Comments +- MongoDB Support +- GraphQL Fields + + +## Sample Configuration + +```yaml +comments: + platform: "odm" +``` \ No newline at end of file diff --git a/Resources/config/services.yml b/Resources/config/services.yml new file mode 100644 index 0000000..508a114 --- /dev/null +++ b/Resources/config/services.yml @@ -0,0 +1,9 @@ +services: + comments_manager: + class: Youshido\CommentsBundle\Service\CommentsManager + arguments: + - "@comments.om" + - "@security.token_storage" + calls: + - ["setAllowAnonymous", ["%comments.config.allow_anonymous%"]] + - ["initiateCurrentUser"] \ No newline at end of file diff --git a/Service/CommentsManager.php b/Service/CommentsManager.php new file mode 100644 index 0000000..f53689e --- /dev/null +++ b/Service/CommentsManager.php @@ -0,0 +1,218 @@ + + * created: 3/15/17 7:25 PM + */ + +namespace Youshido\CommentsBundle\Service; + + +use Doctrine\Common\Persistence\ObjectManager; +use Doctrine\ODM\MongoDB\DocumentManager; +use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorage; +use Youshido\CommentsBundle\Document\Comment; +use Youshido\CommentsBundle\Document\CommentableInterface; +use Youshido\CommentsBundle\Document\CommentInterface; +use Youshido\CommentsBundle\Document\CommentVote; +use Youshido\CommentsBundle\Document\UserReference; + +class CommentsManager +{ + + /** @var ObjectManager */ + private $om; + + /** @var TokenStorage */ + private $tokenStorage; + + /** @var */ + private $currentUser; + + /** @var bool */ + private $allowAnonymous = false; + + /** + * CommentsManager constructor. + * @param ObjectManager $om + * @param TokenStorage $tokenStorage + */ + public function __construct(ObjectManager $om, TokenStorage $tokenStorage) + { + $this->om = $om; + $this->tokenStorage = $tokenStorage; + } + + + /** + * @param CommentableInterface $object + * @param string $content + * @param $parentId + * @return CommentInterface + */ + public function createComment($object, $content, $parentId = null) + { + $comment = new Comment($content); + $comment->setModelId(new \MongoId($object->getId())); + $parentSlug = ''; + if (!empty($parentId)) { + $comment->setParentId($parentId); + $parent = $this->om->getRepository(Comment::class)->find($parentId); + $parentSlug = $parent->getSlug() . '-'; + $comment->setLevel($parent->getLevel() + 1); + } + $comment->setCreatedAt(new \DateTime()); + $comment->setSlug($parentSlug . (new \DateTime())->format("Y.m.d.H.i.s-") . uniqid()); + $this->processAuth($comment); + + $this->om->persist($comment); + $this->om->flush(); + + return $comment; + } + + protected function processAuth(CommentInterface $comment) + { + $user = $this->getCurrentUser(); + $userReference = new UserReference($user); + $comment->setUserReference($userReference); + } + + public function getCurrentUser() + { + if (!$this->allowAnonymous && !$this->currentUser) { + throw new \Exception('Anonymous comments are not allowed'); + } + + return $this->currentUser; + } + + /** + * @param mixed $currentUser + */ + public function setCurrentUser($currentUser) + { + $this->currentUser = $currentUser; + } + + /** + * @param CommentInterface $comment + * @return bool + */ + public function upvote($comment) + { + return $this->addVote($comment, 1); + } + + /** + * @param CommentInterface $comment + * @param $value + * @return bool + */ + public function addVote($comment, $value = 1) + { + if ($this->userHasVoted($comment)) { + return false; + } + + $vote = new CommentVote(); + $vote->setUserId($this->getCurrentUser()->getId()); + $vote->setValue($value); + $comment->addVote($vote); + if ($value > 0) { + $comment->setUpvotesCount($comment->getUpvotesCount() + $value); + } else { + $comment->setDownvotesCount($comment->getDownvotesCount() + abs($value)); + } + $this->om->flush(); + + return $comment->getUpvotesCount() + $comment->getDownvotesCount(); + } + + public function userHasVoted(CommentInterface $comment) + { + if (!$this->currentUser) return null; + $user = $this->getCurrentUser(); + /** @var DocumentManager $om */ + $om = $this->om; + $vote = $om->getRepository(Comment::class)->findOneBy([ + '_id' => $comment->getId(), + 'votes.userId' => new \MongoId($user->getId()), + ]); + + return !empty($vote); + } + + /** + * @param CommentInterface $comment + * + * @return bool + */ + public function downvote($comment) + { + return $this->addVote($comment, -1); + } + + /** + * @param CommentInterface $comment + */ + public function deleteComment($comment) + { + /** @var DocumentManager $om */ + $om = $this->om; + $om->getDocumentCollection(Comment::class)->createQueryBuilder()->remove() + ->field('slug')->equals(new \MongoRegex('/^' . $comment->getSlug() . '/')) + ->getQuery()->execute(); + } + + public function initiateCurrentUser() + { + if ($token = $this->tokenStorage->getToken()) { + if (($user = $token->getUser()) && is_object($user)) { + $this->currentUser = $user; + } + } + } + + /** + * @return CommentInterface[] + */ + public function getCursoredComments($args) + { + return $this->om->getRepository(Comment::class)->getCursoredList($args, !empty($args['filters']) ? $args['filters'] : []); + } + + /** + * @param CommentableInterface $object + * @return CommentInterface[] + */ + public function getComments(CommentableInterface $object) + { + /** @var DocumentManager $om */ + $om = $this->om; + + return $om->getRepository(Comment::class)->createQueryBuilder() + ->field('modelId')->equals(new \MongoId($object->getId())) + ->sort('slug') + ->getQuery()->execute(); + } + + /** + * @return bool + */ + public function isAllowAnonymous(): bool + { + return $this->allowAnonymous; + } + + /** + * @param bool $allowAnonymous + */ + public function setAllowAnonymous(bool $allowAnonymous) + { + $this->allowAnonymous = $allowAnonymous; + } + + +} \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b45dc9f --- /dev/null +++ b/composer.json @@ -0,0 +1,27 @@ +{ + "name": "youshido/comments-bundle", + "description": "Symfony Comments Bundle with MongoDB & GraphQL Support", + "minimum-stability": "stable", + "license": "mit", + "authors": [ + { + "name": "Alexandr Viniychuk", + "email": "a@viniychuk.com" + } + + ], + "autoload": { + "psr-4": { + "Youshido\\CommentsBundle\\": "" + } + }, + "require": { + "php": ">=5.6", + "youshido/graphql-bundle": "~1.2 || 2.0.x-dev" + }, + "require-dev": { + "phpunit/phpunit": "~4.7", + "symfony/symfony": "~3", + "composer/composer": "~1.2" + } +} diff --git a/phpunit.xml.dist b/phpunit.xml.dist new file mode 100644 index 0000000..01dca4d --- /dev/null +++ b/phpunit.xml.dist @@ -0,0 +1,15 @@ + + + + + + ./Tests + + + + + + ./src/ + + +