Completed
Pull Request — master (#26)
by Bernhard
23:59 queued 11:58
created

PackageJsonSerializer   F

Complexity

Total Complexity 87

Size/Duplication

Total Lines 530
Duplicated Lines 8.49 %

Coupling/Cohesion

Components 1
Dependencies 19

Test Coverage

Coverage 97.18%

Importance

Changes 7
Bugs 1 Features 2
Metric Value
wmc 87
c 7
b 1
f 2
lcom 1
cbo 19
dl 45
loc 530
ccs 241
cts 248
cp 0.9718
rs 1.5789

15 Methods

Rating   Name   Duplication   Size   Complexity  
A compareBindingDescriptors() 0 5 1
A serializePackageFile() 17 17 1
A serializeRootPackageFile() 18 18 1
A unserializePackageFile() 0 16 1
A unserializeRootPackageFile() 0 17 1
A encode() 0 11 1
A decode() 0 20 3
A __construct() 0 15 2
D packageFileToJson() 0 110 22
C rootPackageFileToJson() 0 55 10
D jsonToPackageFile() 0 84 23
C jsonToRootPackageFile() 0 40 11
A objectsToArrays() 10 10 3
A assertVersionSupported() 0 10 2
B validate() 0 23 5

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 PackageJsonSerializer 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 PackageJsonSerializer, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of the puli/manager package.
5
 *
6
 * (c) Bernhard Schussek <[email protected]>
7
 *
8
 * For the full copyright and license information, please view the LICENSE
9
 * file that was distributed with this source code.
10
 */
11
12
namespace Puli\Manager\Package;
13
14
use Puli\Discovery\Api\Type\BindingParameter;
15
use Puli\Discovery\Api\Type\BindingType;
16
use Puli\Discovery\Binding\ClassBinding;
17
use Puli\Discovery\Binding\ResourceBinding;
18
use Puli\Manager\Api\Config\Config;
19
use Puli\Manager\Api\Discovery\BindingDescriptor;
20
use Puli\Manager\Api\Discovery\BindingTypeDescriptor;
21
use Puli\Manager\Api\Environment;
22
use Puli\Manager\Api\InvalidConfigException;
23
use Puli\Manager\Api\Package\InstallInfo;
24
use Puli\Manager\Api\Package\PackageFile;
25
use Puli\Manager\Api\Package\PackageFileSerializer;
26
use Puli\Manager\Api\Package\RootPackageFile;
27
use Puli\Manager\Api\Package\UnsupportedVersionException;
28
use Puli\Manager\Api\Repository\PathMapping;
29
use Puli\Manager\Migration\MigrationManager;
30
use Rhumsaa\Uuid\Uuid;
31
use stdClass;
32
use Webmozart\Json\DecodingFailedException;
33
use Webmozart\Json\JsonDecoder;
34
use Webmozart\Json\JsonEncoder;
35
use Webmozart\Json\JsonValidator;
36
use Webmozart\PathUtil\Path;
37
38
/**
39
 * Serializes and unserializes package files to/from JSON.
40
 *
41
 * The JSON is validated against the schema `res/schema/package-schema.json`.
42
 *
43
 * @since  1.0
44
 *
45
 * @author Bernhard Schussek <[email protected]>
46
 */
47
class PackageJsonSerializer implements PackageFileSerializer
48
{
49
    /**
50
     * The default order of the keys in the written package file.
51
     *
52
     * @var string[]
53
     */
54
    private static $keyOrder = array(
55
        'version',
56
        'name',
57
        'path-mappings',
58
        'bindings',
59
        'binding-types',
60
        'override',
61
        'override-order',
62
        'config',
63
        'plugins',
64
        'extra',
65
        'packages',
66
    );
67
68
    /**
69
     * @var MigrationManager
70
     */
71
    private $migrationManager;
72
73
    /**
74
     * @var string
75
     */
76
    private $schemaDir;
77
78
    /**
79
     * @var string
80
     */
81
    private $targetVersion;
82
83
    /**
84
     * @var string[]
85
     */
86
    private $knownVersions;
87
88 3
    public static function compareBindingDescriptors(BindingDescriptor $a, BindingDescriptor $b)
89
    {
90
        // Make sure that bindings are always printed in the same order
91 3
        return strcmp($a->getUuid()->toString(), $b->getUuid()->toString());
92
    }
93
94
    /**
95
     * Creates a new serializer.
96
     *
97
     * @param MigrationManager $migrationManager The manager for migrating
98
     *                                           puli.json files between
99
     *                                           versions.
100
     * @param string           $schemaDir        The directory that contains the
101
     *                                           schema files.
102
     * @param string           $targetVersion    The file version that this
103
     *                                           serializer reads and produces.
104
     */
105 77
    public function __construct(MigrationManager $migrationManager, $schemaDir, $targetVersion = PackageFile::DEFAULT_VERSION)
106
    {
107 77
        $this->migrationManager = $migrationManager;
108 77
        $this->targetVersion = $targetVersion;
109 77
        $this->knownVersions = $this->migrationManager->getKnownVersions();
110
111 77
        if (!in_array($targetVersion, $this->knownVersions, true)) {
112 26
            $this->knownVersions[] = $targetVersion;
113 26
            usort($this->knownVersions, 'version_compare');
114
        }
115
116
        // We can't use realpath(), which doesn't work inside PHARs.
117
        // However, we want to display nice paths if the file is not found.
118 77
        $this->schemaDir = Path::canonicalize($schemaDir);
119 77
    }
120
121
    /**
122
     * {@inheritdoc}
123
     */
124 15 View Code Duplication
    public function serializePackageFile(PackageFile $packageFile)
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...
125
    {
126 15
        $this->assertVersionSupported($packageFile->getVersion());
127
128 15
        $jsonData = (object) array('version' => $this->targetVersion);
129
130 15
        $this->packageFileToJson($packageFile, $jsonData);
131
132
        // Sort according to key order
133 15
        $jsonArray = (array) $jsonData;
134 15
        $orderedKeys = array_intersect_key(array_flip(self::$keyOrder), $jsonArray);
135 15
        $jsonData = (object) array_replace($orderedKeys, $jsonArray);
136
137 15
        $this->migrationManager->migrate($jsonData, $packageFile->getVersion());
138
139 15
        return $this->encode($jsonData, $packageFile->getPath());
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145 7 View Code Duplication
    public function serializeRootPackageFile(RootPackageFile $packageFile)
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 7
        $this->assertVersionSupported($packageFile->getVersion());
148
149 7
        $jsonData = (object) array('version' => $this->targetVersion);
150
151 7
        $this->packageFileToJson($packageFile, $jsonData);
152 7
        $this->rootPackageFileToJson($packageFile, $jsonData);
153
154
        // Sort according to key order
155 7
        $jsonArray = (array) $jsonData;
156 7
        $orderedKeys = array_intersect_key(array_flip(self::$keyOrder), $jsonArray);
157 7
        $jsonData = (object) array_replace($orderedKeys, $jsonArray);
158
159 7
        $this->migrationManager->migrate($jsonData, $packageFile->getVersion());
160
161 7
        return $this->encode($jsonData, $packageFile->getPath());
162
    }
163
164
    /**
165
     * {@inheritdoc}
166
     */
167 35
    public function unserializePackageFile($serialized, $path = null)
168
    {
169 35
        $packageFile = new PackageFile(null, $path);
170
171 35
        $jsonData = $this->decode($serialized, $path);
172
173
        // Remember original version of the package file
174 24
        $packageFile->setVersion($jsonData->version);
175
176
        // Migrate to the expected version
177 24
        $this->migrationManager->migrate($jsonData, $this->targetVersion);
178
179 24
        $this->jsonToPackageFile($jsonData, $packageFile);
180
181 24
        return $packageFile;
182
    }
183
184
    /**
185
     * {@inheritdoc}
186
     */
187 34
    public function unserializeRootPackageFile($serialized, $path = null, Config $baseConfig = null)
188
    {
189 34
        $packageFile = new RootPackageFile(null, $path, $baseConfig);
190
191 34
        $jsonData = $this->decode($serialized, $path);
192
193
        // Remember original version of the package file
194 31
        $packageFile->setVersion($jsonData->version);
195
196
        // Migrate to the expected version
197 31
        $this->migrationManager->migrate($jsonData, $this->targetVersion);
198
199 31
        $this->jsonToPackageFile($jsonData, $packageFile);
200 31
        $this->jsonToRootPackageFile($jsonData, $packageFile);
201
202 31
        return $packageFile;
203
    }
204
205 22
    private function packageFileToJson(PackageFile $packageFile, stdClass $jsonData)
206
    {
207 22
        $mappings = $packageFile->getPathMappings();
208 22
        $bindingDescriptors = $packageFile->getBindingDescriptors();
209 22
        $typeDescriptors = $packageFile->getTypeDescriptors();
210 22
        $overrides = $packageFile->getOverriddenPackages();
211 22
        $extra = $packageFile->getExtraKeys();
212
213 22
        if (null !== $packageFile->getPackageName()) {
214 6
            $jsonData->name = $packageFile->getPackageName();
215
        }
216
217 22
        if (count($mappings) > 0) {
218 4
            $jsonData->{'path-mappings'} = new stdClass();
219
220 4
            foreach ($mappings as $mapping) {
221 4
                $puliPath = $mapping->getRepositoryPath();
222 4
                $localPaths = $mapping->getPathReferences();
223
224 4
                $jsonData->{'path-mappings'}->$puliPath = count($localPaths) > 1 ? $localPaths : reset($localPaths);
225
            }
226
        }
227
228 22
        if (count($bindingDescriptors) > 0) {
229 6
            uasort($bindingDescriptors, array(__CLASS__, 'compareBindingDescriptors'));
230
231 6
            $jsonData->bindings = new stdClass();
232
233 6
            foreach ($bindingDescriptors as $bindingDescriptor) {
234 6
                $binding = $bindingDescriptor->getBinding();
235 6
                $bindingData = new stdClass();
236 6
                $bindingData->_class = get_class($binding);
237
238
                // This needs to be moved to external classes to allow adding
239
                // custom binding classes at some point
240 6
                if ($binding instanceof ResourceBinding) {
241 6
                    $bindingData->query = $binding->getQuery();
242
243 6
                    if ('glob' !== $binding->getLanguage()) {
244 6
                        $bindingData->language = $binding->getLanguage();
245
                    }
246
                } elseif ($binding instanceof ClassBinding) {
247 3
                    $bindingData->class = $binding->getClassName();
248
                }
249
250 6
                $bindingData->type = $bindingDescriptor->getTypeName();
251
252
                // Don't include the default values of the binding type
253 6
                if ($binding->hasParameterValues(false)) {
254 1
                    $bindingData->parameters = $binding->getParameterValues(false);
255 1
                    ksort($bindingData->parameters);
256
                }
257
258 6
                $jsonData->bindings->{$bindingDescriptor->getUuid()->toString()} = $bindingData;
259
            }
260
        }
261
262 22
        if (count($typeDescriptors) > 0) {
263 8
            $bindingTypesData = array();
264
265 8
            foreach ($typeDescriptors as $typeDescriptor) {
266 8
                $type = $typeDescriptor->getType();
267 8
                $typeData = new stdClass();
268
269 8
                if ($typeDescriptor->getDescription()) {
270 2
                    $typeData->description = $typeDescriptor->getDescription();
271
                }
272
273 8
                if ($type->hasParameters()) {
274 6
                    $parametersData = array();
275
276 6
                    foreach ($type->getParameters() as $parameter) {
277 6
                        $parameterData = new stdClass();
278
279 6
                        if ($typeDescriptor->hasParameterDescription($parameter->getName())) {
280 3
                            $parameterData->description = $typeDescriptor->getParameterDescription($parameter->getName());
281
                        }
282
283 6
                        if ($parameter->isRequired()) {
284 1
                            $parameterData->required = true;
285
                        }
286
287 6
                        if (null !== $parameter->getDefaultValue()) {
288 3
                            $parameterData->default = $parameter->getDefaultValue();
289
                        }
290
291 6
                        $parametersData[$parameter->getName()] = $parameterData;
292
                    }
293
294 6
                    ksort($parametersData);
295
296 6
                    $typeData->parameters = (object) $parametersData;
297
                }
298
299 8
                $bindingTypesData[$type->getName()] = $typeData;
300
            }
301
302 8
            ksort($bindingTypesData);
303
304 8
            $jsonData->{'binding-types'} = (object) $bindingTypesData;
305
        }
306
307 22
        if (count($overrides) > 0) {
308 3
            $jsonData->override = count($overrides) > 1 ? $overrides : reset($overrides);
309
        }
310
311 22
        if (count($extra) > 0) {
312 2
            $jsonData->extra = (object) $extra;
313
        }
314 22
    }
315
316 7
    private function rootPackageFileToJson(RootPackageFile $packageFile, stdClass $jsonData)
317
    {
318 7
        $overrideOrder = $packageFile->getOverrideOrder();
319 7
        $installInfos = $packageFile->getInstallInfos();
320
321
        // Pass false to exclude base configuration values
322 7
        $configValues = $packageFile->getConfig()->toRawArray(false);
323
324 7
        if (count($overrideOrder) > 0) {
325 1
            $jsonData->{'override-order'} = $overrideOrder;
326
        }
327
328 7
        if (count($configValues) > 0) {
329 1
            $jsonData->config = (object) $configValues;
330
        }
331
332 7
        if (array() !== $packageFile->getPluginClasses()) {
333 2
            $jsonData->plugins = $packageFile->getPluginClasses();
334
335 2
            sort($jsonData->plugins);
336
        }
337
338 7
        if (count($installInfos) > 0) {
339 3
            $packagesData = array();
340
341 3
            foreach ($installInfos as $installInfo) {
342 3
                $installData = new stdClass();
343 3
                $installData->{'install-path'} = $installInfo->getInstallPath();
344
345 3
                if (InstallInfo::DEFAULT_INSTALLER_NAME !== $installInfo->getInstallerName()) {
346 1
                    $installData->installer = $installInfo->getInstallerName();
347
                }
348
349 3
                if ($installInfo->hasDisabledBindingUuids()) {
350 2
                    $installData->{'disabled-bindings'} = array();
351
352 2
                    foreach ($installInfo->getDisabledBindingUuids() as $uuid) {
353 2
                        $installData->{'disabled-bindings'}[] = $uuid->toString();
354
                    }
355
356 2
                    sort($installData->{'disabled-bindings'});
357
                }
358
359 3
                if (Environment::PROD !== $installInfo->getEnvironment()) {
360 1
                    $installData->env = $installInfo->getEnvironment();
361
                }
362
363 3
                $packagesData[$installInfo->getPackageName()] = $installData;
364
            }
365
366 3
            ksort($packagesData);
367
368 3
            $jsonData->packages = (object) $packagesData;
369
        }
370 7
    }
371
372 41
    private function jsonToPackageFile(stdClass $jsonData, PackageFile $packageFile)
373
    {
374 41
        if (isset($jsonData->name)) {
375 31
            $packageFile->setPackageName($jsonData->name);
376
        }
377
378 41
        if (isset($jsonData->{'path-mappings'})) {
379 4
            foreach ($jsonData->{'path-mappings'} as $path => $relativePaths) {
380 4
                $packageFile->addPathMapping(new PathMapping($path, (array) $relativePaths));
381
            }
382
        }
383
384 41
        if (isset($jsonData->bindings)) {
385 8
            foreach ($jsonData->bindings as $uuid => $bindingData) {
386 8
                $binding = null;
387 8
                $class = isset($bindingData->_class)
388 6
                    ? $bindingData->_class
389 8
                    : 'Puli\Discovery\Binding\ResourceBinding';
390
391
                // Move this code to external classes to allow use of custom
392
                // bindings
393
                switch ($class) {
394 8
                    case 'Puli\Discovery\Binding\ClassBinding':
395 5
                        $binding = new ClassBinding(
396 5
                            $bindingData->class,
397 5
                            $bindingData->type,
398 5
                            isset($bindingData->parameters) ? (array) $bindingData->parameters : array(),
399 5
                            Uuid::fromString($uuid)
400
                        );
401 5
                        break;
402
                    case 'Puli\Discovery\Binding\ResourceBinding':
403 7
                        $binding = new ResourceBinding(
404 7
                            $bindingData->query,
405 7
                            $bindingData->type,
406 7
                            isset($bindingData->parameters) ? (array) $bindingData->parameters : array(),
407 7
                            isset($bindingData->language) ? $bindingData->language : 'glob',
408 7
                            Uuid::fromString($uuid)
409
                        );
410 7
                        break;
411
                    default:
412
                        continue;
413
                }
414
415 8
                $packageFile->addBindingDescriptor(new BindingDescriptor($binding));
0 ignored issues
show
Bug introduced by
It seems like $binding defined by null on line 386 can also be of type null; however, Puli\Manager\Api\Discove...scriptor::__construct() does only seem to accept object<Puli\Discovery\Api\Binding\Binding>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
416
            }
417
        }
418
419 41
        if (isset($jsonData->{'binding-types'})) {
420 5
            foreach ((array) $jsonData->{'binding-types'} as $typeName => $data) {
421 5
                $parameters = array();
422 5
                $parameterDescriptions = array();
423
424 5
                if (isset($data->parameters)) {
425 5
                    foreach ((array) $data->parameters as $parameterName => $parameterData) {
426 5
                        $required = isset($parameterData->required) ? $parameterData->required : false;
427
428 5
                        $parameters[] = new BindingParameter(
429
                            $parameterName,
430 5
                            $required ? BindingParameter::REQUIRED : BindingParameter::OPTIONAL,
431 5
                            isset($parameterData->default) ? $parameterData->default : null
432
                        );
433
434 5
                        if (isset($parameterData->description)) {
435 5
                            $parameterDescriptions[$parameterName] = $parameterData->description;
436
                        };
437
                    }
438
                }
439
440 5
                $packageFile->addTypeDescriptor(new BindingTypeDescriptor(
441 5
                    new BindingType($typeName, $parameters),
442 5
                    isset($data->description) ? $data->description : null,
443
                    $parameterDescriptions
444
                ));
445
            }
446
        }
447
448 41
        if (isset($jsonData->override)) {
449 5
            $packageFile->setOverriddenPackages((array) $jsonData->override);
450
        }
451
452 41
        if (isset($jsonData->extra)) {
453 4
            $packageFile->setExtraKeys((array) $jsonData->extra);
454
        }
455 41
    }
456
457 31
    private function jsonToRootPackageFile(stdClass $jsonData, RootPackageFile $packageFile)
458
    {
459 31
        if (isset($jsonData->{'override-order'})) {
460 2
            $packageFile->setOverrideOrder((array) $jsonData->{'override-order'});
461
        }
462
463 31
        if (isset($jsonData->plugins)) {
464 28
            $packageFile->setPluginClasses($jsonData->plugins);
465
        }
466
467 31
        if (isset($jsonData->config)) {
468 3
            $config = $packageFile->getConfig();
469
470 3
            foreach ($this->objectsToArrays($jsonData->config) as $key => $value) {
471 3
                $config->set($key, $value);
472
            }
473
        }
474
475 31
        if (isset($jsonData->packages)) {
476 28
            foreach ($jsonData->packages as $packageName => $packageData) {
477 28
                $installInfo = new InstallInfo($packageName, $packageData->{'install-path'});
478
479 28
                if (isset($packageData->env)) {
480 2
                    $installInfo->setEnvironment($packageData->env);
481
                }
482
483 28
                if (isset($packageData->installer)) {
484 2
                    $installInfo->setInstallerName($packageData->installer);
485
                }
486
487 28
                if (isset($packageData->{'disabled-bindings'})) {
488 2
                    foreach ($packageData->{'disabled-bindings'} as $uuid) {
489 2
                        $installInfo->addDisabledBindingUuid(Uuid::fromString($uuid));
490
                    }
491
                }
492
493 28
                $packageFile->addInstallInfo($installInfo);
494
            }
495
        }
496 31
    }
497
498 22
    private function encode(stdClass $jsonData, $path = null)
499
    {
500 22
        $encoder = new JsonEncoder();
501 22
        $encoder->setPrettyPrinting(true);
502 22
        $encoder->setEscapeSlash(false);
503 22
        $encoder->setTerminateWithLineFeed(true);
504
505 22
        $this->validate($jsonData, $path);
506
507 22
        return $encoder->encode($jsonData);
508
    }
509
510 55
    private function decode($json, $path = null)
511
    {
512 55
        $decoder = new JsonDecoder();
513
514
        try {
515 55
            $jsonData = $decoder->decode($json);
516 2
        } catch (DecodingFailedException $e) {
517 2
            throw new InvalidConfigException(sprintf(
518 2
                "The configuration%s could not be decoded:\n%s",
519 2
                $path ? ' in '.$path : '',
520 2
                $e->getMessage()
521 2
            ), $e->getCode(), $e);
522
        }
523
524 53
        $this->assertVersionSupported($jsonData->version, $path);
525
526 51
        $this->validate($jsonData, $path);
527
528 41
        return $jsonData;
529
    }
530
531 3 View Code Duplication
    private function objectsToArrays($data)
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...
532
    {
533 3
        $data = (array) $data;
534
535 3
        foreach ($data as $key => $value) {
536 3
            $data[$key] = is_object($value) ? $this->objectsToArrays($value) : $value;
537
        }
538
539 3
        return $data;
540
    }
541
542 75
    private function assertVersionSupported($version, $path = null)
543
    {
544 75
        if (!in_array($version, $this->knownVersions, true)) {
545 2
            throw UnsupportedVersionException::forVersion(
546
                $version,
547 2
                $this->knownVersions,
548
                $path
549
            );
550
        }
551 73
    }
552
553 73
    private function validate($jsonData, $path = null)
554
    {
555 73
        $validator = new JsonValidator();
556 73
        $schema = $this->schemaDir.'/package-schema-'.$jsonData->version.'.json';
557
558 73
        if (!file_exists($schema)) {
559
            throw new InvalidConfigException(sprintf(
560
                'The JSON schema file for version %s was not found%s.',
561
                $jsonData->version,
562
                $path ? ' in '.$path : ''
563
            ));
564
        }
565
566 73
        $errors = $validator->validate($jsonData, $schema);
567
568 73
        if (count($errors) > 0) {
569 10
            throw new InvalidConfigException(sprintf(
570 10
                "The configuration%s is invalid:\n%s",
571 10
                $path ? ' in '.$path : '',
572 10
                implode("\n", $errors)
573
            ));
574
        }
575 63
    }
576
}
577