Passed
Push — master ( 1145e4...3a5c01 )
by Alexey
03:43
created

UserApi::isAuthDataValid()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 12
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 12

Importance

Changes 0
Metric Value
dl 0
loc 12
ccs 0
cts 6
cp 0
rs 9.4285
c 0
b 0
f 0
cc 3
eloc 6
nc 2
nop 2
crap 12
1
<?php
2
3
namespace Skobkin\Bundle\PointToolsBundle\Service;
4
5
use Doctrine\ORM\EntityManager;
6
use Doctrine\ORM\EntityRepository;
7
use Guzzle\Http\Exception\ClientErrorResponseException;
8
use Guzzle\Service\Client;
9
use JMS\Serializer\Serializer;
10
use Skobkin\Bundle\PointToolsBundle\DTO\Api\Auth;
11
use Skobkin\Bundle\PointToolsBundle\Entity\User;
12
use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\ApiException;
13
use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\InvalidResponseException;
14
use Skobkin\Bundle\PointToolsBundle\Service\Exceptions\UserNotFoundException;
15
use Symfony\Component\HttpFoundation\Response;
16
17
/**
18
 * Basic Point.im user API functions from /api/user/*
19
 */
20
class UserApi extends AbstractApi
21
{
22
    const AVATAR_SIZE_SMALL = '24';
23
    const AVATAR_SIZE_MEDIUM = '40';
24
    const AVATAR_SIZE_LARGE = '80';
25
26
    /**
27
     * @var string Base URL for user avatars
28
     */
29
    protected $avatarsBaseUrl = '//point.im/avatar/';
30
31
    /**
32
     * @var EntityManager
33
     */
34
    protected $em;
35
36
    /**
37
     * @var EntityRepository
38
     */
39
    protected $userRepository;
40
41
    /**
42
     * @var Serializer
43
     */
44
    private $serializer;
45
46
47 12
    public function __construct(Client $httpClient, $https = true, $baseUrl = null, EntityManager $entityManager, Serializer $serializer)
0 ignored issues
show
Bug introduced by
You have injected the EntityManager via parameter $entityManager. This is generally not recommended as it might get closed and become unusable. Instead, it is recommended to inject the ManagerRegistry and retrieve the EntityManager via getManager() each time you need it.

The EntityManager might become unusable for example if a transaction is rolled back and it gets closed. Let’s assume that somewhere in your application, or in a third-party library, there is code such as the following:

function someFunction(ManagerRegistry $registry) {
    $em = $registry->getManager();
    $em->getConnection()->beginTransaction();
    try {
        // Do something.
        $em->getConnection()->commit();
    } catch (\Exception $ex) {
        $em->getConnection()->rollback();
        $em->close();

        throw $ex;
    }
}

If that code throws an exception and the EntityManager is closed. Any other code which depends on the same instance of the EntityManager during this request will fail.

On the other hand, if you instead inject the ManagerRegistry, the getManager() method guarantees that you will always get a usable manager instance.

Loading history...
48
    {
49 12
        parent::__construct($httpClient, $https, $baseUrl);
50
51 12
        $this->em = $entityManager;
52 12
        $this->serializer = $serializer;
53 12
        $this->userRepository = $this->em->getRepository('SkobkinPointToolsBundle:User');
54 12
    }
55
56
    public function getName()
57
    {
58
        return 'skobkin_point_tools_api_user';
59
    }
60
61
    public function isAuthDataValid(string $login, string $password): bool
62
    {
63
        $auth = $this->authenticate($login, $password);
64
65
        if (!$auth->getError() && $auth->getToken()) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $auth->getError() of type string|null is loosely compared to false; this is ambiguous if the string can be empty. You might want to explicitly use === null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
Bug Best Practice introduced by
The expression $auth->getToken() of type string|null is loosely compared to true; this is ambiguous if the string can be empty. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For string values, the empty string '' is a special case, in particular the following results might be unexpected:

''   == false // true
''   == null  // true
'ab' == false // false
'ab' == null  // false

// It is often better to use strict comparison
'' === false // false
'' === null  // false
Loading history...
66
            $this->logout($auth);
67
68
            return true;
69
        }
70
71
        return false;
72
    }
73
74
    public function authenticate(string $login, string $password): Auth
75
    {
76
        try {
77
            $authData = $this->getPostRequestData(
78
                '/api/login',
79
                [
80
                    'login' => $login,
81
                    'password' => $password,
82
                ]
83
            );
84
85
            return $this->serializer->deserialize($authData, Auth::class, 'json');
86
        } catch (ClientErrorResponseException $e) {
87
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
88
                throw new InvalidResponseException('API method not found', 0, $e);
89
            } else {
90
                throw $e;
91
            }
92
        }
93
    }
