Compare commits

..

14 Commits

Author SHA1 Message Date
Alexey Eschenko 07f89712d9 Merged in composer_update (pull request #27)
composer update.
2020-04-29 14:22:11 +00:00
Alexey Skobkin fd09f389f7
composer update. 2020-04-29 16:55:02 +03:00
Alexey Skobkin b6f7ac8ec5 Fixing UserRepositoryTest according to new test users. 2019-04-03 20:40:09 +03:00
Alexey Skobkin f598864d4d Crutch-fixing PostController::showAction() exception handling with 404 instead of 403 exception. 2019-04-03 20:34:16 +03:00
Alexey Skobkin 44c4158602 Fixing automatic replacing of AccessDeniedExceptions with InsufficientAuthenticationException in Symfony\Component\Security\Http\Firewall\ExceptionListener::handleAccessDeniedException(). 2019-04-03 20:16:34 +03:00
Alexey Skobkin aa751bbbc1 Fixing MainControllerTest::testAjaxUserAutoCompleteHasValidUserObjectsForUnnamedUser() in case of user's name in null instead of empty string. 2019-04-03 19:43:32 +03:00
Alexey Skobkin 9e5f59a2b2 Fixina small MainControllerTest problems. 2019-04-03 19:38:57 +03:00
Alexey Skobkin 7fcdcbf728 Fixing AJAX data deserialization in the MainControllerTest. 2019-04-03 19:34:01 +03:00
Alexey Skobkin 60dcc5e955 Fixing MainControllerTest dependencies. 2019-04-03 19:28:18 +03:00
Alexey Skobkin c3605b2db1 Fixing MainControllerTest::{testAjaxUserAutoCompleteHasOptions, testFindUsersLikeLogin}() and adding new tests for user without name. 2019-04-03 19:19:03 +03:00
Alexey Skobkin b455a6c8e7 Adding new tests in PostControllerTest to check for potential private post leakage. 2019-04-03 18:55:29 +03:00
Alexey Skobkin 5e8935ce66 Fixing PostController::showAction() exception on private author's post. 2019-04-03 18:52:36 +03:00
Alexey Skobkin d9c0673445 Fixing user and post data fixtures to fix PostControllerTest::testShortPostPageIsOk() according to previous privacy fix. New post for more advanced privacy test-cases added. Test must be written though. 2019-04-03 18:38:53 +03:00
Alexey Skobkin 0c004085fd Fixing privacy in PostController::showAction(). 2019-04-03 18:07:47 +03:00
38 changed files with 1525 additions and 1821 deletions

View File

@ -19,7 +19,6 @@ class AppKernel extends Kernel
new Symfony\Bundle\WebServerBundle\WebServerBundle(), new Symfony\Bundle\WebServerBundle\WebServerBundle(),
new JMS\SerializerBundle\JMSSerializerBundle(), new JMS\SerializerBundle\JMSSerializerBundle(),
new Csa\Bundle\GuzzleBundle\CsaGuzzleBundle(), new Csa\Bundle\GuzzleBundle\CsaGuzzleBundle(),
new Leezy\PheanstalkBundle\LeezyPheanstalkBundle(),
new Ob\HighchartsBundle\ObHighchartsBundle(), new Ob\HighchartsBundle\ObHighchartsBundle(),
new Knp\Bundle\MarkdownBundle\KnpMarkdownBundle(), new Knp\Bundle\MarkdownBundle\KnpMarkdownBundle(),
new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(), new Knp\Bundle\PaginatorBundle\KnpPaginatorBundle(),

View File

@ -1,51 +0,0 @@
<?php
namespace Application\Migrations;
use Doctrine\DBAL\Migrations\AbstractMigration;
use Doctrine\DBAL\Schema\Schema;
/**
* Issue #44 - Post and Comment schema refactoring.
* - Post subscription status added.
* - Comments parent-child relations removed.
* - User login index
* - Other adjustments
*/
class Version20180427143940 extends AbstractMigration
{
public function up(Schema $schema)
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE posts.posts ADD is_subscribed BOOLEAN DEFAULT FALSE NOT NULL');
// Removing parent_id constraint and index
$this->addSql('ALTER TABLE posts.comments DROP CONSTRAINT fk_62899975727aca70');
$this->addSql('DROP INDEX posts.idx_62899975727aca70');
$this->addSql('ALTER TABLE posts.comments DROP parent_id');
$this->addSql('ALTER TABLE posts.comments ADD to_number INT');
$this->addSql('ALTER TABLE posts.comments ALTER number TYPE INT');
$this->addSql('ALTER TABLE posts.comments ALTER number DROP DEFAULT');
$this->addSql('CREATE UNIQUE INDEX UNIQ_6289997596901F54 ON posts.comments (number)');
$this->addSql('CREATE INDEX idx_user_login ON users.users (login)');
}
public function down(Schema $schema)
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('ALTER TABLE posts.posts DROP is_subscribed');
$this->addSql('DROP INDEX posts.UNIQ_6289997596901F54');
$this->addSql('ALTER TABLE posts.comments ADD parent_id INT DEFAULT NULL');
$this->addSql('ALTER TABLE posts.comments DROP to_number');
$this->addSql('ALTER TABLE posts.comments ALTER number TYPE SMALLINT');
$this->addSql('ALTER TABLE posts.comments ALTER number DROP DEFAULT');
$this->addSql('ALTER TABLE posts.comments ADD CONSTRAINT fk_62899975727aca70 FOREIGN KEY (parent_id) REFERENCES posts.comments (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
$this->addSql('CREATE INDEX idx_62899975727aca70 ON posts.comments (parent_id)');
$this->addSql('DROP INDEX users.idx_user_login');
}
}

View File

@ -1,28 +0,0 @@
<?php declare(strict_types=1);
namespace Application\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20190227233244 extends AbstractMigration
{
public function up(Schema $schema) : void
{
// this up() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('CREATE UNIQUE INDEX unique_post_id_comment_number ON posts.comments (post_id, number)');
}
public function down(Schema $schema) : void
{
// this down() migration is auto-generated, please modify it to your needs
$this->abortIf($this->connection->getDatabasePlatform()->getName() !== 'postgresql', 'Migration can only be executed safely on \'postgresql\'.');
$this->addSql('DROP INDEX posts.unique_post_id_comment_number');
}
}

View File

@ -71,13 +71,6 @@ doctrine_migrations:
table_name: migration_versions table_name: migration_versions
name: Application Migrations name: Application Migrations
leezy_pheanstalk:
pheanstalks:
primary:
server: "%beanstalkd_host%"
port: "%beanstalkd_port%"
default: true
# Swiftmailer Configuration # Swiftmailer Configuration
swiftmailer: swiftmailer:
transport: "%mailer_transport%" transport: "%mailer_transport%"

View File

@ -6,11 +6,6 @@ parameters:
database_user: point database_user: point
database_password: ~ database_password: ~
# Message Queue settings
beanstalkd_host: 'localhost'
beanstalkd_port: 11300
beanstalkd_ws_updates_tube: 'point-websocket-updates'
mailer_transport: smtp mailer_transport: smtp
mailer_host: 127.0.0.1 mailer_host: 127.0.0.1
mailer_user: ~ mailer_user: ~

View File

@ -9,4 +9,4 @@ security:
security: false security: false
default: default:
anonymous: ~ anonymous: true

View File

@ -101,13 +101,6 @@ services:
# Send message # Send message
Skobkin\Bundle\PointToolsBundle\Command\TelegramSendMessageCommand: Skobkin\Bundle\PointToolsBundle\Command\TelegramSendMessageCommand:
tags: [{ name: console.command }] tags: [{ name: console.command }]
# WebSocket MQ processing
Skobkin\Bundle\PointToolsBundle\Command\ProcessWebsocketUpdatesCommand:
arguments:
$bsClient: '@leezy.pheanstalk.primary'
$bsTubeName: '%beanstalkd_ws_updates_tube%'
tags:
- { name: console.command }
# Entity repositories as services # Entity repositories as services

View File

