Test implementation of subscriptions checking command.

This commit is contained in:
Alexey Skobkin 2015-05-29 01:47:06 +03:00
parent 032730d2b2
commit 5652a5ca68
7 changed files with 422 additions and 30 deletions

View file

@ -12,6 +12,11 @@ parameters:
mailer_user: ~ mailer_user: ~
mailer_password: ~ 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 locale: en
# A secret key that's used to generate certain security-related tokens # A secret key that's used to generate certain security-related tokens

View file

@ -0,0 +1,58 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Command;
use Skobkin\Bundle\PointToolsBundle\Service\SubscriptionsManager;
use Skobkin\Bundle\PointToolsBundle\Service\UserApi;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
class UpdateSubscriptionsCommand extends ContainerAwareCommand
{
protected function configure()
{
$this
->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
}
}
}

View file

@ -13,6 +13,7 @@ use Doctrine\ORM\Mapping as ORM;
* @ORM\Index(name="date_idx", columns={"date"}) * @ORM\Index(name="date_idx", columns={"date"})
* }) * })
* @ORM\Entity * @ORM\Entity
* @ORM\HasLifecycleCallbacks
*/ */
class SubscriptionEvent class SubscriptionEvent
{ {
@ -58,6 +59,17 @@ class SubscriptionEvent
*/ */
private $action; private $action;
/**
* @ORM\PrePersist
*/
public function onCreate()
{
if (!$this->date) {
$this->date = new \DateTime();
}
}
/** /**
* Get id * Get id
* *

View file

@ -1,10 +1,18 @@
services: services:
skobkin_point_tools.http_client: skobkin_point_tools.http_client:
class: %guzzle.client.class% class: %guzzle.client.class%
arguments: [ "http://point.im/" ] arguments: [ "%point_base_url%" ]
tags: tags:
- { name: guzzle.client } - { name: guzzle.client }
skobkin_point_tools.api_user: skobkin_point_tools.api_user:
class: Skobkin\Bundle\PointToolsBundle\Service\UserApi 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 ]

View file

@ -6,7 +6,12 @@ use Guzzle\Service\Client;
use Guzzle\Http\Message\Request as GuzzleRequest; use Guzzle\Http\Message\Request as GuzzleRequest;
use Guzzle\Http\Message\Response as GuzzleResponse; 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 class AbstractApi
{ {
/** /**
@ -14,24 +19,146 @@ class AbstractApi
*/ */
protected $client; 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->client = $httpClient;
$this->useHttps = ($https) ? true : false;
if ($baseUrl) {
$this->setBaseUrl($baseUrl);
}
} }
/** /**
* Make GET request and return Response object * Make GET request and return Response object
* **
* @param string $pathTemplate * @param string $path Request path
* @param array $parameters * @param array $parameters Key => Value array of query parameters
* @return GuzzleResponse * @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); $request = $this->client->get($path);
$query = $request->getQuery();
foreach ($parameters as $parameter => $value) {
$query->set($parameter, $value);
}
return $request->send(); 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;
}
} }

View file

@ -0,0 +1,126 @@
<?php
namespace Skobkin\Bundle\PointToolsBundle\Service;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\QueryBuilder;
use Skobkin\Bundle\PointToolsBundle\Entity\Subscription;
use Skobkin\Bundle\PointToolsBundle\Entity\SubscriptionEvent;
use Skobkin\Bundle\PointToolsBundle\Entity\User;
class SubscriptionsManager
{
/**
* @var EntityManager
*/
protected $em;
public function __construct(EntityManagerInterface $entityManager)
{
$this->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);
}
}

View file

@ -2,6 +2,10 @@
namespace Skobkin\Bundle\PointToolsBundle\Service; 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; 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_SUBSCRIPTIONS = '/api/user/%s/subscriptions';
const PATH_USER_SUBSCRIBERS = '/api/user/%s/subscribers'; 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 * @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() 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 * @param string $login
* @return User[] * @return User[]
*/ */
public function getUserSubscribersByLogin($login) public function getUserSubscribersByLogin($login)
{ {
$response = $this->sendGetRequest(self::PATH_USER_SUBSCRIBERS, [$login]); $usersList = $this->getGetRequestData('/api/user/' . $login . '/subscribers', [], true);
$body = $response->getBody(true); $users = $this->getUsersFromList($usersList);
// @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;
}
}
return $users; 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;
}
} }