Completed
Push — master ( ac6145...bdf07d )
by Şəhriyar
14:49
created

Registrar   C

Complexity

Total Complexity 47

Size/Duplication

Total Lines 347
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 17

Test Coverage

Coverage 67.35%

Importance

Changes 17
Bugs 6 Features 8
Metric Value
wmc 47
c 17
b 6
f 8
lcom 1
cbo 17
dl 0
loc 347
ccs 99
cts 147
cp 0.6735
rs 6.0317

14 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
A register() 0 19 4
B registerViaOAuth() 0 30 5
A activate() 0 18 4
A delete() 0 16 3
A get() 0 8 2
A update() 0 19 4
A login() 0 20 3
A loginViaOAuth() 0 14 3
A logout() 0 8 2
A sendResetPasswordLinkViaEmail() 0 18 3
B resetPassword() 0 27 6
B linkOAuthAccount() 0 27 6
A getFailedLoginMessage() 0 4 1

How to fix   Complexity   

Complex Class

Complex classes like Registrar often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Registrar, and based on these observations, apply Extract Interface, too.

1
<?php namespace App\Services;
2
3
use App\Contracts\Registrar as RegistrarContract;
4
use App\Events\Users\LoggedIn;
5
use App\Events\Users\LoggedOut;
6
use App\Events\Users\Registered;
7
use App\Events\Users\RequestedResetPasswordLink;
8
use App\Events\Users\ResetPassword;
9
use App\Events\Users\Updated;
10
use App\Exceptions\Common\ValidationException;
11
use App\Exceptions\Users\LoginNotValidException;
12
use App\Exceptions\Users\PasswordNotValidException;
13
use App\Exceptions\Users\TokenNotValidException;
14
use App\Models\User;
15
use App\Models\UserOAuth;
16
use Cartalyst\Sentinel\Activations\EloquentActivation;
17
use Illuminate\Database\Eloquent\ModelNotFoundException;
18
use Illuminate\Foundation\Validation\ValidatesRequests;
19
use Illuminate\Http\Request;
20
use Laravel\Socialite\AbstractUser as SocialiteUser;
21
22
class Registrar implements RegistrarContract
23
{
24
    use ValidatesRequests;
25
26
    /**
27
     * Request instance.
28
     *
29
     * @var Request
30
     */
31
    protected $request;
32
33
    /**
34
     * @var \Cartalyst\Sentinel\Users\UserRepositoryInterface|\Cartalyst\Sentinel\Users\IlluminateUserRepository
35
     */
36
    protected $userRepository;
37
38
    /**
39
     * @var \Cartalyst\Sentinel\Reminders\ReminderRepositoryInterface|\Cartalyst\Sentinel\Reminders\IlluminateReminderRepository
40
     */
41
    protected $reminderRepository;
42
43 32
    public function __construct()
44
    {
45 32
        $this->request = app('router')->getCurrentRequest();
46 32
        $this->userRepository = app('sentinel')->getUserRepository();
47 32
        $this->reminderRepository = app('sentinel')->getReminderRepository();
48 32
    }
49
50
    /**
51
     * Create a new user instance after a valid registration.
52
     *
53
     * @return \Cartalyst\Sentinel\Users\UserInterface
54
     * @throws \App\Exceptions\Common\ValidationException
55
     */
56 4
    public function register()
57
    {
58 4
        $validator = app('validator')->make($this->request->all(), [
59 4
            'name' => 'sometimes|required|max:255',
60 4
            'email' => 'required|email|max:255|unique:users',
61 4
            'password' => 'required|confirmed|min:' . config('auth.password.min_length'),
62 4
        ]);
63 4
        if ($validator->fails()) {
64 2
            throw new ValidationException($validator);
65
        }
66
67 2
        $user = new User();
68 2
        $this->request->has('name') && $user->name = $this->request->input('name');
69 2
        $user->email = $this->request->input('email');
70 2
        $user->password = $this->userRepository->getHasher()->hash($this->request->input('password'));
71 2
        $user->save() && event(new Registered($user));
72
73 2
        return $user;
74
    }
75
76
    /**
77
     * @param SocialiteUser $oauthUserData
78
     * @param string        $provider
79
     *
80
     * @return \Cartalyst\Sentinel\Users\UserInterface|bool
81
     */
82
    public function registerViaOAuth(SocialiteUser $oauthUserData, $provider)
83
    {
84
        if (!($ownerAccount = User::withTrashed()->whereEmail($oauthUserData->email)->first())) {
85
            $ownerAccount = \Eloquent::unguarded(function () use ($oauthUserData, $provider) {
86
                $user = User::create([
87
                    'name' => $oauthUserData->name,
88
                    'email' => $oauthUserData->email,
89
                    'password' => $this->userRepository->getHasher()->hash(uniqid("", true))
90
                ]);
91
                event(new Registered($user, $provider));
92
93
                return $user;
94
            });
95
        }
96
97
        # If user account is soft-deleted, restore it.
98
        $ownerAccount->trashed() && $ownerAccount->restore();
99
100
        # Update missing user name.
101
        if (!$ownerAccount->name) {
102
            $ownerAccount->name = $oauthUserData->name;
103
            $ownerAccount->save();
104
        }
105
106
        ($doLinkOAuthAccount = $this->linkOAuthAccount($oauthUserData, $provider, $ownerAccount)) && app('sentinel')->login($ownerAccount, true);
0 ignored issues
show
Bug Compatibility introduced by
The expression $this->linkOAuthAccount(...ovider, $ownerAccount); of type App\Models\User|false adds the type App\Models\User to the return on line 110 which is incompatible with the return type declared by the interface App\Contracts\Registrar::registerViaOAuth of type Illuminate\Contracts\Auth\Authenticatable|boolean.
Loading history...
107
108
        event(new LoggedIn($ownerAccount, $provider));
109
110
        return $doLinkOAuthAccount;
111
    }
112
113
    /**
114
     * @param string $token
115
     *
116
     * @return bool
117
     * @throws \App\Exceptions\Common\ValidationException
118
     * @throws \App\Exceptions\Users\TokenNotValidException
119
     */
120 2
    public function activate($token = null)
121
    {
122 2
        $data = !is_null($token) ? ['token' => $token] : $this->request->all();
123 2
        $validator = app('validator')->make($data, [
124 2
            'token' => 'required|string',
125 2
        ]);
126 2
        if ($validator->fails()) {
127
            throw new ValidationException($validator);
128
        }
129
130 2
        $activation = EloquentActivation::whereCode($data['token'])->first();
131 2
        if (!$activation) {
132 1
            throw new TokenNotValidException;
133
        }
134 1
        $user = $this->userRepository->findById($activation->user_id);
135
136 1
        return app('sentinel.activations')->complete($user, $data['token']);
137
    }
138
139
    /**
140
     * @param integer $id
141
     *
142
     * @return boolean
143
     * @throws \App\Exceptions\Common\ValidationException
144
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
145
     */
146 2
    public function delete($id)
147
    {
148 2
        $validator = app('validator')->make($this->request->all(), [
149 2
            'password' => 'required|min:' . config('auth.password.min_length'),
150 2
        ]);
151 2
        if ($validator->fails()) {
152
            throw new ValidationException($validator);
153
        }
154
155 2
        $user = $this->get($id);
156 2
        if (!$this->userRepository->getHasher()->check($this->request->input("password"), $user->password)) {
0 ignored issues
show
Bug introduced by
Accessing password on the interface Cartalyst\Sentinel\Users\UserInterface 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...
157 1
            throw new PasswordNotValidException;
158
        }
159
160 1
        return (bool)User::destroy($id);
161
    }
162
163
    /**
164
     * @param integer $id
165
     *
166
     * @return \App\Models\User|\Cartalyst\Sentinel\Users\UserInterface
167
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
168
     */
169 7
    public function get($id)
170
    {
171 7
        if (!empty($user = $this->userRepository->findById($id))) {
172 6
            return $user;
173
        }
174
175 3
        throw new ModelNotFoundException;
176
    }
177
178
    /**
179
     * @param integer $id
180
     *
181
     * @return boolean
182
     * @throws \App\Exceptions\Common\ValidationException
183
     */
184 2
    public function update($id)
185
    {
186 2
        $user = $this->get($id);
187
188 2
        $validator = app('validator')->make($this->request->all(), [
189 2
            'name' => 'sometimes|required|max:255',
190
            'email' => 'required|email|max:255|unique:users,email,' . $id
191 2
        ]);
192 2
        if ($validator->fails()) {
193 1
            throw new ValidationException($validator);
194
        }
195
196 1
        $userBefore = clone $user;
197
198 1
        $this->request->has('name') && $user->name = $this->request->input("name");
0 ignored issues
show
Bug introduced by
Accessing name on the interface Cartalyst\Sentinel\Users\UserInterface 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...
199 1
        $user->email = $this->request->input("email");
200
201 1
        return $user->save() && event(new Updated($userBefore, $user)); // Fire the event on success only!
202
    }
203
204
    /**
205
     * @return bool|\Cartalyst\Sentinel\Users\UserInterface
206
     *
207
     * @throws \App\Exceptions\Common\ValidationException
208
     * @throws \App\Exceptions\Users\LoginNotValidException
209
     */
210 4
    public function login()
211
    {
212 4
        $validator = app('validator')->make($this->request->all(), [
213 4
            'email' => 'required|email',
214 4
            'password' => 'required',
215 4
        ]);
216 4
        if ($validator->fails()) {
217 2
            throw new ValidationException($validator);
218
        }
219
220 4
        $credentials = $this->request->only('email', 'password');
221
222 4
        if ($user = app('sentinel')->authenticate($credentials, $this->request->has('remember'))) {
223 2
            event(new LoggedIn($user));
224
225 2
            return $user;
226
        }
227
228 2
        throw new LoginNotValidException($this->getFailedLoginMessage());
229
    }
230
231
    /**
232
     * @param SocialiteUser $oauthUserData
233
     * @param string        $provider
234
     *
235
     * @return bool
236
     */
237
    public function loginViaOAuth(SocialiteUser $oauthUserData, $provider)
238
    {
239
        /** @var UserOAuth $owningOAuthAccount */
240
        if ($owningOAuthAccount = UserOAuth::whereRemoteProvider($provider)->whereRemoteId($oauthUserData->id)->first()) {
0 ignored issues
show
Bug introduced by
The method whereRemoteId does only exist in App\Models\UserOAuth, but not in Illuminate\Database\Query\Builder.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
241
            $ownerAccount = $owningOAuthAccount->owner;
242
            app('sentinel')->login($ownerAccount, true);
243
244
            event(new LoggedIn($ownerAccount, $provider));
245
246
            return true;
247
        }
248
249
        return !$this->registerViaOAuth($oauthUserData, $provider) ? false : true;
250
    }
251
252
    /**
253
     * @return boolean
254
     */
255 2
    public function logout()
256
    {
257 2
        if ($user = app('sentinel')->getUser()) {
258 2
            event(new LoggedOut($user));
259 2
        }
260
261 2
        return app('sentinel')->logout();
262
    }
263
264
    /**
265
     * @return boolean
266
     *
267
     * @throws \App\Exceptions\Common\ValidationException
268
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
269
     */
270 4
    public function sendResetPasswordLinkViaEmail()
271
    {
272 4
        $validator = app('validator')->make($this->request->all(), [
273
            'email' => 'required|email|max:255'
274 4
        ]);
275 4
        if ($validator->fails()) {
276 2
            throw new ValidationException($validator);
277
        }
278
279 4
        $user = User::whereEmail($this->request->only('email'))->first();
0 ignored issues
show
Bug introduced by
The method first does only exist in Illuminate\Database\Query\Builder, but not in App\Models\User.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
280 4
        if (is_null($user)) {
281 2
            throw new ModelNotFoundException(trans('passwords.user'));
282
        }
283
284 2
        event(new RequestedResetPasswordLink($user));
285
286 2
        return true;
287
    }
288
289
    /**
290
     * @return boolean
291
     *
292
     * @throws \App\Exceptions\Common\ValidationException
293
     * @throws \App\Exceptions\Users\TokenNotValidException
294
     * @throws \Illuminate\Database\Eloquent\ModelNotFoundException
295
     */
296 8
    public function resetPassword()
297
    {
298 8
        $validator = app('validator')->make($this->request->all(), [
299 8
            'token' => 'required|string',
300 8
            'email' => 'required|email|max:255',
301 8
            'password' => 'required|confirmed|min:' . app('config')->get('auth.password.min_length')
302 8
        ]);
303 8
        if ($validator->fails()) {
304 4
            throw new ValidationException($validator);
305
        }
306
307 5
        $credentials = $this->request->only('email', 'password', 'token');
308
309 5
        if (!($user = User::whereEmail($credentials['email'])->first())) {
0 ignored issues
show
Bug introduced by
The method first does only exist in Illuminate\Database\Query\Builder, but not in App\Models\User.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
310 2
            throw new ModelNotFoundException(trans('passwords.user'));
311
        }
312
313
        /** @var \Cartalyst\Sentinel\Reminders\EloquentReminder $reminder */
314 4
        $reminder = $this->reminderRepository->exists($user);
315 4
        if (!$reminder || $reminder->code !== $credentials['token']) {
316 2
            throw new TokenNotValidException(trans('passwords.token'));
317
        }
318
319 2
        app('sentinel.reminders')->complete($user, $credentials['token'], $credentials['password']) && event(new ResetPassword($user));
320
321 2
        return true;
322
    }
323
324
    /**
325
     * @param SocialiteUser $oauthUserData
326
     * @param string        $provider
327
     * @param User          $ownerAccount
328
     *
329
     * @return \App\Models\User|bool
330
     */
331
    private function linkOAuthAccount(SocialiteUser $oauthUserData, $provider, $ownerAccount)
332
    {
333
        /** @var UserOAuth[] $linkedAccounts */
334
        $linkedAccounts = $ownerAccount->linkedAccounts()->ofProvider($provider)->get();
335
336
        foreach ($linkedAccounts as $linkedAccount) {
337
            if ($linkedAccount->remote_id === $oauthUserData->id || $linkedAccount->email === $oauthUserData->email) {
338
                $linkedAccount->remote_id = $oauthUserData->id;
339
                $linkedAccount->nickname = $oauthUserData->nickname;
340
                $linkedAccount->name = $oauthUserData->name;
341
                $linkedAccount->email = $oauthUserData->email;
342
                $linkedAccount->avatar = $oauthUserData->avatar;
343
344
                return $linkedAccount->save() ? $ownerAccount : false;
345
            }
346
        }
347
348
        $linkedAccount = new UserOAuth();
349
        $linkedAccount->remote_provider = $provider;
350
        $linkedAccount->remote_id = $oauthUserData->id;
351
        $linkedAccount->nickname = $oauthUserData->nickname;
352
        $linkedAccount->name = $oauthUserData->name;
353
        $linkedAccount->email = $oauthUserData->email;
354
        $linkedAccount->avatar = $oauthUserData->avatar;
355
356
        return $ownerAccount->linkedAccounts()->save($linkedAccount) ? $ownerAccount : false;
357
    }
358
359
    /**
360
     * Get the failed login message.
361
     *
362
     * @return string
363
     */
364 2
    private function getFailedLoginMessage()
365
    {
366 2
        return 'These credentials do not match our records!';
367
    }
368
}
369