Completed
Pull Request — master (#133)
by Damian
20:49 queued 18:16
created

RoleManager   C

Complexity

Total Complexity 55

Size/Duplication

Total Lines 370
Duplicated Lines 23.78 %

Coupling/Cohesion

Components 1
Dependencies 10

Test Coverage

Coverage 85.62%

Importance

Changes 0
Metric Value
wmc 55
lcom 1
cbo 10
dl 88
loc 370
ccs 131
cts 153
cp 0.8562
rs 6.8
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
B create() 0 27 5
C update() 0 51 11
A delete() 0 14 2
A matchRoles() 18 18 4
C setReferences() 32 32 8
C generateMigration() 0 67 8
A createLimitation() 0 12 3
D assignRole() 38 45 10
A addPolicy() 0 13 3

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like RoleManager 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 RoleManager, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Kaliop\eZMigrationBundle\Core\Executor;
4
5
use eZ\Publish\API\Repository\Values\User\Role;
6
use eZ\Publish\API\Repository\RoleService;
7
use eZ\Publish\API\Repository\UserService;
8
use Kaliop\eZMigrationBundle\API\Collection\RoleCollection;
9
use Kaliop\eZMigrationBundle\API\MigrationGeneratorInterface;
10
use Kaliop\eZMigrationBundle\Core\Helper\LimitationConverter;
11
use Kaliop\eZMigrationBundle\Core\Matcher\RoleMatcher;
12
13
/**
14
 * Handles role migrations.
15
 */
16
class RoleManager extends RepositoryExecutor implements MigrationGeneratorInterface
17
{
18
    protected $supportedStepTypes = array('role');
19
20
    protected $limitationConverter;
21
    protected $roleMatcher;
22
23 70
    public function __construct(RoleMatcher $roleMatcher, LimitationConverter $limitationConverter)
24
    {
25 70
        $this->roleMatcher = $roleMatcher;
26 70
        $this->limitationConverter = $limitationConverter;
27 70
    }
28
29
    /**
30
     * Method to handle the create operation of the migration instructions
31
     */
32 1
    protected function create($step)
33
    {
34 1
        $roleService = $this->repository->getRoleService();
35 1
        $userService = $this->repository->getUserService();
36
37 1
        $roleCreateStruct = $roleService->newRoleCreateStruct($step->dsl['name']);
38
39
        // Publish new role
40 1
        $role = $roleService->createRole($roleCreateStruct);
41 1
        if (is_callable(array($roleService, 'publishRoleDraft'))) {
42 1
            $roleService->publishRoleDraft($role);
0 ignored issues
show
Bug introduced by
The method publishRoleDraft() does not seem to exist on object<eZ\Publish\API\Repository\RoleService>.

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
43
        }
44
45 1
        if (isset($step->dsl['policies'])) {
46 1
            foreach ($step->dsl['policies'] as $key => $ymlPolicy) {
47 1
                $this->addPolicy($role, $roleService, $ymlPolicy);
48
            }
49
        }
50
51 1
        if (isset($step->dsl['assign'])) {
52
            $this->assignRole($role, $roleService, $userService, $step->dsl['assign']);
53
        }
54
55 1
        $this->setReferences($role, $step);
56
57 1
        return $role;
58
    }
59
60
    /**
61
     * Method to handle the update operation of the migration instructions
62
     */
63 1
    protected function update($step)
64
    {
65 1
        $roleCollection = $this->matchRoles('update', $step);
66
67 1
        if (count($roleCollection) > 1 && isset($step->dsl['references'])) {
68
            throw new \Exception("Can not execute Role update because multiple roles match, and a references section is specified in the dsl. References can be set when only 1 role matches");
69
        }
70
71 1
        if (count($roleCollection) > 1 && isset($step->dsl['new_name'])) {
72
            throw new \Exception("Can not execute Role update because multiple roles match, and a new_name is specified in the dsl.");
73
        }
74
75 1
        $roleService = $this->repository->getRoleService();
76 1
        $userService = $this->repository->getUserService();
77
78
        /** @var \eZ\Publish\API\Repository\Values\User\Role $role */
79 1
        foreach ($roleCollection as $key => $role) {
0 ignored issues
show
Bug introduced by
The expression $roleCollection of type object<Kaliop\eZMigratio...on\RoleCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
80
81
            // Updating role name
82 1
            if (isset($step->dsl['new_name'])) {
83
                $update = $roleService->newRoleUpdateStruct();
84
                $update->identifier = $step->dsl['new_name'];
85
                $role = $roleService->updateRole($role, $update);
86
            }
87
88 1
            if (isset($step->dsl['policies'])) {
89 1
                $ymlPolicies = $step->dsl['policies'];
90
91
                // Removing all policies so we can add them back.
92
                // TODO: Check and update policies instead of remove and add.
93 1
                $policies = $role->getPolicies();
94 1
                foreach ($policies as $policy) {
95
                    $roleService->deletePolicy($policy);
96
                }
97
98 1
                foreach ($ymlPolicies as $ymlPolicy) {
99 1
                    $this->addPolicy($role, $roleService, $ymlPolicy);
100
                }
101
            }
102
103 1
            if (isset($step->dsl['assign'])) {
104 1
                $this->assignRole($role, $roleService, $userService, $step->dsl['assign']);
105
            }
106
107 1
            $roleCollection[$key] = $role;
108
        }
109
110 1
        $this->setReferences($roleCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $roleCollection defined by $this->matchRoles('update', $step) on line 65 can be null; however, Kaliop\eZMigrationBundle...anager::setReferences() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
111
112 1
        return $roleCollection;
113
    }
114
115
    /**
116
     * Method to handle the delete operation of the migration instructions
117
     */
118 1
    protected function delete($step)
119
    {
120 1
        $roleCollection = $this->matchRoles('delete', $step);
121
122 1
        $this->setReferences($roleCollection, $step);
0 ignored issues
show
Bug introduced by
It seems like $roleCollection defined by $this->matchRoles('delete', $step) on line 120 can be null; however, Kaliop\eZMigrationBundle...anager::setReferences() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
123
124 1
        $roleService = $this->repository->getRoleService();
125
126 1
        foreach ($roleCollection as $role) {
0 ignored issues
show
Bug introduced by
The expression $roleCollection of type object<Kaliop\eZMigratio...on\RoleCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
127 1
            $roleService->deleteRole($role);
128
        }
129
130 1
        return $roleCollection;
131
    }
132
133
    /**
134
     * @param string $action
135
     * @return RoleCollection
136
     * @throws \Exception
137
     */
138 1 View Code Duplication
    protected function matchRoles($action, $step)
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...
139
    {
140 1
        if (!isset($step->dsl['name']) && !isset($step->dsl['match'])) {
141
            throw new \Exception("The name of a role or a match condition is required to $action it");
142
        }
143
144
        // Backwards compat
145 1
        if (isset($step->dsl['match'])) {
146 1
            $match = $step->dsl['match'];
147
        } else {
148 1
            $match = array('identifier' => $step->dsl['name']);
149
        }
150
151
        // convert the references passed in the match
152 1
        $match = $this->resolveReferencesRecursively($match);
0 ignored issues
show
Deprecated Code introduced by
The method Kaliop\eZMigrationBundle...ReferencesRecursively() has been deprecated with message: will be moved into the reference resolver classes

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
153
154 1
        return $this->roleMatcher->match($match);
155
    }
156
157
    /**
158
     * Set references to object attributes to be retrieved later.
159
     *
160
     * The Role Manager currently support setting references to role_ids.
161
     *
162
     * @param \eZ\Publish\API\Repository\Values\User\Role|RoleCollection $role
163
     * @throws \InvalidArgumentException When trying to assign a reference to an unsupported attribute
164
     * @return boolean
165
     */
166 1 View Code Duplication
    protected function setReferences($role, $step)
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...
167
    {
168 1
        if (!array_key_exists('references', $step->dsl)) {
169 1
            return false;
170
        }
171
172 1
        $references = $this->setReferencesCommon($role, $step->dsl['references']);
173 1
        $role = $this->insureSingleEntity($role, $references);
174
175 1
        foreach ($references as $reference) {
176 1
            switch ($reference['attribute']) {
177 1
                case 'role_id':
178 1
                case 'id':
179 1
                    $value = $role->id;
180 1
                    break;
181 1
                case 'identifier':
182
                case 'role_identifier':
183 1
                    $value = $role->identifier;
184 1
                    break;
185
                default:
186
                    throw new \InvalidArgumentException('Role Manager does not support setting references for attribute ' . $reference['attribute']);
187
            }
188
189 1
            $overwrite = false;
190 1
            if (isset($reference['overwrite'])) {
191
                $overwrite = $reference['overwrite'];
192
            }
193 1
            $this->referenceResolver->addReference($reference['identifier'], $value, $overwrite);
194
        }
195
196 1
        return true;
197
    }
198
199
    /**
200
     * @param array $matchCondition
201
     * @param string $mode
202
     * @param array $context
203
     * @throws \Exception
204
     * @return array
205
     */
206 5
    public function generateMigration(array $matchCondition, $mode, array $context = array())
207
    {
208 5
        $previousUserId = $this->loginUser($this->getAdminUserIdentifierFromContext($context));
209 5
        $roleCollection = $this->roleMatcher->match($matchCondition);
210 5
        $data = array();
211
212
        /** @var \eZ\Publish\API\Repository\Values\User\Role $role */
213 5
        foreach ($roleCollection as $role) {
0 ignored issues
show
Bug introduced by
The expression $roleCollection of type object<Kaliop\eZMigratio...on\RoleCollection>|null is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
214
            $roleData = array(
215 5
                'type' => reset($this->supportedStepTypes),
216 5
                'mode' => $mode
217
            );
218
219
            switch ($mode) {
220 5
                case 'create':
221 2
                    $roleData = array_merge(
222 2
                        $roleData,
223
                        array(
224 2
                            'name' => $role->identifier
225
                        )
226
                    );
227 2
                    break;
228 3
                case 'update':
229 2
                case 'delete':
230 3
                    $roleData = array_merge(
231 3
                        $roleData,
232
                        array(
233
                            'match' => array(
234 3
                                RoleMatcher::MATCH_ROLE_IDENTIFIER => $role->identifier
235
                            )
236
                        )
237
                    );
238 3
                    break;
239
                default:
240
                    throw new \Exception("Executor 'role' doesn't support mode '$mode'");
241
            }
242
243 5
            if ($mode != 'delete') {
244 3
                $policies = array();
245 3
                foreach ($role->getPolicies() as $policy) {
246 3
                    $limitations = array();
247
248 3
                    foreach ($policy->getLimitations() as $limitation) {
249 3
                        $limitations[] = $this->limitationConverter->getLimitationArrayWithIdentifiers($limitation);
250
                    }
251
252 3
                    $policies[] = array(
253 3
                        'module' => $policy->module,
254 3
                        'function' => $policy->function,
255 3
                        'limitations' => $limitations
256
                    );
257
                }
258
259 3
                $roleData = array_merge(
260 3
                    $roleData,
261
                    array(
262 3
                        'policies' => $policies
263
                    )
264
                );
265
            }
266
267 5
            $data[] = $roleData;
268
        }
269
270 5
        $this->loginUser($previousUserId);
271 5
        return $data;
272
    }
273
274
    /**
275
     * Create a new Limitation object based on the type and value in the $limitation array.
276
     *
277
     * <pre>
278
     * $limitation = array(
279
     *  'identifier' => Type of the limitation
280
     *  'values' => array(Values to base the limitation on)
281
     * )
282
     * </pre>
283
     *
284
     * @param \eZ\Publish\API\Repository\RoleService $roleService
285
     * @param array $limitation
286
     * @return \eZ\Publish\API\Repository\Values\User\Limitation
287
     */
288 1
    protected function createLimitation(RoleService $roleService, array $limitation)
289
    {
290 1
        $limitationType = $roleService->getLimitationType($limitation['identifier']);
291
292 1
        $limitationValue = is_array($limitation['values']) ? $limitation['values'] : array($limitation['values']);
293
294 1
        foreach ($limitationValue as $id => $value) {
295 1
            $limitationValue[$id] = $this->referenceResolver->resolveReference($value);
296
        }
297 1
        $limitationValue = $this->limitationConverter->resolveLimitationValue($limitation['identifier'], $limitationValue);
298 1
        return $limitationType->buildValue($limitationValue);
299
    }
300
301
    /**
302
     * Assign a role to users and groups in the assignment array.
303
     *
304
     * <pre>
305
     * $assignments = array(
306
     *      array(
307
     *          'type' => 'user',
308
     *          'ids' => array(user ids),
309
     *          'limitation' => array(limitations)
310
     *      )
311
     * )
312
     * </pre>
313
     *
314
     * @param \eZ\Publish\API\Repository\Values\User\Role $role
315
     * @param \eZ\Publish\API\Repository\RoleService $roleService
316
     * @param \eZ\Publish\API\Repository\UserService $userService
317
     * @param array $assignments
318
     */
319 1
    protected function assignRole(Role $role, RoleService $roleService, UserService $userService, array $assignments)
320
    {
321 1
        foreach ($assignments as $assign) {
322 1
            switch ($assign['type']) {
323 1 View Code Duplication
                case 'user':
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...
324 1
                    foreach ($assign['ids'] as $userId) {
325 1
                        $userId = $this->referenceResolver->resolveReference($userId);
326
327 1
                        $user = $userService->loadUser($userId);
328
329 1
                        if (!isset($assign['limitations'])) {
330
                            $roleService->assignRoleToUser($role, $user);
331
                        } else {
332 1
                            foreach ($assign['limitations'] as $limitation) {
333 1
                                $limitationObject = $this->createLimitation($roleService, $limitation);
334 1
                                $roleService->assignRoleToUser($role, $user, $limitationObject);
0 ignored issues
show
Documentation introduced by
$limitationObject is of type object<eZ\Publish\API\Re...Values\User\Limitation>, but the function expects a null|object<eZ\Publish\A...itation\RoleLimitation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
335
                            }
336
                        }
337
                    }
338 1
                    break;
339 View Code Duplication
                case 'group':
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...
340
                    foreach ($assign['ids'] as $groupId) {
341
                        $groupId = $this->referenceResolver->resolveReference($groupId);
342
343
                        $group = $userService->loadUserGroup($groupId);
344
345
                        if (!isset($assign['limitations'])) {
346
                            // q: why are we swallowing exceptions here ?
347
                            //try {
348
                                $roleService->assignRoleToUserGroup($role, $group);
349
                            //} catch (InvalidArgumentException $e) {}
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
350
                        } else {
351
                            foreach ($assign['limitations'] as $limitation) {
352
                                $limitationObject = $this->createLimitation($roleService, $limitation);
353
                                // q: why are we swallowing exceptions here ?
354
                                //try {
355
                                    $roleService->assignRoleToUserGroup($role, $group, $limitationObject);
0 ignored issues
show
Documentation introduced by
$limitationObject is of type object<eZ\Publish\API\Re...Values\User\Limitation>, but the function expects a null|object<eZ\Publish\A...itation\RoleLimitation>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
356
                                //} catch (InvalidArgumentException $e) {}
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
357
                            }
358
                        }
359
                    }
360 1
                    break;
361
            }
362
        }
363 1
    }
364
365
    /**
366
     * Add new policies to the $role Role.
367
     *
368
     * @param \eZ\Publish\API\Repository\Values\User\Role $role
369
     * @param \eZ\Publish\API\Repository\RoleService $roleService
370
     * @param array $policy
371
     */
372 1
    protected function addPolicy(Role $role, RoleService $roleService, array $policy)
373
    {
374 1
        $policyCreateStruct = $roleService->newPolicyCreateStruct($policy['module'], $policy['function']);
375
376 1
        if (array_key_exists('limitations', $policy)) {
377 1
            foreach ($policy['limitations'] as $limitation) {
378 1
                $limitationObject = $this->createLimitation($roleService, $limitation);
379 1
                $policyCreateStruct->addLimitation($limitationObject);
380
            }
381
        }
382
383 1
        $roleService->addPolicy($role, $policyCreateStruct);
384 1
    }
385
}
386