@ -13,6 +13,7 @@
}, },
"require": { "require": {
"php": ">=7.1.0", "php": ">=7.1.0",
"ext-json": "*",
"symfony/symfony": "^3.4", "symfony/symfony": "^3.4",
"doctrine/orm": "^2.5", "doctrine/orm": "^2.5",
"doctrine/annotations": "^1.3.0", "doctrine/annotations": "^1.3.0",
@ -33,8 +34,7 @@
"unreal4u/telegram-api": "^2.2", "unreal4u/telegram-api": "^2.2",
"csa/guzzle-bundle": "^3", "csa/guzzle-bundle": "^3",
"symfony/web-server-bundle": "^3.3", "symfony/web-server-bundle": "^3.3",
"sentry/sentry-symfony": "^2.2", "sentry/sentry-symfony": "^2.2"
"leezy/pheanstalk-bundle": "^3.3"
}, },
"require-dev": { "require-dev": {
"symfony/phpunit-bridge": "^3.0", "symfony/phpunit-bridge": "^3.0",

1912
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,114 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Command;
use Doctrine\ORM\EntityManagerInterface;
use JMS\Serializer\Serializer;
use Leezy\PheanstalkBundle\Proxy\PheanstalkProxy;
use Pheanstalk\Job;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message;
use Skobkin\Bundle\PointToolsBundle\Exception\WebSocket\UnsupportedTypeException;
use Skobkin\Bundle\PointToolsBundle\Service\WebSocket\WebSocketMessageProcessor;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\{InputInterface, InputOption};
use Symfony\Component\Console\Output\OutputInterface;
/**
* This command processes WebSocket updates MQ and stores new content in the DB
*/
class ProcessWebsocketUpdatesCommand extends Command
{
/** @var EntityManagerInterface */
private $em;
/** @var PheanstalkProxy */
private $bsClient;
/** @var string */
private $bsTubeName;
/** @var Serializer */
private $serializer;
/** @var WebSocketMessageProcessor */
private $messageProcessor;
/** @var \Raven_Client */
private $sentryClient;
public function __construct(
EntityManagerInterface $em,
\Raven_Client $raven,
PheanstalkProxy $bsClient,
string $bsTubeName,
Serializer $serializer,
WebSocketMessageProcessor $processor
) {
$this->em = $em;
$this->sentryClient = $raven;
$this->serializer = $serializer;
$this->messageProcessor = $processor;
$this->bsClient = $bsClient;
$this->bsTubeName = $bsTubeName;
parent::__construct();
}
/** {@inheritdoc} */
protected function configure()
{
$this
->setName('point:update:websocket-messages')
->setDescription('Reads and processes updates from Beanstalkd queue pipe')
->addOption('keep-jobs', 'k', InputOption::VALUE_NONE, 'Don\'t delete jobs from queue after processing')
;
}
/** {@inheritdoc} */
protected function execute(InputInterface $input, OutputInterface $output)
{
$keepJobs = (bool) $input->getOption('keep-jobs');
/** @var Job $job */
while ($job = $this->bsClient->reserveFromTube($this->bsTubeName, 0)) {
try {
/** @var Message $message */
$message = $this->serializer->deserialize($job->getData(), Message::class, 'json');
} catch (\Exception $e) {
$output->writeln(sprintf(
'Error while deserializing #%d data: \'%s\'',
$job->getId(),
$job->getData()
));
$this->sentryClient->captureException($e);
continue;
}
$output->writeln('Processing job #'.$job->getId().' ('.$message->getA().')');
try {
if ($this->messageProcessor->processMessage($message)) {
$this->em->flush();
if (!$keepJobs) {
$this->bsClient->delete($job);
}
$this->em->clear();
}
} catch (UnsupportedTypeException $e) {
$output->writeln(' Unsupported message type: '.$message->getA());
$this->sentryClient->captureException($e);
continue;
} catch (\Exception $e) {
$output->writeln(' Message processing error: '.$e->getMessage());
$this->sentryClient->captureException($e);
continue;
}
}
}
}

View File

@ -12,11 +12,19 @@ class PostController extends AbstractController
{ {
/** /**
* @ParamConverter("post", class="SkobkinPointToolsBundle:Blogs\Post") * @ParamConverter("post", class="SkobkinPointToolsBundle:Blogs\Post")
*
* @return Response
*/ */
public function showAction(Post $post, PostRepository $postRepository): Response public function showAction(Post $post, PostRepository $postRepository): Response
{ {
if ((!$post->getAuthor()->isPublic()) || $post->getAuthor()->isWhitelistOnly()) {
/**
* Throwing 404 instead of 403 because of
* @see \Symfony\Component\Security\Http\Firewall\ExceptionListener::handleAccessDeniedException()
* starts to replace 403 by 401 exceptions for anonymous users and tries to authenticate them.
*/
throw $this->createNotFoundException('Author\'s blog is private.');
//throw $this->createAccessDeniedException('Author\'s blog is private.');
}
return $this->render('SkobkinPointToolsBundle:Post:show.html.twig', [ return $this->render('SkobkinPointToolsBundle:Post:show.html.twig', [
'post' => $postRepository->getPostWithComments($post->getId()), 'post' => $postRepository->getPostWithComments($post->getId()),
]); ]);

View File

@ -4,22 +4,39 @@ namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
class Comment implements ValidableInterface class Comment implements ValidableInterface
{ {
/** @var int|null */ /**
* @var string|null
*/
private $postId;
/**
* @var int|null
*/
private $number; private $number;
/** @var int|null */ /**
* @var int|null
*/
private $toCommentId; private $toCommentId;
/** @var string|null */ /**
* @var string|null
*/
private $created; private $created;
/** @var string|null */ /**
* @var string|null
*/
private $text; private $text;
/** @var User|null */ /**
* @var User|null
*/
private $author; private $author;
/** @var bool|null */ /**
* @var bool|null
*/
private $isRec; private $isRec;

View File

@ -4,17 +4,27 @@ namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
class MetaPost implements ValidableInterface class MetaPost implements ValidableInterface
{ {
/** @var Post|null */ /**
* @var Post|null
*/
private $post; private $post;
/** @var Comment[]|null */ /**
* @var Comment[]|null
*/
private $comments; private $comments;
public function getPost(): ?Post public function getPost(): ?Post
{ {
return $this->post; return $this->post;
} }
public function setPost(?Post $post): void
{
$this->post = $post;
}
/** /**
* @return Comment[]|null * @return Comment[]|null
*/ */
@ -23,8 +33,20 @@ class MetaPost implements ValidableInterface
return $this->comments; return $this->comments;
} }
/**
* @param Comment[]|null $comments
*/
public function setComments(?array $comments): void
{
$this->comments = $comments;
}
public function isValid(): bool public function isValid(): bool
{ {
return (null !== $this->post && $this->post->isValid()); if (null !== $this->post && $this->post->isValid()) {
return true;
}
return false;
} }
} }

View File

@ -4,28 +4,44 @@ namespace Skobkin\Bundle\PointToolsBundle\DTO\Api;
class Post implements ValidableInterface class Post implements ValidableInterface
{ {
/** @var string|null */ /**
* @var string|null
*/
private $id; private $id;
/** @var string[]|null */ /**
* @var string[]|null
*/
private $tags; private $tags;
/** @var string[]|null */ /**
* @var string[]|null
*/
private $files; private $files;
/** @var User|null */ /**
* @var User|null
*/
private $author; private $author;
/** @var string|null */ /**
* @var string|null
*/
private $text; private $text;
/** @var string|null */ /**
* @var string|null
*/
private $created; private $created;
/** @var string|null */ /**
* @var string|null
*/
private $type; private $type;
/** @var bool|null */ /**
* @var bool|null
*/
private $private; private $private;

View File

@ -1,330 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\ValidableInterface;
use Skobkin\Bundle\PointToolsBundle\Exception\WebSocket\UnsupportedTypeException;
/**
* WebSocket update message
*/
class Message implements ValidableInterface
{
public const TYPE_COMMENT = 'comment';
public const TYPE_COMMENT_EDITED = 'comment_edited';
public const TYPE_POST = 'post';
public const TYPE_POST_EDITED = 'post_edited';
public const TYPE_POST_COMMENT_RECOMMENDATION = 'rec';
public const TYPE_RECOMMENDATION_WITH_COMMENT = 'ok';
/**
* Event type. @see Message::TYPE_* constants
*
* @var string
*/
private $a;
/**
* Login of the user
*
* @var string
*/
private $author;
/**
* @var int
*/
private $authorId;
/** @var string|null */
private $authorName;
/**
* Number of the comment in the thread
*
* @var int|null
*/
private $commentId;
/**
* ???
*
* @var bool|null
*/
private $cut;
/**
* Array of file paths
*
* @var string[]|null
*/
private $files;
/** @var string|null */
private $html;
/**
* @deprecated Link in the Post::type=feed posts
*
* @var string|null
*/
private $link;
/**
* @var int|null
*/
private $postAuthorId;
/** @var string */
private $postId;
/** @var bool|null */
private $private;
/**
* Number of the comment in the thread for recommendation with text
*
* @var int|null
*/
private $rcid;
/**
* Array of tags
*
* @var string[]|null
*/
private $tags;
/** @var string */
private $text;
/**
* @deprecated ???
*
* @var string|null
*/
private $title;
/**
* Number of the comment to which this comment is answering
*
* @var string|null
*/
private $toCommentId;
/**
* Text quotation of the comment to which this comment is answering
*
* @var string|null
*/
private $toText;
/**
* Array of logins of users to which post is addressed
*
* @var string[]|null
*/
private $toUsers;
public function isPost(): bool
{
return self::TYPE_POST === $this->a;
}
public function isComment(): bool
{
return self::TYPE_COMMENT === $this->a;
}
public function isCommentRecommendation(): bool
{
return self::TYPE_RECOMMENDATION_WITH_COMMENT === $this->a;
}
public function isPostRecommendation(): bool
{
return self::TYPE_POST_COMMENT_RECOMMENDATION === $this->a;
}
/**
* @throws \RuntimeException
* @throws UnsupportedTypeException
*/
public function isValid(): bool
{
switch ($this->a) {
case self::TYPE_POST:
return $this->isValidPost();
break;
case self::TYPE_POST_EDITED:
return $this->isValidPostEdited();
break;
case self::TYPE_COMMENT;
return $this->isValidComment();
break;
case self::TYPE_COMMENT_EDITED;
return $this->isValidCommentEdited();
break;
case self::TYPE_RECOMMENDATION_WITH_COMMENT;
return $this->isValidRecommendationWithComment();
break;
case self::TYPE_POST_COMMENT_RECOMMENDATION;
return $this->isValidPostCommentRecommendation();
break;
case null:
throw new \RuntimeException('Message has NULL type.');
break;
default:
throw new UnsupportedTypeException(sprintf('Type \'%s\' is not supported.', $this->a));
}
}
public function getA(): string
{
return $this->a;
}
public function getAuthor(): string
{
return $this->author;
}
public function getAuthorId(): int
{
return $this->authorId;
}
public function getAuthorName(): ?string
{
return $this->authorName;
}
public function getCommentId(): ?int
{
return $this->commentId;
}
public function getCut(): ?bool
{
return $this->cut;
}
public function getFiles(): ?array
{
return $this->files;
}
public function getHtml(): ?string
{
return $this->html;
}
public function getLink(): ?string
{
return $this->link;
}
public function getPostId(): string
{
return $this->postId;
}
public function getPostAuthorId(): ?int
{
return $this->postAuthorId;
}
public function getPrivate(): ?bool
{
return $this->private;
}
public function getRcid(): ?int
{
return $this->rcid;
}
public function getTags(): ?array
{
return $this->tags;
}
public function getText(): string
{
return $this->text;
}
public function getTitle(): ?string
{
return $this->title;
}
public function getToCommentId(): ?string
{
return $this->toCommentId;
}
public function getToText(): ?string
{
return $this->toText;
}
public function getToUsers(): ?array
{
return $this->toUsers;
}
private function hasCommonMandatoryData(): bool
{
return (
null !== $this->author &&
null !== $this->authorId &&
null !== $this->postId
);
}
private function isValidPost(): bool
{
return $this->hasCommonMandatoryData() && (
// Text can be empty ("") though
null !== $this->text &&
null !== $this->tags
);
}
private function isValidPostEdited(): bool
{
return $this->isValidPost();
}
private function isValidComment(): bool
{
return $this->hasCommonMandatoryData() && (
null !== $this->commentId &&
null !== $this->html &&
null !== $this->text
);
}
private function isValidCommentEdited(): bool
{
return $this->hasCommonMandatoryData() && (null !== $this->commentId);
}
private function isValidRecommendationWithComment(): bool
{
return $this->hasCommonMandatoryData();
}
private function isValidPostCommentRecommendation(): bool
{
return $this->hasCommonMandatoryData() && (null !== $this->postAuthorId);
}
}

View File

@ -11,7 +11,7 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
class LoadCommentsData extends AbstractFixture implements OrderedFixtureInterface class LoadCommentsData extends AbstractFixture implements OrderedFixtureInterface
{ {
public function load(ObjectManager $om): void public function load(ObjectManager $om)
{ {
/** @var Post $post */ /** @var Post $post */
$post = $this->getReference('test_post_longpost'); $post = $this->getReference('test_post_longpost');
@ -25,28 +25,29 @@ class LoadCommentsData extends AbstractFixture implements OrderedFixtureInterfac
$this->getReference('test_user_99995'), $this->getReference('test_user_99995'),
]; ];
$text = 'Some text with [link to @skobkin-ru site](https://skobk.in/) and `code block`'.PHP_EOL. $comments = [];
'and some quotation:'.PHP_EOL.
'> test test quote'.PHP_EOL.
'and some text after';
foreach (range(1, 10000) as $num) { foreach (range(1, 10000) as $num) {
$comment = new Comment( $comment = (new Comment())
$text, ->setNumber($num)
new \DateTime(), ->setDeleted(mt_rand(0, 15) ? false : true)
false, ->setCreatedAt(new \DateTime())
$post, ->setAuthor($users[array_rand($users)])
$num, ->setRec(false)
($num > 1 && !random_int(0, 4)) ? random_int(1, $num - 1) : null, ->setText(
$users[array_rand($users)], 'Some text with [link to @skobkin-ru site](https://skobk.in/) and `code block`'.PHP_EOL.
[] 'and some quotation:'.PHP_EOL.
); '> test test quote'.PHP_EOL.
'and some text after'
)
;
if (!random_int(0, 15)) { if (count($comments) > 0 && mt_rand(0, 1)) {
$comment->delete(); $comment->setParent($comments[mt_rand(0, count($comments) - 1)]);
} }
$post->addComment($comment); $post->addComment($comment);
$comments[] = $comment;
$om->persist($comment); $om->persist($comment);
} }

View File

@ -2,33 +2,64 @@
namespace Skobkin\Bundle\PointToolsBundle\DataFixtures\ORM; namespace Skobkin\Bundle\PointToolsBundle\DataFixtures\ORM;
use Doctrine\Common\DataFixtures\AbstractFixture; use Doctrine\Common\DataFixtures\{AbstractFixture, OrderedFixtureInterface};
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager; use Doctrine\Common\Persistence\ObjectManager;
use Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Post; use Skobkin\Bundle\PointToolsBundle\Entity\{Blogs\Post, User};
use Skobkin\Bundle\PointToolsBundle\Entity\User;
class LoadPostData extends AbstractFixture implements OrderedFixtureInterface class LoadPostData extends AbstractFixture implements OrderedFixtureInterface
{ {
public const POST_ID_LONG = 'longpost';
public const POST_ID_SHORT = 'shortpost';
public const POST_ID_PR_USER = 'prusrpst';
public const POST_ID_WL_USER = 'wlusrpst';
public const POST_ID_PR_WL_USER = 'prwlusrpst';
public function load(ObjectManager $om) public function load(ObjectManager $om)
{ {
/** @var User $testUser */ /** @var User $mainUser */
$testUser = $this->getReference('test_user_99999'); $mainUser = $this->getReference('test_user_'.LoadUserData::USER_MAIN_ID);
/** @var User $privateUser */
$privateUser = $this->getReference('test_user_'.LoadUserData::USER_PRIV_ID);
/** @var User $wlUser */
$wlUser = $this->getReference('test_user_'.LoadUserData::USER_WLON_ID);
/** @var User $prWlUser */
$prWlUser = $this->getReference('test_user_'.LoadUserData::USER_PRWL_ID);
$longPost = (new Post('longpost', $testUser, new \DateTime(), Post::TYPE_POST)) $longPost = (new Post(self::POST_ID_LONG, $mainUser, new \DateTime(), Post::TYPE_POST))
->setText('Test post with many comments') ->setText('Test post with many comments')
->setPrivate(false) ->setPrivate(false)
->setDeleted(false) ->setDeleted(false)
; ;
$shortPost = (new Post('shortpost', $testUser, new \DateTime(), Post::TYPE_POST)) $shortPost = (new Post(self::POST_ID_SHORT, $mainUser, new \DateTime(), Post::TYPE_POST))
->setText('Test short post') ->setText('Test short post')
->setPrivate(false) ->setPrivate(false)
->setDeleted(false) ->setDeleted(false)
; ;
$privateUserPost = (new Post(self::POST_ID_PR_USER, $privateUser, new \DateTime(), Post::TYPE_POST))
->setText('Post from private user. Should not be visible in the public feed.')
->setPrivate(false)
->setDeleted(false)
;
$wlUserPost = (new Post(self::POST_ID_WL_USER, $wlUser, new \DateTime(), Post::TYPE_POST))
->setText('Post from whitelist-only user. Should only be visible for whitelisted users.')
->setPrivate(false)
->setDeleted(false)
;
$privateWlUserPost = (new Post(self::POST_ID_PR_WL_USER, $prWlUser, new \DateTime(), Post::TYPE_POST))
->setText('Post from private AND whitelist-only user. Should not be visible in the public feed.')
->setPrivate(false)
->setDeleted(false)
;
$om->persist($longPost); $om->persist($longPost);
$om->persist($shortPost); $om->persist($shortPost);
$om->persist($privateUserPost);
$om->persist($wlUserPost);
$om->persist($privateWlUserPost);
$om->flush(); $om->flush();
$this->addReference('test_post_longpost', $longPost); $this->addReference('test_post_longpost', $longPost);

View File

@ -9,25 +9,27 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
class LoadUserData extends AbstractFixture implements OrderedFixtureInterface class LoadUserData extends AbstractFixture implements OrderedFixtureInterface
{ {
public const USER_MAIN_ID = 99999;
public const USER_SCND_ID = 99998;
public const USER_PRIV_ID = 99997;
public const USER_WLON_ID = 99996;
public const USER_PRWL_ID = 99995;
public const USER_UNNM_ID = 99994;
private $users = [ private $users = [
// 99999 ['id' => self::USER_MAIN_ID, 'login' => 'testuser', 'name' => 'Test User 1', 'private' => false, 'whitelist-only' => false],
['login' => 'testuser', 'name' => 'Test User 1'], ['id' => self::USER_SCND_ID, 'login' => 'testuser2', 'name' => 'Test User 2 for autocomplete test', 'private' => false, 'whitelist-only' => false],
// 99998 ['id' => self::USER_PRIV_ID, 'login' => 'private_user', 'name' => 'Test User 3', 'private' => true, 'whitelist-only' => false],
['login' => 'testuser2', 'name' => 'Test User 2'], ['id' => self::USER_WLON_ID, 'login' => 'whitelist_only_user', 'name' => 'Test User 4', 'private' => false, 'whitelist-only' => true],
// 99997 ['id' => self::USER_PRWL_ID, 'login' => 'private_whitelist_only_user', 'name' => 'Test User 4', 'private' => false, 'whitelist-only' => true],
['login' => 'testuser3', 'name' => 'Test User 3'], ['id' => self::USER_UNNM_ID, 'login' => 'unnamed_user', 'name' => null, 'private' => false, 'whitelist-only' => false],
// 99996
['login' => 'testuser4', 'name' => 'Test User 4'],
//99995
['login' => 'testuser5', 'name' => null],
]; ];
public function load(ObjectManager $om) public function load(ObjectManager $om)
{ {
$userId = 99999;
foreach ($this->users as $userData) { foreach ($this->users as $userData) {
$user = new User($userId--, new \DateTime(), $userData['login'], $userData['name']); $user = new User($userData['id'], new \DateTime(), $userData['login'], $userData['name']);
$user->updatePrivacy(!$userData['private'], $userData['whitelist-only']);
$om->persist($user); $om->persist($user);

View File

@ -9,8 +9,6 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
/** /**
* @ORM\Table(name="comments", schema="posts", indexes={ * @ORM\Table(name="comments", schema="posts", indexes={
* @ORM\Index(name="idx_comment_created_at", columns={"created_at"}) * @ORM\Index(name="idx_comment_created_at", columns={"created_at"})
* }, uniqueConstraints={
* @ORM\UniqueConstraint(name="unique_post_id_comment_number", columns={"post_id", "number"})
* }) * })
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Repository\Blogs\CommentRepository") * @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Repository\Blogs\CommentRepository")
*/ */
@ -64,17 +62,10 @@ class Comment
/** /**
* @var int * @var int
* *
* @ORM\Column(name="number", type="integer", unique=true) * @ORM\Column(name="number", type="smallint")
*/ */
private $number; private $number;
/**
* @var int|null
*
* @ORM\Column(name="to_number", type="integer", nullable=true)
*/
private $toNumber;
/** /**
* @var User * @var User
* *
@ -94,36 +85,26 @@ class Comment
*/ */
private $files; private $files;
/**
* @var Comment|null
*
* @ORM\ManyToOne(targetEntity="Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Comment", inversedBy="children")
* @ORM\JoinColumn(name="parent_id", nullable=true)
*/
private $parent;
public function __construct( /**
string $text, * @var Comment[]|ArrayCollection
\DateTime $createdAt, *
bool $rec, * @ORM\OneToMany(targetEntity="Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Comment", fetch="EXTRA_LAZY", mappedBy="parent")
Post $post, */
int $number, private $children;
?int $toNumber,
User $author,
array $files
) {
$this->text = $text;
$this->createdAt = $createdAt;
$this->rec = $rec;
$this->post = $post;
$this->number = $number;
$this->toNumber = $toNumber;
$this->author = $author;
public function __construct()
{
$this->files = new ArrayCollection(); $this->files = new ArrayCollection();
foreach ($files as $file) { $this->children = new ArrayCollection();
if (!($file instanceof File)) {
throw new \RuntimeException(sprintf(
'$files array must contain only \'%s\' objects. %s given.',
\is_object($file) ? \get_class($file) : \gettype($file)
));
}
$this->files->add($file);
}
} }
public function getId(): int public function getId(): int
@ -131,41 +112,95 @@ class Comment
return $this->id; return $this->id;
} }
public function setCreatedAt(\DateTime $createdAt): self
{
$this->createdAt = $createdAt;
return $this;
}
public function getCreatedAt(): \DateTime public function getCreatedAt(): \DateTime
{ {
return $this->createdAt; return $this->createdAt;
} }
public function setText(string $text): self
{
$this->text = $text;
return $this;
}
public function getText(): string public function getText(): string
{ {
return $this->text; return $this->text;
} }
public function setRec(bool $rec): self
{
$this->rec = $rec;
return $this;
}
public function isRec(): bool public function isRec(): bool
{ {
return $this->rec; return $this->rec;
} }
public function getRec(): bool
{
return $this->rec;
}
public function getPost(): Post public function getPost(): Post
{ {
return $this->post; return $this->post;
} }
public function setPost(Post $post): self
{
$this->post = $post;
return $this;
}
public function setNumber(int $number): self
{
$this->number = $number;
return $this;
}
public function getNumber(): int public function getNumber(): int
{ {
return $this->number; return $this->number;
} }
public function getToNumber(): ?int
{
return $this->toNumber;
}
public function getAuthor(): User public function getAuthor(): User
{ {
return $this->author; return $this->author;
} }
public function setAuthor(User $author): self
{
$this->author = $author;
return $this;
}
public function addFile(File $files): self
{
$this->files[] = $files;
return $this;
}
public function removeFile(File $files): void
{
$this->files->removeElement($files);
}
/** /**
* @return File[]|ArrayCollection * @return File[]|ArrayCollection
*/ */
@ -174,22 +209,52 @@ class Comment
return $this->files; return $this->files;
} }
public function delete(): self public function getParent(): ?Comment
{ {
$this->deleted = true; return $this->parent;
}
public function setParent(Comment $parent): self
{
$this->parent = $parent;
return $this; return $this;
} }
public function restore(): self public function setDeleted(bool $deleted): self
{ {
$this->deleted = false; $this->deleted = $deleted;
return $this; return $this;
} }
public function getDeleted(): bool
{
return $this->deleted;
}
public function isDeleted(): bool public function isDeleted(): bool
{ {
return $this->deleted; return $this->deleted;
} }
public function addChild(Comment $children): self
{
$this->children[] = $children;
return $this;
}
public function removeChild(Comment $children): void
{
$this->children->removeElement($children);
}
/**
* @return Comment[]|ArrayCollection
*/
public function getChildren(): iterable
{
return $this->children;
}
} }

View File

@ -69,13 +69,6 @@ class Post
*/ */
private $deleted = false; private $deleted = false;
/**
* @var bool Status of point-tools subscription to the post (to receive WS updates)
*
* @ORM\Column(name="is_subscribed", type="boolean", options={"default": false})
*/
private $subscribed = false;
/** /**
* @var User * @var User
* *
@ -110,7 +103,7 @@ class Post
private $comments; private $comments;
public function __construct(string $id, User $author, \DateTime $createdAt, string $type = self::TYPE_POST) public function __construct(string $id, User $author, \DateTime $createdAt, string $type)
{ {
$this->id = $id; $this->id = $id;
$this->author = $author; $this->author = $author;
@ -223,25 +216,6 @@ class Post
{ {
return $this->deleted; return $this->deleted;
} }
public function isSubscribed(): bool
{
return $this->subscribed;
}
public function subscribe(): self
{
$this->subscribed = true;
return $this;
}
public function unsubscribe(): self
{
$this->subscribed = false;
return $this;
}
public function setPrivate(bool $private): self public function setPrivate(bool $private): self
{ {
@ -263,6 +237,7 @@ class Post
public function addComment(Comment $comment): self public function addComment(Comment $comment): self
{ {
$this->comments[] = $comment; $this->comments[] = $comment;
$comment->setPost($this);
return $this; return $this;
} }

View File

@ -7,8 +7,8 @@ use Skobkin\Bundle\PointToolsBundle\Entity\User;
/** /**
* @ORM\Table(name="telegram_accounts", schema="users", indexes={ * @ORM\Table(name="telegram_accounts", schema="users", indexes={
* @ORM\Index(name="subscriber_notification_idx", columns={"subscriber_notification"}, options={"where": "(subscriber_notification = true)"}), * @ORM\Index(name="subscriber_notification_idx", columns={"subscriber_notification"}, options={"where": "subscriber_notification = TRUE"}),
* @ORM\Index(name="rename_notification_idx", columns={"rename_notification"}, options={"where": "(rename_notification = true)"}), * @ORM\Index(name="rename_notification_idx", columns={"rename_notification"}, options={"where": "rename_notification = TRUE"}),
* }) * })
* @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Repository\Telegram\AccountRepository") * @ORM\Entity(repositoryClass="Skobkin\Bundle\PointToolsBundle\Repository\Telegram\AccountRepository")
* @ORM\HasLifecycleCallbacks() * @ORM\HasLifecycleCallbacks()

View File

@ -7,7 +7,6 @@ use Doctrine\ORM\Mapping as ORM;
/** /**
* @ORM\Table(name="users", schema="users", indexes={ * @ORM\Table(name="users", schema="users", indexes={
* @ORM\Index(name="idx_user_login", columns={"login"}),
* @ORM\Index(name="idx_user_public", columns={"public"}), * @ORM\Index(name="idx_user_public", columns={"public"}),
* @ORM\Index(name="idx_user_removed", columns={"is_removed"}) * @ORM\Index(name="idx_user_removed", columns={"is_removed"})
* }) * })
@ -191,24 +190,15 @@ class User
return $this->createdAt; return $this->createdAt;
} }
public function updateCreatedAt(\DateTime $date): self
{
$this->createdAt = $date;
return $this;
}
public function getUpdatedAt(): ?\DateTime public function getUpdatedAt(): ?\DateTime
{ {
return $this->updatedAt; return $this->updatedAt;
} }
public function updatePrivacy(bool $public, bool $whitelistOnly): self public function updatePrivacy(?bool $public, ?bool $whitelistOnly): void
{ {
$this->public = $public; $this->public = $public;
$this->whitelistOnly = $whitelistOnly; $this->whitelistOnly = $whitelistOnly;
return $this;
} }
public function isPublic(): ?bool public function isPublic(): ?bool

View File

@ -1,20 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Exception\Api;
class PostNotFoundException extends NotFoundException
{
/** @var string */
private $id;
public function __construct(string $id, \Exception $previous)
{
parent::__construct('Post not found', 0, $previous);
$this->id = $id;
}
public function getId(): string
{
return $this->id;
}
}

View File

@ -4,11 +4,16 @@ namespace Skobkin\Bundle\PointToolsBundle\Exception\Api;
class UserNotFoundException extends NotFoundException class UserNotFoundException extends NotFoundException
{ {
/** @var int */ /**
private $userId; * @var int
*/
protected $userId;
/**
* @var string
*/
protected $login;
/** @var string */
private $login;
/** /**
* {@inheritdoc} * {@inheritdoc}

View File

@ -1,9 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Exception\WebSocket;
use RuntimeException;
class UnsupportedTypeException extends RuntimeException
{
}

View File

@ -2,6 +2,10 @@ Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment:
exclusion_policy: none exclusion_policy: none
access_type: public_method access_type: public_method
properties: properties:
postId:
serialized_name: 'post_id'
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post'
max_depth: 2
number: number:
serialized_name: 'id' serialized_name: 'id'
type: integer type: integer

View File

@ -1,9 +1,10 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost: Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost:
exclusion_policy: none exclusion_policy: none
access_type: public_method
properties: properties:
post: post:
serialized_name: 'post'
type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post' type: 'Skobkin\Bundle\PointToolsBundle\DTO\Api\Post'
max_depth: 2
comments: comments:
serialized_name: 'comments'
type: 'array<Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment>' type: 'array<Skobkin\Bundle\PointToolsBundle\DTO\Api\Comment>'
max_depth: 2

View File

@ -1,60 +0,0 @@
Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message:
exclusion_policy: none
properties:
a:
type: string
serialized_name: 'a'
author:
type: string
serialized_name: 'author'
authorId:
type: int
serialized_name: 'author_id'
authorName:
type: string
serialized_name: 'author_name'
commentId:
type: int
serialized_name: 'comment_id'
cut:
type: bool
serialized_name: 'cut'
files:
type: array<string>
serialized_name: 'files'
html:
type: string
serialized_name: 'html'
link:
type: string
serialized_name: 'link'
postId:
type: string
serialized_name: 'post_id'
postAuthorId:
type: int
serialized_name: 'post_author_id'
private:
type: bool
serialized_name: 'private'
rcid:
type: int
serialized_name: 'rcid'
tags:
type: array<string>
serialized_name: 'tags'
text:
type: string
serialized_name: 'text'
title:
type: string
serialized_name: 'title'
toCommentId:
type: string
serialized_name: 'to_comment_id'
toText:
type: string
serialized_name: 'to_text'
toUsers:
type: array<string>
serialized_name: 'to_users'

View File

@ -5,9 +5,6 @@ namespace Skobkin\Bundle\PointToolsBundle\Service\Api;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use JMS\Serializer\SerializerInterface; use JMS\Serializer\SerializerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\MetaPost;
use Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Post;
use Skobkin\Bundle\PointToolsBundle\Exception\Api\{NotFoundException, PostNotFoundException};
use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\PostFactory; use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\PostFactory;
/** /**
@ -15,8 +12,6 @@ use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\PostFactory;
*/ */
class PostApi extends AbstractApi class PostApi extends AbstractApi
{ {
private const PREFIX = '/api/post/';
/** /**
* @var PostFactory * @var PostFactory
*/ */
@ -29,24 +24,4 @@ class PostApi extends AbstractApi
$this->postFactory = $postFactory; $this->postFactory = $postFactory;
} }
/**
* @throws PostNotFoundException
*/
public function getById(string $id): Post
{
try {
$postData = $this->getGetJsonData(
self::PREFIX.$id,
[],
MetaPost::class
);
} catch (NotFoundException $e) {
throw new PostNotFoundException($id, $e);
}
// Not catching ForbiddenException right now
return $this->postFactory->findOrCreateFromDtoWithContent($postData);
}
} }

View File

@ -3,7 +3,9 @@
namespace Skobkin\Bundle\PointToolsBundle\Service\Api; namespace Skobkin\Bundle\PointToolsBundle\Service\Api;
use GuzzleHttp\ClientInterface; use GuzzleHttp\ClientInterface;
use JMS\Serializer\{DeserializationContext, SerializerInterface}; use JMS\Serializer\{
DeserializationContext, SerializerInterface
};
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\{Auth, User as UserDTO}; use Skobkin\Bundle\PointToolsBundle\DTO\Api\{Auth, User as UserDTO};
use Skobkin\Bundle\PointToolsBundle\Entity\User; use Skobkin\Bundle\PointToolsBundle\Entity\User;

View File

@ -6,8 +6,6 @@ use Psr\Log\LoggerInterface;
abstract class AbstractFactory abstract class AbstractFactory
{ {
public const DATE_FORMAT = 'Y-m-d_H:i:s';
/** @var LoggerInterface */ /** @var LoggerInterface */
protected $logger; protected $logger;

View File

@ -3,10 +3,7 @@
namespace Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs; namespace Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message;
use Skobkin\Bundle\PointToolsBundle\Entity\Blogs\Comment;
use Skobkin\Bundle\PointToolsBundle\Repository\Blogs\{CommentRepository, PostRepository}; use Skobkin\Bundle\PointToolsBundle\Repository\Blogs\{CommentRepository, PostRepository};
use Skobkin\Bundle\PointToolsBundle\Service\Api\PostApi;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\{AbstractFactory, UserFactory}; use Skobkin\Bundle\PointToolsBundle\Service\Factory\{AbstractFactory, UserFactory};
class CommentFactory extends AbstractFactory class CommentFactory extends AbstractFactory
@ -20,45 +17,12 @@ class CommentFactory extends AbstractFactory
/** @var UserFactory */ /** @var UserFactory */
private $userFactory; private $userFactory;
/** @var PostApi */
private $postApi;
public function __construct(LoggerInterface $logger, CommentRepository $commentRepository, PostRepository $postRepository, UserFactory $userFactory)
public function __construct(LoggerInterface $logger, CommentRepository $commentRepository, PostRepository $postRepository, UserFactory $userFactory, PostApi $postApi)
{ {
parent::__construct($logger); parent::__construct($logger);
$this->userFactory = $userFactory; $this->userFactory = $userFactory;
$this->commentRepository = $commentRepository; $this->commentRepository = $commentRepository;
$this->postRepository = $postRepository; $this->postRepository = $postRepository;
$this->postApi = $postApi;
}
public function findOrCreateFromWebsocketMessage(Message $message): Comment
{
if ($message->isValid()) {
throw new \InvalidArgumentException('Comment is invalid');
}
if ($message->isComment()) {
throw new \InvalidArgumentException(sprintf(
'Invalid Message object provided. %s expected, %s given',
Message::TYPE_COMMENT,
$message->getA()
));
}
if (null === $comment = $this->commentRepository->findOneBy(['post' => $post, 'number' => $message->getCommentId()])) {
$author = $this->userFactory->findOrCreateFromIdLoginAndName(
$message->getAuthorId(),
$message->getAuthor(),
$message->getAuthorName()
);
if (null === $post = $this->postRepository->find($message->getPostId())) {
$post = $this->postApi->getById($message->getPostId());
}
// TODO
//$comment = new Comment()
}
} }
} }

View File

@ -4,11 +4,10 @@ namespace Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Psr\Log\LoggerInterface; use Psr\Log\LoggerInterface;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\{MetaPost, Post as ApiPost, PostsPage}; use Skobkin\Bundle\PointToolsBundle\DTO\Api\{MetaPost, Post as PostDTO, PostsPage};
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message as WebsocketMessage;
use Skobkin\Bundle\PointToolsBundle\Entity\{Blogs\Post, Blogs\PostTag, User}; use Skobkin\Bundle\PointToolsBundle\Entity\{Blogs\Post, Blogs\PostTag, User};
use Skobkin\Bundle\PointToolsBundle\Exception\{Api\InvalidResponseException, Factory\Blog\InvalidDataException}; use Skobkin\Bundle\PointToolsBundle\Exception\{Api\InvalidResponseException, Factory\Blog\InvalidDataException};
use Skobkin\Bundle\PointToolsBundle\Repository\{Blogs\PostRepository, UserRepository}; use Skobkin\Bundle\PointToolsBundle\Repository\Blogs\PostRepository;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\{AbstractFactory, UserFactory}; use Skobkin\Bundle\PointToolsBundle\Service\Factory\{AbstractFactory, UserFactory};
class PostFactory extends AbstractFactory class PostFactory extends AbstractFactory
@ -19,9 +18,6 @@ class PostFactory extends AbstractFactory
/** @var PostRepository */ /** @var PostRepository */
private $postRepository; private $postRepository;
/** @var UserRepository */
private $userRepository;
/** @var UserFactory */ /** @var UserFactory */
private $userFactory; private $userFactory;
@ -39,7 +35,6 @@ class PostFactory extends AbstractFactory
LoggerInterface $logger, LoggerInterface $logger,
EntityManagerInterface $em, EntityManagerInterface $em,
PostRepository $postRepository, PostRepository $postRepository,
UserRepository $userRepository,
UserFactory $userFactory, UserFactory $userFactory,
FileFactory $fileFactory, FileFactory $fileFactory,
CommentFactory $commentFactory, CommentFactory $commentFactory,
@ -48,7 +43,6 @@ class PostFactory extends AbstractFactory
parent::__construct($logger); parent::__construct($logger);
$this->em = $em; $this->em = $em;
$this->postRepository = $postRepository; $this->postRepository = $postRepository;
$this->userRepository = $userRepository;
$this->userFactory = $userFactory; $this->userFactory = $userFactory;
$this->fileFactory = $fileFactory; $this->fileFactory = $fileFactory;
$this->commentFactory = $commentFactory; $this->commentFactory = $commentFactory;
@ -98,6 +92,8 @@ class PostFactory extends AbstractFactory
/** /**
* Create full post with tags, files and comments * Create full post with tags, files and comments
* *
* @todo Implement comments
*
* @throws InvalidDataException * @throws InvalidDataException
*/ */
public function findOrCreateFromDtoWithContent(MetaPost $metaPost): Post public function findOrCreateFromDtoWithContent(MetaPost $metaPost): Post
@ -115,7 +111,7 @@ class PostFactory extends AbstractFactory
throw $e; throw $e;
} }
$post = $this->findOrCreateFromApiDto($postData, $author); $post = $this->findOrCreateFromDto($postData, $author);
try { try {
$this->updatePostTags($post, $postData->getTags() ?: []); $this->updatePostTags($post, $postData->getTags() ?: []);
@ -131,62 +127,10 @@ class PostFactory extends AbstractFactory
throw $e; throw $e;
} }
// @TODO implement comments
return $post; return $post;
} }
public function findOrCreateFromWebsocketDto(WebsocketMessage $message): Post private function findOrCreateFromDto(PostDTO $postData, User $author): Post
{
if (!$message->isValid()) {
throw new InvalidDataException('Invalid post data');
}
if (!$message->isPost()) {
throw new \LogicException(sprintf(
'Incorrect message type received. \'post\' expected \'%s\' given',
$message->getA()
));
}
if (null === $post = $this->postRepository->find($message->getPostId())) {
$author = $this->userFactory->findOrCreateFromIdLoginAndName(
$message->getAuthorId(),
$message->getAuthor(),
$message->getAuthorName()
);
$post = new Post(
$message->getPostId(),
$author,
new \DateTime(),
Post::TYPE_POST
);
$this->postRepository->add($post);
}
$post
->setText($message->getText())
->setPrivate((bool) $message->getPrivate())
;
try {
$this->updatePostTags($post, $message->getTags() ?: []);
} catch (\Exception $e) {
$this->logger->error('Error while updating post tags');
throw $e;
}
try {
$this->updatePostFiles($post, $message->getFiles() ?: []);
} catch (\Exception $e) {
$this->logger->error('Error while updating post files');
throw $e;
}
return $post;
}
private function findOrCreateFromApiDto(ApiPost $postData, User $author): Post
{ {
if (null === ($post = $this->postRepository->find($postData->getId()))) { if (null === ($post = $this->postRepository->find($postData->getId()))) {
// Creating new post // Creating new post
@ -201,7 +145,7 @@ class PostFactory extends AbstractFactory
$post $post
->setText($postData->getText()) ->setText($postData->getText())
->setPrivate((bool) $postData->getPrivate()) ->setPrivate($postData->getPrivate())
; ;
return $post; return $post;

View File

@ -10,9 +10,12 @@ use Skobkin\Bundle\PointToolsBundle\Exception\Factory\InvalidUserDataException;
class UserFactory extends AbstractFactory class UserFactory extends AbstractFactory
{ {
public const DATE_FORMAT = 'Y-m-d_H:i:s';
/** @var UserRepository */ /** @var UserRepository */
private $userRepository; private $userRepository;
public function __construct(LoggerInterface $logger, UserRepository $userRepository) public function __construct(LoggerInterface $logger, UserRepository $userRepository)
{ {
parent::__construct($logger); parent::__construct($logger);
@ -20,25 +23,27 @@ class UserFactory extends AbstractFactory
} }
/** /**
* @param UserDTO $userData
*
* @return User
*
* @throws InvalidUserDataException * @throws InvalidUserDataException
*/ */
public function findOrCreateFromDTO(UserDTO $userData): User public function findOrCreateFromDTO(UserDTO $userData): User
{ {
// @todo LOG
if (!$userData->isValid()) { if (!$userData->isValid()) {
throw new InvalidUserDataException('Invalid user data', $userData); throw new InvalidUserDataException('Invalid user data', $userData);
} }
$createdAt = \DateTime::createFromFormat(self::DATE_FORMAT, $userData->getCreated()) ?: new \DateTime();
/** @var User $user */ /** @var User $user */
if (null === ($user = $this->userRepository->find($userData->getId()))) { if (null === ($user = $this->userRepository->find($userData->getId()))) {
$user = new User( $user = new User(
$userData->getId(), $userData->getId(),
$createdAt \DateTime::createFromFormat('Y-m-d_H:i:s', $userData->getCreated()) ?: new \DateTime()
); );
$this->userRepository->add($user); $this->userRepository->add($user);
} else {
$user->updateCreatedAt($createdAt);
} }
$user->updateLoginAndName($userData->getLogin(), $userData->getName()); $user->updateLoginAndName($userData->getLogin(), $userData->getName());
@ -50,21 +55,6 @@ class UserFactory extends AbstractFactory
return $user; return $user;
} }
public function findOrCreateFromIdLoginAndName(int $id, string $login, ?string $name): User
{
/** @var User $user */
if (null === $user = $this->userRepository->find($id)) {
// We're using current date now but next time when we'll be updating user from API it'll be fixed
$user = new User($id, new \DateTime(), $login, $name);
$this->userRepository->add($user);
} else {
// @todo update login?
// Probably don't because no name in the WS message (or maybe after PR to point?)
}
return $user;
}
/** /**
* @return User[] * @return User[]
*/ */

View File

@ -1,69 +0,0 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service\WebSocket;
use Skobkin\Bundle\PointToolsBundle\DTO\Api\WebSocket\Message;
use Skobkin\Bundle\PointToolsBundle\Service\Factory\Blogs\{CommentFactory, PostFactory};
class WebSocketMessageProcessor
{
/** @var PostFactory */
private $postFactory;
/** @var CommentFactory */
private $commentFactory;
public function __construct(PostFactory $postFactory, CommentFactory $commentFactory)
{
$this->postFactory = $postFactory;
$this->commentFactory = $commentFactory;
}
/**
* Returns true on success (all data saved)
*/
public function processMessage(Message $message): bool
{
if (!$message->isValid()) {
return false;
}
switch (true) {
case $message->isPost():
return $this->processPost($message);
break;
case $message->isComment():
return $this->processComment($message);
break;
case $message->isCommentRecommendation():
return $this->processRecommendation($message);
break;
}
return false;
}
private function processPost(Message $postData): bool
{
$this->postFactory->findOrCreateFromWebsocketDto($postData);
return true;
}
private function processComment(Message $commentData): bool
{
// Not done yet
return false;
$this->commentFactory->findOrCreateFromWebsocketMessage($commentData);
return true;
}
private function processRecommendation(Message $recommendData): bool
{
return false;
}
}

View File

@ -2,11 +2,12 @@
namespace Tests\Skobkin\PointToolsBundle\Controller; namespace Tests\Skobkin\PointToolsBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Client;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
class MainControllerTest extends WebTestCase class MainControllerTest extends WebTestCase
{ {
public function testUserSearch() public function testUserSearch(): void
{ {
$client = static::createClient(); $client = static::createClient();
$crawler = $client->request('GET', '/'); $crawler = $client->request('GET', '/');
@ -19,7 +20,7 @@ class MainControllerTest extends WebTestCase
$this->assertTrue($client->getResponse()->isRedirect('/user/testuser'), 'Redirect to testuser\'s page didn\'t happen'); $this->assertTrue($client->getResponse()->isRedirect('/user/testuser'), 'Redirect to testuser\'s page didn\'t happen');
} }
public function testNonExistingUserSearch() public function testNonExistingUserSearch(): void
{ {
$client = static::createClient(); $client = static::createClient();
$crawler = $client->request('GET', '/'); $crawler = $client->request('GET', '/');
@ -49,7 +50,7 @@ class MainControllerTest extends WebTestCase
$this->assertEquals(' Login not found', $firstError->text(), 'Incorrect error text'); $this->assertEquals(' Login not found', $firstError->text(), 'Incorrect error text');
} }
public function testUserStats() public function testUserStats(): void
{ {
$client = static::createClient(); $client = static::createClient();
$crawler = $client->request('GET', '/'); $crawler = $client->request('GET', '/');
@ -75,14 +76,10 @@ class MainControllerTest extends WebTestCase
/** /**
* Tests AJAX user search autocomplete and returns JSON response string * Tests AJAX user search autocomplete and returns JSON response string
*
* @return string
*/ */
public function testAjaxUserAutoComplete() public function testAjaxUserAutoComplete(): string
{ {
$client = static::createClient(); $client = $this->createClientForAjaxUserSearchByLogin('testuser');
// We need to search all test user with 'testuser5' included which will test the code against null-string problem in User#getName()
$client->request('GET', '/ajax/users/search/testuser');
$this->assertTrue($client->getResponse()->headers->contains('Content-Type', 'application/json'), 'Response has "Content-Type" = "application/json"'); $this->assertTrue($client->getResponse()->headers->contains('Content-Type', 'application/json'), 'Response has "Content-Type" = "application/json"');
@ -91,26 +88,22 @@ class MainControllerTest extends WebTestCase
/** /**
* @depends testAjaxUserAutoComplete * @depends testAjaxUserAutoComplete
*
* @param $json
*/ */
public function testAjaxUserAutoCompleteHasOptions($json) public function testAjaxUserAutoCompleteHasOptions(string $json): array
{ {
$data = json_decode($json); $data = json_decode($json, true);
$this->assertNotNull($data, 'JSON data successfully decoded and not empty'); $this->assertNotNull($data, 'JSON data successfully decoded and not empty');
$this->assertTrue(is_array($data), 'JSON data is array'); $this->assertTrue(is_array($data), 'JSON data is array');
$this->assertCount(5, $data, 'Array has 5 elements'); $this->assertCount(2, $data, 'Array has 2 elements');
return $data; return $data;
} }
/** /**
* @depends testAjaxUserAutoCompleteHasOptions * @depends testAjaxUserAutoCompleteHasOptions
*
* @param array $users
*/ */
public function testAjaxUserAutoCompleteHasValidUserObjects(array $users) public function testAjaxUserAutoCompleteHasValidUserObjects(array $users): void
{ {
foreach ($users as $key => $user) { foreach ($users as $key => $user) {
$this->assertTrue(array_key_exists('login', $user), sprintf('%d row of result has \'login\' field', $key)); $this->assertTrue(array_key_exists('login', $user), sprintf('%d row of result has \'login\' field', $key));
@ -118,7 +111,43 @@ class MainControllerTest extends WebTestCase
} }
} }
public function testAjaxUserAutoCompleteForNonExistingUser() /**
* Tests AJAX user search autocomplete for unnamed user and returns JSON response string
*/
public function testAjaxUserAutoCompleteForUnnamedUser(): string
{
$client = $this->createClientForAjaxUserSearchByLogin('unnamed_user');
$this->assertTrue($client->getResponse()->headers->contains('Content-Type', 'application/json'), 'Response has "Content-Type" = "application/json"');
return $client->getResponse()->getContent();
}
/**
* @depends testAjaxUserAutoCompleteForUnnamedUser
*/
public function testAjaxUserAutoCompleteHasOptionsForUnnamedUser(string $json): array
{
$data = json_decode($json, true);
$this->assertNotNull($data, 'JSON data successfully decoded and not empty');
$this->assertInternalType('array', $data, 'JSON data is array');
$this->assertCount(1, $data, 'Array has 1 elements');
return reset($data);
}
/**
* @depends testAjaxUserAutoCompleteHasOptionsForUnnamedUser
*/
public function testAjaxUserAutoCompleteHasValidUserObjectsForUnnamedUser(array $user): void
{
$this->assertTrue(array_key_exists('login', $user), 'Result has \'login\' field');
$this->assertTrue(array_key_exists('name', $user), 'Result has \'name\' field');
$this->assertEquals(true, ('' === $user['name'] || null === $user['name']), 'User name is empty string or null');
}
public function testAjaxUserAutoCompleteIsEmptyForNonExistingUser(): void
{ {
$client = static::createClient(); $client = static::createClient();
$client->request('GET', '/ajax/users/search/aksdjhaskdjhqwhdgqkjwhdgkjah'); $client->request('GET', '/ajax/users/search/aksdjhaskdjhqwhdgqkjwhdgkjah');
@ -128,7 +157,15 @@ class MainControllerTest extends WebTestCase
$data = json_decode($client->getResponse()->getContent()); $data = json_decode($client->getResponse()->getContent());
$this->assertNotNull($data, 'JSON data successfully decoded and not empty'); $this->assertNotNull($data, 'JSON data successfully decoded and not empty');
$this->assertTrue(is_array($data), 'JSON data is array'); $this->assertInternalType('array', $data, 'JSON data is array');
$this->assertEquals(0, count($data), 'Array has no elements'); $this->assertCount(0, $data, 'Array has no elements');
}
private function createClientForAjaxUserSearchByLogin(string $login): Client
{
$client = static::createClient();
$client->request('GET', '/ajax/users/search/'.$login);
return $client;
} }
} }

View File

@ -2,15 +2,15 @@
namespace Tests\Skobkin\PointToolsBundle\Controller; namespace Tests\Skobkin\PointToolsBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; use Skobkin\Bundle\PointToolsBundle\DataFixtures\ORM\LoadPostData;
use Symfony\Bundle\FrameworkBundle\{Client, Test\WebTestCase};
use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\DomCrawler\Crawler;
class PostControllerTest extends WebTestCase class PostControllerTest extends WebTestCase
{ {
public function testNonExistingPostPage() public function testNonExistingPostPage()
{ {
$client = static::createClient(); $client = $this->createClientForPostId('nonexistingpost');
$client->request('GET', '/nonexistingpost');
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for non-existing post'); $this->assertTrue($client->getResponse()->isNotFound(), '404 response code for non-existing post');
} }
@ -20,12 +20,11 @@ class PostControllerTest extends WebTestCase
*/ */
public function testShortPostPageIsOk() public function testShortPostPageIsOk()
{ {
$client = static::createClient(); $client = $this->createClientForPostId(LoadPostData::POST_ID_SHORT);
$crawler = $client->request('GET', '/shortpost');
$this->assertTrue($client->getResponse()->isOk(), '200 response code for existing post'); $this->assertTrue($client->getResponse()->isOk(), '200 response code for existing post');
return $crawler; return $client->getCrawler();
} }
/** /**
@ -58,4 +57,33 @@ class PostControllerTest extends WebTestCase
$this->assertEquals(1, $p->count(), '.post-text has zero or more than one paragraphs'); $this->assertEquals(1, $p->count(), '.post-text has zero or more than one paragraphs');
$this->assertEquals('Test short post', $p->text(), '.post-text has no correct post text'); $this->assertEquals('Test short post', $p->text(), '.post-text has no correct post text');
} }
public function testPrivateUserPostForbidden()
{
$client = $this->createClientForPostId(LoadPostData::POST_ID_PR_USER);
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for private user\'s post');
}
public function testWhitelistOnlyUserPostForbidden()
{
$client = $this->createClientForPostId(LoadPostData::POST_ID_WL_USER);
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for whitelist-only user\'s post');
}
public function testPrivateWhitelistOnlyUserPostForbidden()
{
$client = $this->createClientForPostId(LoadPostData::POST_ID_PR_WL_USER);
$this->assertTrue($client->getResponse()->isNotFound(), '404 response code for private whitelist-only user\'s post');
}
private function createClientForPostId(string $id): Client
{
$client = static::createClient();
$client->request('GET', '/'.$id);
return $client;
}
} }

View File

@ -32,7 +32,7 @@ class UserRepositoryTest extends KernelTestCase
{ {
$users = $this->userRepo->findAll(); $users = $this->userRepo->findAll();
$this->assertCount(5, $users, 'Not exactly 5 users in the databas'); $this->assertCount(6, $users, 'Not exactly 6 users in the databas');
} }
public function testFindOneByLogin() public function testFindOneByLogin()
@ -58,14 +58,14 @@ class UserRepositoryTest extends KernelTestCase
// Searching LIKE %stus% (testuserX) // Searching LIKE %stus% (testuserX)
$users = $this->userRepo->findUsersLikeLogin('stus'); $users = $this->userRepo->findUsersLikeLogin('stus');
$this->assertCount(5, $users, 'Repository found not exactly 5 users'); $this->assertCount(2, $users, 'Repository found not exactly 5 users');
} }
public function testGetUsersCount() public function testGetUsersCount()
{ {
$count = $this->userRepo->getUsersCount(); $count = $this->userRepo->getUsersCount();
$this->assertEquals(5, $count, 'Counted not exactly 5 users'); $this->assertEquals(6, $count, 'Counted not exactly 5 users');
} }
public function testFindUserSubscribersById() public function testFindUserSubscribersById()