Completed
Push — 1.0 ( de14e4...537877 )
by Bernhard
57:08 queued 17:45
created

AbstractJsonRepository   D

Complexity

Total Complexity 63

Size/Duplication

Total Lines 612
Duplicated Lines 1.8 %

Coupling/Cohesion

Components 1
Dependencies 14

Test Coverage

Coverage 98.86%

Importance

Changes 15
Bugs 4 Features 2
Metric Value
wmc 63
c 15
b 4
f 2
lcom 1
cbo 14
dl 11
loc 612
ccs 174
cts 176
cp 0.9886
rs 4.9324

28 Methods

Rating   Name   Duplication   Size   Complexity  
A setLogger() 0 4 1
insertReference() 0 1 ?
removeReferences() 0 1 ?
getReferencesForPath() 0 1 ?
getReferencesForGlob() 0 1 ?
getReferencesForRegex() 0 1 ?
getReferencesInDirectory() 0 1 ?
A createResources() 0 8 2
A ensureDirectoryExists() 0 13 3
A __construct() 0 14 2
B add() 11 26 4
A get() 0 16 3
A find() 0 14 2
A contains() 0 13 2
A remove() 0 17 2
A clear() 0 15 2
A listChildren() 0 21 4
B hasChildren() 0 22 4
A log() 0 6 2
A logReferenceNotFound() 0 9 2
A addFilesystemResource() 0 11 1
B load() 0 26 6
B flush() 0 27 5
A isLinkReference() 0 4 2
A isFilesystemReference() 0 4 2
B createResource() 0 21 6
B addResource() 0 24 4
A getShortClassName() 0 8 2

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

1
<?php
2
3
/*
4
 * This file is part of the puli/repository 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\Repository;
13
14
use Psr\Log\LoggerAwareInterface;
15
use Psr\Log\LoggerInterface;
16
use Psr\Log\LogLevel;
17
use Puli\Repository\Api\ChangeStream\ChangeStream;
18
use Puli\Repository\Api\Resource\FilesystemResource;
19
use Puli\Repository\Api\Resource\PuliResource;
20
use Puli\Repository\Api\ResourceCollection;
21
use Puli\Repository\Api\ResourceNotFoundException;
22
use Puli\Repository\Api\UnsupportedResourceException;
23
use Puli\Repository\Resource\Collection\ArrayResourceCollection;
24
use Puli\Repository\Resource\DirectoryResource;
25
use Puli\Repository\Resource\FileResource;
26
use Puli\Repository\Resource\GenericResource;
27
use Puli\Repository\Resource\LinkResource;
28
use RuntimeException;
29
use Webmozart\Assert\Assert;
30
use Webmozart\Json\JsonDecoder;
31
use Webmozart\Json\JsonEncoder;
32
use Webmozart\PathUtil\Path;
33
34
/**
35
 * Base class for repositories backed by a JSON file.
36
 *
37
 * The generated JSON file is described by res/schema/repository-schema-1.0.json.
38
 *
39
 * @since  1.0
40
 *
41
 * @author Bernhard Schussek <[email protected]>
42
 * @author Titouan Galopin <[email protected]>
43
 */
