Completed
Push — master ( 5e7707...6e3d76 )
by Igor
03:56
created

SocialAuth::__construct()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 0
loc 5
rs 9.4285
cc 1
eloc 3
nc 1
nop 1
1
<?php
2
3
namespace app\services;
4
5
use yii\authclient\ClientInterface;
6
use yii\helpers\ArrayHelper;
7
use app\models\User;
8
use app\models\UserProvider;
9
10
/**
11
 * SocialAuth handles successful authentication
12
 */
13
class SocialAuth
14
{
15
    /**
16
     * @var int
17
     */
18
    private $type;
19
    /**
20
     * @var User
21
     */
22
    private $user;
23
    /**
24
     * @var string
25
     */
26
    private $email;
27
    /**
28
     * @var bool
29
     */
30
    private $isVerified = false;
31
    /**
32
     * @var bool
33
     */
34
    private $isExist = false;
35
    /**
36
     * @var ClientInterface
37
     */
38
    private $client;
39
40
    public function execute(ClientInterface $client): SocialAuth
41
    {
42
        $this->client = $client;
43
        $this->type = UserProvider::getTypeByName($client->id);
0 ignored issues
show
Bug introduced by
Accessing id on the interface yii\authclient\ClientInterface suggest that you code against a concrete implementation. How about adding an instanceof check?

If you access a property on an interface, you most likely code against a concrete implementation of the interface.

Available Fixes

  1. Adding an additional type check:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeInterface $object) {
        if ($object instanceof SomeClass) {
            $a = $object->a;
        }
    }
    
  2. Changing the type hint:

    interface SomeInterface { }
    class SomeClass implements SomeInterface {
        public $a;
    }
    
    function someFunction(SomeClass $object) {
        $a = $object->a;
    }
    
Loading history...
44
45
        $user = $this->findUserByProvider();
46
        if ($user) {
47
            $this->isExist = true;
48
        } else {
49
            $profile = $this->client->getUserAttributes();
50
            $this->email = ArrayHelper::getValue($profile, 'email');
51
            $this->isVerified = ArrayHelper::getValue($profile, 'verified', false);
52
53
            if ($this->isVerified && !empty($this->email)) {
54
                $user = User::find()->email($this->email)->one();
55
            }
56
57
            if (!$user) {
58
                $user = new User();
59
                $user->setProfile($this->parseProfile());
60
            }
61
62
            if ($this->isVerified) {
63
                $user->setConfirmed();
64
            }
65
        }
66
67
        $user->setProviders($this->parseProvider());
68
        $this->user = $user;
0 ignored issues
show
Documentation Bug introduced by
It seems like $user of type object<yii\db\ActiveRecord> or array or object<app\services\app\models\User> is incompatible with the declared type object<app\models\User> of property $user.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
69
70
        return $this;
71
    }
72
73
    public function user(): ?User
74
    {
75
        return $this->user;
76
    }
77
78
    public function email(): ?string
79
    {
80
        return $this->email;
81
    }
82
83
    public function isExist(): bool
84
    {
85
        return $this->isExist;
86
    }
87
88
    public function isVerified(): bool
89
    {
90
        return $this->isVerified;
91
    }
92
93
    /**
94
     * Find user by provider
95
     *
96
     * @return app\models\User|null
97
     */
98
    private function findUserByProvider(): ?User
99
    {
100
        $profile = $this->client->getUserAttributes();
101
        $id = ArrayHelper::getValue($profile, 'id');
102
        if ($provider = UserProvider::find()->provider($this->type, $id)->one()) {
103
            $user = $provider->user;
104
            $provider->setAttributes($this->parseProvider());
105
            $provider->save();
106
107
            return $user;
108
        }
109
        return null;
110
    }
111
112
    /**
113
     * Parse provider
114
     *
115
     * @return array
116
     */
117
    private function parseProvider(): array
118
    {
119
        $profile = $this->client->getUserAttributes();
120
        $token = $this->client->getAccessToken()->getParams();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface yii\authclient\ClientInterface as the method getAccessToken() does only exist in the following implementations of said interface: yii\authclient\BaseOAuth, yii\authclient\OAuth1, yii\authclient\OAuth2, yii\authclient\OpenIdConnect, yii\authclient\clients\Facebook, yii\authclient\clients\GitHub, yii\authclient\clients\Google, yii\authclient\clients\GoogleHybrid, yii\authclient\clients\LinkedIn, yii\authclient\clients\Live, yii\authclient\clients\Twitter, yii\authclient\clients\VKontakte, yii\authclient\clients\Yandex.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
121
122
        $data = [];
123
        switch ($this->type) {
124
            case UserProvider::TYPE_FACEBOOK:
125
                $data = $this->parseProviderFacebook($profile, $token);
126
                break;
127
128
            case UserProvider::TYPE_VKONTAKTE:
129
                $data = $this->parseProviderVkontakte($profile, $token);
130
                break;
131
132
            case UserProvider::TYPE_TWITTER:
133
                $data = $this->parseProviderTwitter($profile, $token);
134
                break;
135
        }
136
        $data['type'] = $this->type;
137
        return $data;
138
    }