94
95
    public function logout(Auth $auth): bool
96
    {
97
        try {
98
            $this->getPostRequestData('/api/logout', ['csrf_token' => $auth->getCsRfToken()]);
99
100
            return true;
101
        } catch (ClientErrorResponseException $e) {
102
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
103
                throw new InvalidResponseException('API method not found', 0, $e);
104
            } elseif (Response::HTTP_FORBIDDEN === $e->getResponse()->getStatusCode()) {
105
                return true;
106
            } else {
107
                throw $e;
108
            }
109
        }
110
    }
111
112
    /**
113
     * Get user subscribers by user login
114
     *
115
     * @param string $login
116
     * @return User[]
117
     * @throws ApiException
118
     * @throws InvalidResponseException
119
     * @throws UserNotFoundException
120
     */
121 View Code Duplication
    public function getUserSubscribersByLogin($login)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
    {
123
        try {
124
            $usersList = $this->getGetRequestData('/api/user/'.urlencode($login).'/subscribers', [], true);
125
        } catch (ClientErrorResponseException $e) {
126
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
127
                throw new UserNotFoundException('User not found', 0, $e, null, $login);
128
            } else {
129
                throw $e;
130
            }
131
        }
132
133
        return $this->getUsersFromList($usersList);
134
    }
135
136
    /**
137
     * Get user subscribers by user id
138
     *
139
     * @param $id
140
     * @return User[]
141
     * @throws ApiException
142
     * @throws InvalidResponseException
143
     * @throws UserNotFoundException
144
     */
145 View Code Duplication
    public function getUserSubscribersById($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
146
    {
147
        if (!is_numeric($id)) {
148
            throw new \InvalidArgumentException('$id must be an integer');
149
        }
150
151
        try {
152
            $usersList = $this->getGetRequestData('/api/user/id/'.(int) $id.'/subscribers', [], true);
153
        } catch (ClientErrorResponseException $e) {
154
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
155
                throw new UserNotFoundException('User not found', 0, $e, $id);
156
            } else {
157
                throw $e;
158
            }
159
        }
160
161
        return $this->getUsersFromList($usersList);
162
    }
163
164
    /**
165
     * Get user subscriptions by user login
166
     *
167
     * @param string $login
168
     * @return User[]
169
     * @throws ApiException
170
     * @throws InvalidResponseException
171
     * @throws UserNotFoundException
172
     */
173 View Code Duplication
    public function getUserSubscriptionsByLogin($login)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
174
    {
175
        try {
176
            $usersList = $this->getGetRequestData('/api/user/'.urlencode($login).'/subscriptions', [], true);
177
        } catch (ClientErrorResponseException $e) {
178
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
179
                throw new UserNotFoundException('User not found', 0, $e, null, $login);
180
            } else {
181
                throw $e;
182
            }
183
        }
184
185
        return $this->getUsersFromList($usersList);
186
    }
187
188
    /**
189
     * Get user subscriptions by user id
190
     *
191
     * @param $id
192
     * @return User[]
193
     * @throws ApiException
194
     * @throws InvalidResponseException
195
     * @throws UserNotFoundException
196
     */
197 View Code Duplication
    public function getUserSubscriptionsById($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
198
    {
199
        if (!is_numeric($id)) {
200
            throw new \InvalidArgumentException('$id must be an integer');
201
        }
202
203
        try {
204
            $usersList = $this->getGetRequestData('/api/user/id/'.(int) $id.'/subscriptions', [], true);
205
        } catch (ClientErrorResponseException $e) {
206
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
207
                throw new UserNotFoundException('User not found', 0, $e, $id);
208
            } else {
209
                throw $e;
210
            }
211
        }
212
213
        return $this->getUsersFromList($usersList);
214
    }
215
216
    /**
217
     * Get single user by login
218
     *
219
     * @param string $login
220
     * @return User
221
     * @throws UserNotFoundException
222
     * @throws ClientErrorResponseException
223
     */
224 View Code Duplication
    public function getUserByLogin($login)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
225
    {
226
        try {
227
            $userInfo = $this->getGetRequestData('/api/user/login/'.urlencode($login), [], true);
228
        } catch (ClientErrorResponseException $e) {
229
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
230
                throw new UserNotFoundException('User not found', 0, $e, null, $login);
231
            } else {
232
                throw $e;
233
            }
234
        }
235
236
        return $this->getUserFromUserInfo($userInfo);
237
    }
238
239
    /**
240
     * Get single user by id
241
     *
242
     * @param $id
243
     * @return User
244
     * @throws UserNotFoundException
245
     * @throws ClientErrorResponseException
246
     */
247 View Code Duplication
    public function getUserById($id)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