44
abstract class AbstractJsonRepository extends AbstractEditableRepository implements LoggerAwareInterface
45
{
46
    /**
47
     * Flag: Whether to stop after the first result.
48
     *
49
     * @internal
50
     */
51
    const STOP_ON_FIRST = 1;
52
53
    /**
54
     * @var array
55
     */
56
    protected $json;
57
58
    /**
59
     * @var string
60
     */
61
    protected $baseDirectory;
62
63
    /**
64
     * @var string
65
     */
66
    private $path;
67
68
    /**
69
     * @var string
70
     */
71
    private $schemaPath;
72
73
    /**
74
     * @var JsonEncoder
75
     */
76
    private $encoder;
77
78
    /**
79
     * @var LoggerInterface
80
     */
81
    private $logger;
82
83
    /**
84
     * Creates a new repository.
85
     *
86
     * @param string            $path          The path to the JSON file. If
87
     *                                         relative, it must be relative to
88
     *                                         the base directory.
89
     * @param string            $baseDirectory The base directory of the store.
90
     *                                         Paths inside that directory are
91
     *                                         stored as relative paths. Paths
92
     *                                         outside that directory are stored
93
     *                                         as absolute paths.
94
     * @param bool              $validateJson  Whether to validate the JSON file
95
     *                                         against the schema. Slow but
96
     *                                         spots problems.
97
     * @param ChangeStream|null $changeStream  If provided, the repository will
98
     *                                         append resource changes to this
99
     *                                         change stream.
100
     */
101 392
    public function __construct($path, $baseDirectory, $validateJson = false, ChangeStream $changeStream = null)
102
    {
103 392
        parent::__construct($changeStream);
104
105 392
        $this->baseDirectory = $baseDirectory;
106 392
        $this->path = Path::makeAbsolute($path, $baseDirectory);
107 392
        $this->encoder = new JsonEncoder();
108 392
        $this->encoder->setPrettyPrinting(true);
109 392
        $this->encoder->setEscapeSlash(false);
110
111 392
        if ($validateJson) {
112 392
            $this->schemaPath = realpath(__DIR__.'/../res/schema/path-mappings-schema-1.0.json');
113
        }
114 392
    }
115
116
    /**
117
     * {@inheritdoc}
118
     */
119 22
    public function setLogger(LoggerInterface $logger = null)
120
    {
121 22
        $this->logger = $logger;
122 22
    }
123
124
    /**
125
     * {@inheritdoc}
126
     */
127 352
    public function add($path, $resource)
128
    {
129 352
        if (null === $this->json) {
130 352
            $this->load();
131
        }
132
133 352
        $path = $this->sanitizePath($path);
134
135 340 View Code Duplication
        if ($resource instanceof ResourceCollection) {
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...
136 4
            $this->ensureDirectoryExists($path);
137
138 4
            foreach ($resource as $child) {
139 4
                $this->addResource($path.'/'.$child->getName(), $child);
140
            }
141
142 4
            $this->flush();
143
144 4
            return;
145
        }
146
147 336
        $this->ensureDirectoryExists(Path::getDirectory($path));
148
149 336
        $this->addResource($path, $resource);
0 ignored issues
show
Documentation introduced by
$resource is of type object<Puli\Repository\Api\Resource\PuliResource>, but the function expects a object<Puli\Repository\A...\Resource\LinkResource>.

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...
150
151 332
        $this->flush();
152 332
    }
153
154
    /**
155
     * {@inheritdoc}
156
     */
157 164
    public function get($path)
158
    {
159 164
        if (null === $this->json) {
160 28
            $this->load();
161
        }
162
163 164
        $path = $this->sanitizePath($path);
164 152
        $references = $this->getReferencesForPath($path);
165
166
        // Might be null, don't use isset()
167 152
        if (array_key_exists($path, $references)) {
168 142
            return $this->createResource($path, $references[$path]);
169
        }
170
171 10
        throw ResourceNotFoundException::forPath($path);
172
    }
173
174
    /**
175
     * {@inheritdoc}
176
     */
177 48
    public function find($query, $language = 'glob')
178
    {
179 48
        if (null === $this->json) {
180 6
            $this->load();
181
        }
182
183 48
        $this->failUnlessGlob($language);
184 44
        $query = $this->sanitizePath($query);
185 32
        $results = $this->createResources($this->getReferencesForGlob($query));
186
187 32
        ksort($results);
188
189 32
        return new ArrayResourceCollection(array_values($results));
190
    }
191
192
    /**
193
     * {@inheritdoc}
194
     */
195 68
    public function contains($query, $language = 'glob')
196
    {
197 68
        if (null === $this->json) {
198 22
            $this->load();
199
        }
200
201 68
        $this->failUnlessGlob($language);
202 64
        $query = $this->sanitizePath($query);
203
204 52
        $results = $this->getReferencesForGlob($query, self::STOP_ON_FIRST);
205
206 52
        return !empty($results);
207
    }
208
209
    /**
210
     * {@inheritdoc}
211
     */
212 66
    public function remove($query, $language = 'glob')
213
    {
214 66
        if (null === $this->json) {
215 24
            $this->load();
216
        }
217
218 66
        $this->failUnlessGlob($language);
219 62
        $query = $this->sanitizePath($query);
220
221 50
        Assert::notEmpty(trim($query, '/'), 'The root directory cannot be removed.');
222
223 42
        $removed = $this->removeReferences($query);
224
225 40
        $this->flush();
226
227 40
        return $removed;
228
    }
229
230
    /**
231
     * {@inheritdoc}
232
     */
233 6
    public function clear()
234
    {
235 6
        if (null === $this->json) {
236 2
            $this->load();
237
        }
238
239
        // Subtract root which is not deleted
240 6
        $removed = count($this->getReferencesForRegex('/', '~.~')) - 1;
241
242 6
        $this->json = array();
243
244 6
        $this->flush();
245
246 6
        return $removed;
247
    }
248
249
    /**
250
     * {@inheritdoc}
251
     */
252 48
    public function listChildren($path)
253
    {
254 48
        if (null === $this->json) {
255 2
            $this->load();
256
        }
257
258 48
        $path = $this->sanitizePath($path);
259 36
        $results = $this->createResources($this->getReferencesInDirectory($path));
260
261 36
        if (empty($results)) {
262 16
            $pathResults = $this->getReferencesForPath($path);
263
264 16
            if (empty($pathResults)) {
265 4
                throw ResourceNotFoundException::forPath($path);
266
            }
267
        }
268
269 32
        ksort($results);
270
271 32
        return new ArrayResourceCollection(array_values($results));
272
    }
273
274
    /**
275
     * {@inheritdoc}
276
     */
277 32
    public function hasChildren($path)
278
    {
279 32
        if (null === $this->json) {
280 2
            $this->load();
281
        }
282
283 32
        $path = $this->sanitizePath($path);
284
285 20
        $results = $this->getReferencesInDirectory($path, self::STOP_ON_FIRST);
286
287 20
        if (empty($results)) {
288 12
            $pathResults = $this->getReferencesForPath($path);
289
290 12
            if (empty($pathResults)) {
291 4
                throw ResourceNotFoundException::forPath($path);
292
            }
293
294 8
            return false;
295
        }
296
297 12
        return true;
298
    }
299
300
    /**
301
     * Inserts a path reference into the JSON file.
302
     *
303
     * The path reference can be:
304
     *
305
     *  * a link starting with `@`
306
     *  * an absolute filesystem path
307
     *
308
     * @param string      $path      The Puli path.
309
     * @param string|null $reference The path reference.
310
     */
311
    abstract protected function insertReference($path, $reference);
1 ignored issue
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
312
313
    /**
314
     * Removes all path references matching the given glob from the JSON file.
315
     *
316
     * @param string $glob The glob for a list of Puli paths.
317
     */
318
    abstract protected function removeReferences($glob);
1 ignored issue
show
Documentation introduced by
For interfaces and abstract methods it is generally a good practice to add a @return annotation even if it is just @return void or @return null, so that implementors know what to do in the overridden method.

For interface and abstract methods, it is impossible to infer the return type from the immediate code. In these cases, it is generally advisible to explicitly annotate these methods with a @return doc comment to communicate to implementors of these methods what they are expected to return.

Loading history...
319
320
    /**
321
     * Returns the references for a given Puli path.
322
     *
323
     * Each reference returned by this method can be:
324
     *
325
     *  * `null`
326
     *  * a link starting with `@`
327
     *  * an absolute filesystem path
328
     *
329
     * The result has either one entry or none, if no path was found. The key
330
     * of the single entry is the path passed to this method.
331
     *
332
     * @param string $path The Puli path.
333
     *
334
     * @return string[]|null[] A one-level array of references with Puli paths
335
     *                         as keys. The array has at most one entry.
336
     */
337
    abstract protected function getReferencesForPath($path);
338
339
    /**
340
     * Returns the references matching a given Puli path glob.
341
     *
342
     * Each reference returned by this method can be:
343
     *
344
     *  * `null`
345
     *  * a link starting with `@`
346
     *  * an absolute filesystem path
347
     *
348
     * The keys of the returned array are Puli paths. Their order is undefined.
349
     *
350
     * @param string $glob  The glob.
351
     * @param int    $flags A bitwise combination of the flag constants in this
352
     *                      class.
353
     *
354
     * @return string[]|null[] A one-level array of references with Puli paths
355
     *                         as keys.
356
     */
357
    abstract protected function getReferencesForGlob($glob, $flags = 0);
358
359
    /**
360
     * Returns the references matching a given Puli path regular expression.
361
     *
362
     * Each reference returned by this method can be:
363
     *
364
     *  * `null`
365
     *  * a link starting with `@`
366
     *  * an absolute filesystem path
367
     *
368
     * The keys of the returned array are Puli paths. Their order is undefined.
369
     *
370
     * @param string $staticPrefix The static prefix of all Puli paths matching
371
     *                             the regular expression.
372
     * @param string $regex        The regular expression.
373
     * @param int    $flags        A bitwise combination of the flag constants
374
     *                             in this class.
375
     *
376
     * @return string[]|null[] A one-level array of references with Puli paths
377
     *                         as keys.
378
     */
379
    abstract protected function getReferencesForRegex($staticPrefix, $regex, $flags = 0);
380
381
    /**
382
     * Returns the references in a given Puli path.
383
     *
384
     * Each reference returned by this method can be:
385
     *
386
     *  * `null`
387
     *  * a link starting with `@`
388
     *  * an absolute filesystem path
389
     *
390
     * The keys of the returned array are Puli paths. Their order is undefined.
391
     *
392
     * @param string $path  The Puli path.
393
     * @param int    $flags A bitwise combination of the flag constants in this
394
     *                      class.
395
     *
396
     * @return string[]|null[] A one-level array of references with Puli paths
397
     *                         as keys.
398
     */
399
    abstract protected function getReferencesInDirectory($path, $flags = 0);
400
401
    /**
402
     * Logs a message.
403
     *
404
     * @param mixed  $level   One of the level constants in {@link LogLevel}.
405
     * @param string $message The message.
406
     */
407 22
    protected function log($level, $message)
408
    {
409 22
        if (null !== $this->logger) {
410 22
            $this->logger->log($level, $message);
411
        }
412 22
    }
413
414
    /**
415
     * Logs a warning that a reference could not be found.
416
     *
417
     * @param string $path              The Puli path of a path mapping.
418
     * @param string $reference         The reference that was not found.
419
     * @param string $absoluteReference The absolute filesystem path of the
420
     *                                  reference.
421
     */
422 22
    protected function logReferenceNotFound($path, $reference, $absoluteReference)
423
    {
424 22
        $this->log(LogLevel::WARNING, sprintf(
425 22
            'The reference "%s"%s mapped by the path %s could not be found.',
426
            $reference,
427 22
            $reference !== $absoluteReference ? ' ('.$absoluteReference.')' : '',
428
            $path
429
        ));
430 22
    }
431
432
    /**
433
     * Adds a filesystem resource to the JSON file.
434
     *
435
     * @param string             $path     The Puli path.
436
     * @param FilesystemResource $resource The resource to add.
437
     */
438 336
    protected function addFilesystemResource($path, FilesystemResource $resource)
439
    {
440 336
        $resource = clone $resource;
441 336
        $resource->attachTo($this, $path);
442
443 336
        $relativePath = Path::makeRelative($resource->getFilesystemPath(), $this->baseDirectory);
444
445 336
        $this->insertReference($path, $relativePath);
446
447 336
        $this->storeVersion($resource);
448 336
    }
449
450
    /**
451
     * Loads the JSON file.
452
     */
453 392
    protected function load()
454
    {
455 392
        $decoder = new JsonDecoder();
456
457 392
        $this->json = file_exists($this->path)
458 69
            ? (array) $decoder->decodeFile($this->path, $this->schemaPath)
459 392
            : array();
460
461 392
        if (isset($this->json['_order'])) {
462 5
            $this->json['_order'] = (array) $this->json['_order'];
463
464 5
            foreach ($this->json['_order'] as $path => $entries) {
465 5
                foreach ($entries as $key => $entry) {
466 5
                    $this->json['_order'][$path][$key] = (array) $entry;
467
                }
468
            }
469
        }
470
471
        // The root node always exists
472 392
        if (!isset($this->json['/'])) {
473 392
            $this->json['/'] = null;
474
        }
475
476
        // Make sure the JSON is sorted in reverse order
477 392
        krsort($this->json);
478 392
    }
479
480
    /**
481
     * Writes the JSON file.
482
     */
483 340
    protected function flush()
484
    {
485
        // The root node always exists
486 340
        if (!isset($this->json['/'])) {
487 144
            $this->json['/'] = null;
488
        }
489
490
        // Always save in reverse order
491 340
        krsort($this->json);
492
493
        // Comply to schema
494 340
        $json = (object) $this->json;
495
496 340
        if (isset($json->{'_order'})) {
497 10
            $order = $json->{'_order'};
498
499 10
            foreach ($order as $path => $entries) {
500 10
                foreach ($entries as $key => $entry) {
501 10
                    $order[$path][$key] = (object) $entry;
502
                }
503
            }
504
505 10
            $json->{'_order'} = (object) $order;
506
        }
507
508 340
        $this->encoder->encodeFile($json, $this->path, $this->schemaPath);
509 340
    }
510
511
    /**
512
     * Returns whether a reference contains a link.
513
     *
514
     * @param string $reference The reference.
515
     *
516
     * @return bool Whether the reference contains a link.
517
     */
518 284
    protected function isLinkReference($reference)
519
    {
520 284
        return isset($reference{0}) && '@' === $reference{0};
521
    }
522
523
    /**
524
     * Returns whether a reference contains an absolute or relative filesystem
525
     * path.
526
     *
527
     * @param string $reference The reference.
528
     *
529
     * @return bool Whether the reference contains a filesystem path.
530
     */
531 290
    protected function isFilesystemReference($reference)
532
    {
533 290
        return null !== $reference && !$this->isLinkReference($reference);
534
    }
535
536
    /**
537
     * Turns a reference into a resource.
538
     *
539
     * @param string      $path      The Puli path.
540
     * @param string|null $reference The reference.
541
     *
542
     * @return PuliResource The resource.
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use GenericResource.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
543
     */
544 160
    protected function createResource($path, $reference)
545
    {
546 160
        if (null === $reference) {
547 12
            $resource = new GenericResource();
548 148
        } elseif (isset($reference{0}) && '@' === $reference{0}) {
549 8
            $resource = new LinkResource(substr($reference, 1));
550 148
        } elseif (is_dir($reference)) {
551 84
            $resource = new DirectoryResource($reference);
552 100
        } elseif (is_file($reference)) {
553 100
            $resource = new FileResource($reference);
554
        } else {
555
            throw new RuntimeException(sprintf(
556
                'Trying to create a FilesystemResource on a non-existing file or directory "%s"',
557
                $reference
558
            ));
559
        }
560
561 160
        $resource->attachTo($this, $path);
562
563 160
        return $resource;
564
    }
565
566
    /**
567
     * Turns a list of references into a list of resources.
568
     *
569
     * The references are expected to be in the format returned by
570
     * {@link getReferencesForPath()}, {@link getReferencesForGlob()} and
571
     * {@link getReferencesInDirectory()}.
572
     *
573
     * The result contains Puli paths as keys and {@link PuliResource}
574
     * implementations as values. The order of the results is undefined.
575
     *
576
     * @param string[]|null[] $references The references indexed by Puli paths.
577
     *
578
     * @return array
0 ignored issues
show
Documentation introduced by
Consider making the return type a bit more specific; maybe use array<string|null|GenericResource>.

This check looks for the generic type array as a return type and suggests a more specific type. This type is inferred from the actual code.

Loading history...
579
     */
580 68
    private function createResources(array $references)
581
    {
582 68
        foreach ($references as $path => $reference) {
583 44
            $references[$path] = $this->createResource($path, $reference);
0 ignored issues
show
Bug introduced by
It seems like $reference defined by $reference on line 582 can also be of type object<Puli\Repository\Resource\GenericResource>; however, Puli\Repository\Abstract...itory::createResource() does only seem to accept string|null, 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...
584
        }
585
586 68
        return $references;
587
    }
588
589
    /**
590
     * Adds all ancestor directories of a path to the repository.
591
     *
592
     * @param string $path A Puli path.
593
     */
594 340
    private function ensureDirectoryExists($path)
595
    {
596 340
        if (array_key_exists($path, $this->json)) {
597 340
            return;
598
        }
599
600
        // Recursively initialize parent directories
601 104
        if ('/' !== $path) {
602 104
            $this->ensureDirectoryExists(Path::getDirectory($path));
603
        }
604
605 104
        $this->json[$path] = null;
606 104
    }
607
608
    /**
609
     * Adds a resource to the repository.
610
     *
611
     * @param string                          $path     The Puli path to add the
612
     *                                                  resource at.
613
     * @param FilesystemResource|LinkResource $resource The resource to add.
614
     */
615 340
    private function addResource($path, $resource)
616
    {
617 340
        if (!$resource instanceof FilesystemResource && !$resource instanceof LinkResource) {
618 4
            throw new UnsupportedResourceException(sprintf(
619
                'The %s only supports adding FilesystemResource and '.
620 4
                'LinkedResource instances. Got: %s',
621
                // Get the short class name
622 4
                $this->getShortClassName(get_class($this)),
623 4
                $this->getShortClassName(get_class($resource))
624
            ));
625
        }
626
627 336
        if ($resource instanceof LinkResource) {
628 8
            $resource = clone $resource;
629 8
            $resource->attachTo($this, $path);
630
631 8
            $this->insertReference($path, '@'.$resource->getTargetPath());
632
633 8
            $this->storeVersion($resource);
634
        } else {
635
            // Extension point for the optimized repository
636 336
            $this->addFilesystemResource($path, $resource);
637
        }
638 336
    }
639
640
    /**
641
     * Returns the short name of a fully-qualified class name.
642
     *
643
     * @param string $className The fully-qualified class name.
644
     *
645
     * @return string The short class name.
646
     */
647 4
    private function getShortClassName($className)
648
    {
649 4
        if (false !== ($pos = strrpos($className, '\\'))) {
650 4
            return substr($className, $pos + 1);
651
        }
652
653 4
        return $className;
654
    }
655
}
656