139
140
    /**
141
     * Parse profile
142
     *
143
     * @return array
144
     */
145
    private function parseProfile(): array
146
    {
147
        $profile = $this->client->getUserAttributes();
148
149
        $data = [];
150
        switch ($this->type) {
151
            case UserProvider::TYPE_FACEBOOK:
152
                $data = $this->parseProfileFacebook($profile);
153
                break;
154
155
            case UserProvider::TYPE_VKONTAKTE:
156
                $data = $this->parseProfileVkontakte($profile);
157
                break;
158
159
            case UserProvider::TYPE_TWITTER:
160
                $data = $this->parseProfileTwitter($profile);
161
                break;
162
        }
163
        return $data;
164
    }
165
166
    /**
167
     * Prepare provider attributes for facebook
168
     *
169
     * @param array $profile
170
     * @param array $token
171
     * @return array
172
     */
173
    private function parseProviderFacebook(array $profile, array $token): array
174
    {
175
        return [
176
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
177
            'profile_url' => ArrayHelper::getValue($profile, 'link'),
178
            'access_token' => ArrayHelper::getValue($token, 'access_token'),
179
            'access_token_secret' => ''
180
        ];
181
    }
182
183
    /**
184
     * Prepare provider attributes for vkontakte
185
     *
186
     * @param array $profile
187
     * @param array $token
188
     * @return array
189
     */
190
    private function parseProviderVkontakte(array $profile, array $token): array
191
    {
192
        return [
193
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
194
            'profile_url' => 'https://vk.com/id' . ArrayHelper::getValue($profile, 'id'),
195
            'access_token' => ArrayHelper::getValue($token, 'access_token'),
196
            'access_token_secret' => ''
197
        ];
198
    }
199
200
    /**
201
     * Prepare provider attributes for twitter
202
     *
203
     * @param array $profile
204
     * @param array $token
205
     * @return array
206
     */
207
    private function parseProviderTwitter(array $profile, array $token): array
208
    {
209
        return [
210
            'profile_id' => ArrayHelper::getValue($profile, 'id'),
211
            'profile_url' => 'https://twitter.com/' . ArrayHelper::getValue($profile, 'screen_name'),
212
            'access_token' => ArrayHelper::getValue($token, 'oauth_token'),
213
            'access_token_secret' => ArrayHelper::getValue($token, 'oauth_token_secret')
214
        ];
215
    }
216
217
    /**
218
     * Prepare profile attributes for facebook
219
     *
220
     * @param array $profile
221
     * @return array
222
     */
223
    private function parseProfileFacebook(array $profile): array
224
    {
225
        return [
226
            'full_name' => trim(ArrayHelper::getValue($profile, 'name')),
227
            'birth_day' => '',
228
            'photo' => ArrayHelper::getValue($profile, 'picture.data.url', '')
229
        ];
230
    }
231
232
    /**
233
     * Prepare profile attributes for vkontakte
234
     *
235
     * @param array $profile
236
     * @return array
237
     */
238
    private function parseProfileVkontakte(array $profile): array
239
    {
240
        $firstName = ArrayHelper::getValue($profile, 'first_name');
241
        $lastName = ArrayHelper::getValue($profile, 'last_name');
242
        $birthDay = date_create_from_format('d.m.Y', ArrayHelper::getValue($profile, 'bdate'));
243
        return [
244
            'full_name' => trim($firstName . ' ' . $lastName),
245
            'birth_day' => date_format($birthDay, 'Y-m-d'),
246
            'photo' => str_replace('_50', '_400', ArrayHelper::getValue($profile, 'photo'))
247
        ];
248
    }
249
250
    /**
251
     * Prepare profile attributes for twitter
252
     *
253
     * @param array $profile
254
     * @return array
255
     */
256
    private function parseProfileTwitter(array $profile): array
257
    {
258
        $photo = ArrayHelper::getValue($profile, 'profile_image_url');
259
        return [
260
            'full_name' => $profile['name'],
261
            'photo' => str_replace('_normal', '_400x400', $photo)
262
        ];
263
    }
264
}
265