248
    {
249
        if (!is_numeric($id)) {
250
            throw new \InvalidArgumentException('$id must be an integer');
251
        }
252
253
        try {
254
            $userInfo = $this->getGetRequestData('/api/user/id/'.(int) $id, [], true);
255
        } catch (ClientErrorResponseException $e) {
256
            if (Response::HTTP_NOT_FOUND === $e->getResponse()->getStatusCode()) {
257
                throw new UserNotFoundException('User not found', 0, $e, $id);
258
            } else {
259
                throw $e;
260
            }
261
        }
262
263
        return $this->getUserFromUserInfo($userInfo);
264
    }
265
266
    /**
267
     * Finds and updates or create new user from API response data
268
     *
269
     * @param array $userInfo
270
     * @return User
271
     * @throws ApiException
272
     * @throws InvalidResponseException
273
     */
274
    public function getUserFromUserInfo(array $userInfo)
275
    {
276
        if (!is_array($userInfo)) {
277
            throw new \InvalidArgumentException('$userInfo must be an array');
278
        }
279
280
        // @todo Refactor to UserFactory->createFromArray()
281
        if (array_key_exists('id', $userInfo) && array_key_exists('login', $userInfo) && array_key_exists('name', $userInfo) && is_numeric($userInfo['id'])) {
282
            /** @var User $user */
283 View Code Duplication
            if (null === ($user = $this->userRepository->find($userInfo['id']))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
284
                // Creating new user
285
                $user = new User($userInfo['id']);
286
                $this->em->persist($user);
287
            }
288
289
            // Updating data
290
            $user
291
                ->setLogin($userInfo['login'])
292
                ->setName($userInfo['name'])
293
            ;
294
295
            try {
296
                $this->em->flush($user);
297
            } catch (\Exception $e) {
298
                throw new ApiException(sprintf('Error while flushing changes for [%d] %s: %s', $user->getId(), $user->getLogin(), $e->getMessage()), 0, $e);
299
            }
300
301
            return $user;
302
        }
303
304
        throw new InvalidResponseException('Invalid API response. Mandatory fields do not exist.');
305
    }
306
307
    /**
308
     * Get array of User objects from API response containing user list
309
     *
310
     * @param array $users
311
     * @return User[]
312
     * @throws ApiException
313
     * @throws InvalidResponseException
314
     */
315
    private function getUsersFromList(array $users = [])
316
    {
317
        if (!is_array($users)) {
318
            throw new \InvalidArgumentException('$users must be an array');
319
        }
320
321
        /** @var User[] $resultUsers */
322
        $resultUsers = [];
323
324
        foreach ($users as $userInfo) {
325
            if (array_key_exists('id', $userInfo) && array_key_exists('login', $userInfo) && array_key_exists('name', $userInfo) && is_numeric($userInfo['id'])) {
326
327
                // @todo Optimize with prehashed id's list
328 View Code Duplication
                if (null === ($user = $this->userRepository->find($userInfo['id']))) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
329
                    $user = new User((int) $userInfo['id']);
330
                    $this->em->persist($user);
331
                }
332
333
                // Updating data
334
                $user
335
                    ->setLogin($userInfo['login'])
336
                    ->setName($userInfo['name'])
337
                ;
338
339
                try {
340
                    $this->em->flush($user);
341
                } catch (\Exception $e) {
342
                    throw new ApiException(sprintf('Error while flushing changes for [%d] %s: %s', $user->getId(), $user->getLogin(), $e->getMessage()), 0, $e);
343
                }
344
345
                $resultUsers[] = $user;
346
            } else {
347
                throw new InvalidResponseException('Invalid API response. Mandatory fields do not exist.');
348
            }
349
        }
350
351
        return $resultUsers;
352
    }
353
354
    /**
355
     * Creates URL of avatar with specified size by User object
356
     *
357
     * @param User $user
358
     * @param int $size
359
     * @return string
360
     */
361 3
    public function getAvatarUrl(User $user, $size)
362
    {
363 3
        return $this->getAvatarUrlByLogin($user->getLogin(), $size);
364
    }
365
366
    /**
367
     * Creates URL of avatar with specified size by login string
368
     *
369
     * @param $login
370
     * @param $size
371
     *
372
     * @return string
373
     */
374 4
    public function getAvatarUrlByLogin($login, $size)
375
    {
376 4
        if (!in_array($size, [self::AVATAR_SIZE_SMALL, self::AVATAR_SIZE_MEDIUM, self::AVATAR_SIZE_LARGE], true)) {
377
            throw new \InvalidArgumentException('Avatar size must be one of restricted variants. See UserApi class AVATAR_SIZE_* constants.');
378
        }
379
380 4
        return $this->avatarsBaseUrl.urlencode($login).'/'.$size;
381
    }
382
}
383