From 5652a5ca681b5a3a09fe0842d737b6b69f24bffb Mon Sep 17 00:00:00 2001 From: Alexey Skobkin Date: Fri, 29 May 2015 01:47:06 +0300 Subject: [PATCH] Test implementation of subscriptions checking command. --- app/config/parameters.yml.dist | 5 + .../Command/UpdateSubscriptionsCommand.php | 58 +++++++ .../Entity/SubscriptionEvent.php | 12 ++ .../Resources/config/services.yml | 12 +- .../PointToolsBundle/Service/AbstractApi.php | 143 +++++++++++++++++- .../Service/SubscriptionsManager.php | 126 +++++++++++++++ .../PointToolsBundle/Service/UserApi.php | 96 +++++++++--- 7 files changed, 422 insertions(+), 30 deletions(-) create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Command/UpdateSubscriptionsCommand.php create mode 100644 src/Skobkin/Bundle/PointToolsBundle/Service/SubscriptionsManager.php diff --git a/app/config/parameters.yml.dist b/app/config/parameters.yml.dist index 48232d5..7e31c09 100644 --- a/app/config/parameters.yml.dist +++ b/app/config/parameters.yml.dist @@ -12,6 +12,11 @@ parameters: mailer_user: ~ mailer_password: ~ + point_base_url: https://point.im/ + point_api_base_url: https://point.im/api/ + point_use_https: true + point_login: point-tools + locale: en # A secret key that's used to generate certain security-related tokens diff --git a/src/Skobkin/Bundle/PointToolsBundle/Command/UpdateSubscriptionsCommand.php b/src/Skobkin/Bundle/PointToolsBundle/Command/UpdateSubscriptionsCommand.php new file mode 100644 index 0000000..7052c6c --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Command/UpdateSubscriptionsCommand.php @@ -0,0 +1,58 @@ +setName('point:update:subscriptions') + ->setDescription('Update subscriptions of users subscribed to service') + ->addOption( + 'check-only', + null, + InputOption::VALUE_NONE, + 'If set, command will not perform write operations in the database' + ) + // @todo add option for checking only selected user + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + /** @var UserApi $api */ + $api = $this->getContainer()->get('skobkin_point_tools.api_user'); + /** @var SubscriptionsManager $subscriptionsManager */ + $subscriptionsManager = $this->getContainer()->get('skobkin_point_tools.subscriptions_manager'); + + $serviceUserName = $this->getContainer()->getParameter('point_login'); + $serviceUser = $this->getContainer()->get('doctrine.orm.entity_manager')->getRepository('SkobkinPointToolsBundle:User')->findOneBy(['login' => $serviceUserName]); + + if (!$serviceUser) { + // @todo Retrieving user + } + + $serviceSubscribers = $api->getUserSubscribersByLogin($serviceUserName); + + // Updating service subscribers + $subscriptionsManager->updateUserSubscribers($serviceUser, $serviceSubscribers); + + // Updating service users subscribers + foreach ($serviceSubscribers as $user) { + $userCurrentSubscribers = $api->getUserSubscribersByLogin($user->getLogin()); + + $subscriptionsManager->updateUserSubscribers($user, $userCurrentSubscribers); + + // @todo some pause for lower API load + } + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Entity/SubscriptionEvent.php b/src/Skobkin/Bundle/PointToolsBundle/Entity/SubscriptionEvent.php index 981912e..2b98686 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Entity/SubscriptionEvent.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Entity/SubscriptionEvent.php @@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping as ORM; * @ORM\Index(name="date_idx", columns={"date"}) * }) * @ORM\Entity + * @ORM\HasLifecycleCallbacks */ class SubscriptionEvent { @@ -58,6 +59,17 @@ class SubscriptionEvent */ private $action; + + /** + * @ORM\PrePersist + */ + public function onCreate() + { + if (!$this->date) { + $this->date = new \DateTime(); + } + } + /** * Get id * diff --git a/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml b/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml index 6b09cc5..f81d137 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml +++ b/src/Skobkin/Bundle/PointToolsBundle/Resources/config/services.yml @@ -1,10 +1,18 @@ services: skobkin_point_tools.http_client: class: %guzzle.client.class% - arguments: [ "http://point.im/" ] + arguments: [ "%point_base_url%" ] tags: - { name: guzzle.client } skobkin_point_tools.api_user: class: Skobkin\Bundle\PointToolsBundle\Service\UserApi - arguments: [ @skobkin_point_tools.http_client ] + arguments: + - @skobkin_point_tools.http_client + - "%point_use_https%" + - "%point_api_base_url%" + - @doctrine.orm.entity_manager + + skobkin_point_tools.subscriptions_manager: + class: Skobkin\Bundle\PointToolsBundle\Service\SubscriptionsManager + arguments: [ @doctrine.orm.entity_manager ] \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php b/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php index cb79f4a..2bcdea5 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/AbstractApi.php @@ -6,7 +6,12 @@ use Guzzle\Service\Client; use Guzzle\Http\Message\Request as GuzzleRequest; use Guzzle\Http\Message\Response as GuzzleResponse; -// @todo Implement commands: https://github.com/misd-service-development/guzzle-bundle/blob/master/Resources/doc/serialization.md +/** + * @todo Implement commands + * @see https://github.com/misd-service-development/guzzle-bundle/blob/master/Resources/doc/serialization.md + * @see https://github.com/misd-service-development/guzzle-bundle/blob/master/Resources/doc/clients.md + * @see https://github.com/misd-service-development/guzzle-bundle/blob/master/Resources/doc/param_converter.md + */ class AbstractApi { /** @@ -14,24 +19,146 @@ class AbstractApi */ protected $client; - public function __construct($httpClient) + /** + * @var bool Use HTTPS instead of HTTP + */ + protected $useHttps; + + /** + * @param Client $httpClient HTTP-client from Guzzle + * @param bool $https Use HTTPS instead of HTTP + * @param string $baseUrl Base URL for API + */ + public function __construct(Client $httpClient, $https = true, $baseUrl = null) { $this->client = $httpClient; + $this->useHttps = ($https) ? true : false; + + if ($baseUrl) { + $this->setBaseUrl($baseUrl); + } } /** * Make GET request and return Response object - * - * @param string $pathTemplate - * @param array $parameters + ** + * @param string $path Request path + * @param array $parameters Key => Value array of query parameters * @return GuzzleResponse */ - public function sendGetRequest($pathTemplate, array $parameters = []) + public function sendGetRequest($path, array $parameters = []) { - $path = vsprintf($pathTemplate, $parameters); - + /** @var $request GuzzleRequest */ $request = $this->client->get($path); + $query = $request->getQuery(); + + foreach ($parameters as $parameter => $value) { + $query->set($parameter, $value); + } + return $request->send(); } + + /** + * Make GET request and return data from response + * + * @param string $path Path template + * @param array $parameters Parameters array used to fill path template + * @param bool $decodeJsonResponse Decode JSON or return plaintext + * @param bool $decodeJsonToObjects Decode JSON objects to PHP objects instead of arrays + * @return array|string + */ + public function getGetRequestData($path, array $parameters = [], $decodeJsonResponse = false, $decodeJsonToObjects = false) + { + $response = $this->sendGetRequest($path, $parameters); + + if ($decodeJsonResponse) { + if ($decodeJsonToObjects) { + return json_decode($response->getBody(true)); + } else { + return $response->json(); + } + } else { + return $response->getBody(true); + } + } + + /** + * Make POST request and return data from response + * @todo implement method + */ + public function getPostRequestData() + { + + } + + /** + * Get HTTP client base URL + * + * @return string Base URL of client + */ + public function getBaseUrl() + { + return $this->client->getBaseUrl(); + } + + /** + * Set HTTP client base URL + * + * @param string $baseUrl Base URL of API + * @param bool $useProtocol Do not change URL scheme (http/https) defined in $baseUrl + * @return $this + */ + public function setBaseUrl($baseUrl, $useProtocol = false) + { + // Overriding protocol + if (!$useProtocol) { + $baseUrl = str_replace(['http://', 'https://',], ($this->useHttps) ? 'https://' : 'http://', $baseUrl); + } + // Adding missing protocol + if ((false === strpos(strtolower($baseUrl), 'http://')) && (false === strpos(strtolower($baseUrl), 'https://'))) { + $baseUrl = (($this->useHttps) ? 'https://' : 'http://') . $baseUrl; + } + + $this->client->setBaseUrl($baseUrl); + + return $this; + } + + /** + * Check if API service uses HTTPS + * + * @return bool + */ + public function isHttps() + { + return $this->useHttps; + } + + /** + * Enable HTTPS + * + * @return $this + */ + public function enableHttps() + { + $this->useHttps = true; + $this->setBaseUrl($this->getBaseUrl()); + + return $this; + } + + /** + * Disable HTTPS + * + * @return $this + */ + public function disableHttps() + { + $this->useHttps = false; + $this->setBaseUrl($this->getBaseUrl()); + + return $this; + } } diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/SubscriptionsManager.php b/src/Skobkin/Bundle/PointToolsBundle/Service/SubscriptionsManager.php new file mode 100644 index 0000000..a9b0a8a --- /dev/null +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/SubscriptionsManager.php @@ -0,0 +1,126 @@ +em = $entityManager; + } + + /** + * @param User $user + * @param User[]|array $newSubscribersList + */ + public function updateUserSubscribers(User $user, $newSubscribersList = []) + { + /** @var Subscription[] $tmpOldSubscribers */ + $tmpOldSubscribers = $user->getSubscribers(); + + $oldSubscribersList = []; + + foreach ($tmpOldSubscribers as $subscription) { + $oldSubscribersList[] = $subscription->getSubscriber(); + } + + unset($tmpOldSubscribers); + + $subscribedList = $this->getUsersListsDiff($newSubscribersList, $oldSubscribersList); + $unsubscribedList = $this->getUsersListsDiff($oldSubscribersList, $newSubscribersList); + + /** @var User $subscribedUser */ + foreach ($subscribedList as $subscribedUser) { + $subscription = new Subscription(); + $subscription + ->setAuthor($user) + ->setSubscriber($subscribedUser) + ; + + $user->addSubscriber($subscription); + + $logEvent = new SubscriptionEvent(); + $logEvent + ->setSubscriber($subscribedUser) + ->setAuthor($user) + ->setAction(SubscriptionEvent::ACTION_SUBSCRIBE) + ; + + $user->addNewSubscriberEvent($logEvent); + + $this->em->persist($subscription); + $this->em->persist($logEvent); + } + + unset($subscribedList); + + /** @var QueryBuilder $unsubscribedQuery */ + $unsubscribedQuery = $this->em->getRepository('SkobkinPointToolsBundle:Subscription')->createQueryBuilder('s'); + $unsubscribedQuery + ->delete() + ->where('s.author = :author') + ->andWhere('s.subscriber IN (:subscribers)') + ; + + + /** @var User $unsubscribedUser */ + foreach ($unsubscribedList as $unsubscribedUser) { + $logEvent = new SubscriptionEvent(); + $logEvent + ->setSubscriber($unsubscribedUser) + ->setAction($user) + ->setAction(SubscriptionEvent::ACTION_UNSUBSCRIBE) + ; + + $user->addNewSubscriberEvent($logEvent); + + $this->em->persist($logEvent); + } + + $unsubscribedQuery + ->setParameter('author', $user->getId()) + ->setParameter('subscribers', $unsubscribedList) + ->getQuery()->execute(); + ; + + unset($unsubscribedList); + + $this->em->flush(); + } + + /** + * Compares $list1 against $list2 and returns the values in $list1 that are not present in $list2. + * + * @param User[] $list1 + * @param User[] $list2 + * @return User[] Diff + */ + public function getUsersListsDiff(array $list1 = [], array $list2 = []) + { + $hash1 = []; + $hash2 = []; + + foreach ($list1 as $user) { + $hash1[$user->getId()] = $user; + } + foreach ($list2 as $user) { + $hash2[$user->getId()] = $user; + } + + return array_diff_key($hash1, $hash2); + } +} \ No newline at end of file diff --git a/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php b/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php index ea370d5..352d1cc 100644 --- a/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php +++ b/src/Skobkin/Bundle/PointToolsBundle/Service/UserApi.php @@ -2,6 +2,10 @@ namespace Skobkin\Bundle\PointToolsBundle\Service; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\EntityRepository; +use Guzzle\Service\Client; use Skobkin\Bundle\PointToolsBundle\Entity\User; /** @@ -13,10 +17,27 @@ class UserApi extends AbstractApi const PATH_USER_SUBSCRIPTIONS = '/api/user/%s/subscriptions'; const PATH_USER_SUBSCRIBERS = '/api/user/%s/subscribers'; + const AVATAR_SIZE_SMALL = '24'; + const AVATAR_SIZE_MEDIUM = '40'; + const AVATAR_SIZE_LARGE = '80'; + /** * @var string Base URL for user avatars */ - protected $avatarsBaseUrl = '//i.point.im/a/'; + protected $avatarsBaseUrl = 'point.im/avatar/'; + + /** + * @var EntityManager + */ + protected $em; + + + public function __construct(Client $httpClient, $https = true, $baseUrl = null, EntityManagerInterface $entityManager) + { + parent::__construct($httpClient, $https, $baseUrl); + + $this->em = $entityManager; + } public function getName() { @@ -24,33 +45,68 @@ class UserApi extends AbstractApi } /** - * Get user subscribers by his/her name + * Get user subscribers by user login * * @param string $login * @return User[] */ public function getUserSubscribersByLogin($login) { - $response = $this->sendGetRequest(self::PATH_USER_SUBSCRIBERS, [$login]); + $usersList = $this->getGetRequestData('/api/user/' . $login . '/subscribers', [], true); - $body = $response->getBody(true); - - // @todo use JMSSerializer - $data = json_decode($body); - - $users = []; - - if (is_array($data)) { - foreach ($data as $apiUser) { - $user = new User(); - $user->setId($apiUser->id); - $user->setLogin($apiUser->login); - $user->setName($apiUser->name); - - $users[] = $user; - } - } + $users = $this->getUsersFromList($usersList); return $users; } + + /** + * @return User[] + */ + private function getUsersFromList(array $users = []) + { + if (!is_array($users)) { + throw new \InvalidArgumentException('$users must be an array'); + } + + /** @var EntityRepository $userRepo */ + $userRepo = $this->em->getRepository('SkobkinPointToolsBundle:User'); + + $resultUsers = []; + + foreach ($users as $userData) { + if (array_key_exists('id', $userData) && array_key_exists('login', $userData) && array_key_exists('name', $userData) && is_numeric($userData['id'])) { + + // @todo Optimize with prehashed id's list + $user = $userRepo->findOneBy(['id' => $userData['id']]); + + if (!$user) { + $user = new User(); + $user->setId((int) $userData['id']); + $this->em->persist($user); + } + + // Updating data + if ($user->getLogin() !== $userData['login']) { + $user->setLogin($userData['login']); + } + if ($user->getName() !== $userData['name']) { + $user->setName($userData['name']); + } + + $resultUsers[] = $user; + } + } + + $this->em->flush(); + + return $resultUsers; + } + + /** + * @param $login + */ + public function getAvatarUrl(User $user, $size) + { + return ($this->useHttps ? 'https://' : 'http://') . $this->avatarsBaseUrl . $user->getLogin() . '/' . $size; + } }