Completed
Push — master ( 2aa028...155152 )
by Bernhard
17:49 queued 05:52
created

FactoryManagerImpl::addGetPackageOrderMethod()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 32
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 23
CRAP Score 3

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 32
ccs 23
cts 23
cp 1
rs 8.8571
cc 3
eloc 19
nc 2
nop 1
crap 3
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\Factory;
13
14
use Puli\Manager\Api\Config\Config;
15
use Puli\Manager\Api\Context\ProjectContext;
16
use Puli\Manager\Api\Event\GenerateFactoryEvent;
17
use Puli\Manager\Api\Event\PuliEvents;
18
use Puli\Manager\Api\Factory\FactoryManager;
19
use Puli\Manager\Api\Factory\Generator\GeneratorRegistry;
20
use Puli\Manager\Api\Package\PackageCollection;
21
use Puli\Manager\Api\Php\Argument;
22
use Puli\Manager\Api\Php\Clazz;
23
use Puli\Manager\Api\Php\Import;
24
use Puli\Manager\Api\Php\Method;
25
use Puli\Manager\Api\Php\ReturnValue;
26
use Puli\Manager\Api\Server\ServerCollection;
27
use Puli\Manager\Assert\Assert;
28
use Puli\Manager\Conflict\OverrideGraph;
29
use Puli\Manager\Php\ClassWriter;
30
use Webmozart\PathUtil\Path;
31
32
/**
33
 * The default {@link FactoryManager} implementation.
34
 *
35
 * @since  1.0
36
 *
37
 * @author Bernhard Schussek <[email protected]>
38
 */
39
class FactoryManagerImpl implements FactoryManager
40
{
41
    /**
42
     * The name of the resource repository variable.
43
     */
44
    const REPO_VAR_NAME = 'repo';
45
46
    /**
47
     * The name of the discovery variable.
48
     */
49
    const DISCOVERY_VAR_NAME = 'discovery';
50
51
    /**
52
     * @var ProjectContext
53
     */
54
    private $context;
55
56
    /**
57
     * @var Config
58
     */
59
    private $config;
60
61
    /**
62
     * @var string
63
     */
64
    private $rootDir;
65
66
    /**
67
     * @var GeneratorRegistry
68
     */
69
    private $generatorRegistry;
70
71
    /**
72
     * @var ClassWriter
73
     */
74
    private $classWriter;
75
76
    /**
77
     * @var PackageCollection
78
     */
79
    private $packages;
80
81
    /**
82
     * @var ServerCollection
83
     */
84
    private $servers;
85
86
    /**
87
     * Creates a new factory generator.
88
     *
89
     * @param ProjectContext         $context           The project context.
90
     * @param GeneratorRegistry      $generatorRegistry The registry providing
91
     *                                                  the generators for the
92
     *                                                  services returned by the
93
     *                                                  factory.
94
     * @param ClassWriter            $classWriter       The writer that writes
95
     *                                                  the class to a file.
96
     * @param PackageCollection|null $packages          The loaded packages.
97
     * @param ServerCollection|null  $servers           The configured servers.
98
     */
99 44
    public function __construct(ProjectContext $context, GeneratorRegistry $generatorRegistry, ClassWriter $classWriter, PackageCollection $packages = null, ServerCollection $servers = null)
100
    {
101 44
        $this->context = $context;
102 44
        $this->config = $context->getConfig();
103 44
        $this->rootDir = $context->getRootDirectory();
104 44
        $this->generatorRegistry = $generatorRegistry;
105 44
        $this->classWriter = $classWriter;
106 44
        $this->packages = $packages;
107 44
        $this->servers = $servers;
108 44
    }
109
110
    /**
111
     * Sets the packages included in the getPackageOrder() method.
112
     *
113
     * @param PackageCollection $packages The loaded packages.
114
     */
115 14
    public function setPackages(PackageCollection $packages)
116
    {
117 14
        $this->packages = $packages;
118 14
    }
119
120
    /**
121
     * Sets the servers included in the createUrlGenerator() method.
122
     *
123
     * @param ServerCollection $servers The configured servers.
124
     */
125 14
    public function setServers(ServerCollection $servers)
126
    {
127 14
        $this->servers = $servers;
128 14
    }
129
130
    /**
131
     * {@inheritdoc}
132
     */
133 13
    public function createFactory($path = null, $className = null)
134
    {
135 13
        Assert::nullOrStringNotEmpty($path, 'The path to the generated factory file must be a non-empty string or null. Got: %s');
136 13
        Assert::nullOrStringNotEmpty($className, 'The class name of the generated factory must be a non-empty string or null. Got: %s');
137
138 13
        $this->refreshFactoryClass($path, $className);
139
140 13
        $className = $className ?: $this->config->get(Config::FACTORY_IN_CLASS);
141 13
        $path = $path ?: $this->config->get(Config::FACTORY_IN_FILE);
142
143 13
        if (null !== $path && !class_exists($className, false)) {
144 12
            require_once Path::makeAbsolute($path, $this->rootDir);
145 12
        }
146
147 13
        return new $className();
148
    }
149
150
    /**
151
     * {@inheritdoc}
152
     */
153 1
    public function isFactoryClassAutoGenerated()
154
    {
155 1
        return $this->config->get(Config::FACTORY_AUTO_GENERATE);
156
    }
157
158
    /**
159
     * {@inheritdoc}
160
     */
161 32
    public function generateFactoryClass($path = null, $className = null)
162
    {
163 32
        Assert::nullOrStringNotEmpty($path, 'The path to the generated factory file must be a non-empty string or null. Got: %s');
164 30
        Assert::nullOrStringNotEmpty($className, 'The class name of the generated factory must be a non-empty string or null. Got: %s');
165
166 28
        $path = Path::makeAbsolute($path ?: $this->config->get(Config::FACTORY_OUT_FILE), $this->rootDir);
167 28
        $className = $className ?: $this->config->get(Config::FACTORY_OUT_CLASS);
168 28
        $dispatcher = $this->context->getEventDispatcher();
169
170 28
        $class = new Clazz($className);
171 28
        $class->setFilePath($path);
172 28
        $class->setDescription(
173
<<<EOF
174
Creates Puli's core services.
175
176
This class was auto-generated by Puli.
177
178
IMPORTANT: Before modifying the code below, set the "factory.auto-generate"
179
configuration key to false:
180
181
    $ puli config factory.auto-generate false
182
183
Otherwise any modifications will be overwritten!
184
EOF
185 28
        );
186
187 28
        $this->addCreateRepositoryMethod($class);
188 28
        $this->addCreateDiscoveryMethod($class);
189 28
        $this->addCreateUrlGeneratorMethod($class);
190 28
        $this->addGetPackageOrderMethod($class);
191
192 28
        if ($dispatcher->hasListeners(PuliEvents::GENERATE_FACTORY)) {
193 1
            $dispatcher->dispatch(PuliEvents::GENERATE_FACTORY, new GenerateFactoryEvent($class));
194 1
        }
195
196 28
        $this->classWriter->writeClass($class);
197 28
    }
198
199
    /**
200
     * {@inheritdoc}
201
     */
202 3
    public function autoGenerateFactoryClass($path = null, $className = null)
203
    {
204 3
        if (!$this->config->get(Config::FACTORY_AUTO_GENERATE)) {
205 1
            return;
206
        }
207
208 2
        $this->generateFactoryClass($path, $className);
209 2
    }
210
211
    /**
212
     * {@inheritdoc}
213
     */
214 24
    public function refreshFactoryClass($path = null, $className = null)
215
    {
216 24
        Assert::nullOrStringNotEmpty($path, 'The path to the generated factory file must be a non-empty string or null. Got: %s');
217 24
        Assert::nullOrStringNotEmpty($className, 'The class name of the generated factory must be a non-empty string or null. Got: %s');
218
219 24
        $path = Path::makeAbsolute($path ?: $this->config->get(Config::FACTORY_OUT_FILE), $this->rootDir);
220 24
        $className = $className ?: $this->config->get(Config::FACTORY_OUT_CLASS);
221
222 24
        if (!$this->config->get(Config::FACTORY_AUTO_GENERATE)) {
223 1
            return;
224
        }
225
226 23
        if (!file_exists($path)) {
227 17
            $this->generateFactoryClass($path, $className);
228
229 17
            return;
230
        }
231
232 6
        $rootPackageFile = $this->context->getRootPackageFile()->getPath();
233
234 6
        if (!file_exists($rootPackageFile)) {
235 1
            return;
236
        }
237
238
        // Regenerate file if the configuration has changed and
239
        // auto-generation is enabled
240 5
        clearstatcache(true, $rootPackageFile);
241 5
        $lastConfigChange = filemtime($rootPackageFile);
242
243 5
        $configFile = $this->context->getConfigFile()
244 5
            ? $this->context->getConfigFile()->getPath()
245 5
            : '';
246
247 5
        if (file_exists($configFile)) {
248 2
            clearstatcache(true, $configFile);
249 2
            $lastConfigChange = max(filemtime($configFile), $lastConfigChange);
250 2
        }
251
252 5
        clearstatcache(true, $path);
253 5
        $lastFactoryUpdate = filemtime($path);
254
255 5
        if ($lastConfigChange > $lastFactoryUpdate) {
256 3
            $this->generateFactoryClass($path, $className);
257 3
        }
258 5
    }
259
260
    /**
261
     * Adds the createRepository() method.
262
     *
263
     * @param Clazz $class The factory class model.
264
     */
265 28
    private function addCreateRepositoryMethod(Clazz $class)
266
    {
267 28
        $method = new Method('createRepository');
268 28
        $method->setDescription('Creates the resource repository.');
269 28
        $method->setReturnValue(new ReturnValue(
270 28
            '$'.self::REPO_VAR_NAME,
271 28
            'ResourceRepository',
272
            'The created resource repository.'
273 28
        ));
274 28
        $method->addBody(
275
<<<EOF
276
if (!interface_exists('Puli\Repository\Api\ResourceRepository')) {
277
    throw new RuntimeException('Please install puli/repository to create ResourceRepository instances.');
278
}
279
280
EOF
281 28
        );
282
283 28
        $class->addImport(new Import('Puli\Repository\Api\ResourceRepository'));
284 28
        $class->addImport(new Import('RuntimeException'));
285 28
        $class->addMethod($method);
286
287
        // Add method body
288 28
        $config = $this->config;
289 28
        $type = $config->get(Config::REPOSITORY_TYPE);
290 28
        $options = $this->camelizeKeys($config->get(Config::REPOSITORY));
291 28
        $options['root-dir'] = $this->rootDir;
292 28
        $options['change-stream'] = $config->get(Config::CHANGE_STREAM);
293
294 28
        $generator = $this->generatorRegistry->getServiceGenerator(GeneratorRegistry::REPOSITORY, $type);
295 28
        $generator->generateNewInstance(self::REPO_VAR_NAME, $method, $this->generatorRegistry, $options);
296 28
    }
297
298
    /**
299
     * Adds the createDiscovery() method.
300
     *
301
     * @param Clazz $class The factory class model.
302
     */
303 28
    private function addCreateDiscoveryMethod(Clazz $class)
304
    {
305 28
        $method = new Method('createDiscovery');
306 28
        $method->setDescription('Creates the resource discovery.');
307
308 28
        $arg = new Argument(self::REPO_VAR_NAME);
309 28
        $arg->setTypeHint('ResourceRepository');
310 28
        $arg->setType('ResourceRepository');
311 28
        $arg->setDescription('The resource repository to read from.');
312
313 28
        $method->addArgument($arg);
314
315 28
        $method->setReturnValue(new ReturnValue(
316 28
            '$'.self::DISCOVERY_VAR_NAME,
317 28
            'Discovery',
318
            'The created discovery.'
319 28
        ));
320
321 28
        $method->addBody(
322
<<<EOF
323
if (!interface_exists('Puli\Discovery\Api\Discovery')) {
324
    throw new RuntimeException('Please install puli/discovery to create Discovery instances.');
325
}
326
327
EOF
328 28
        );
329
330 28
        $class->addImport(new Import('Puli\Repository\Api\ResourceRepository'));
331 28
        $class->addImport(new Import('Puli\Discovery\Api\Discovery'));
332 28
        $class->addImport(new Import('RuntimeException'));
333 28
        $class->addMethod($method);
334
335
        // Add method body
336 28
        $config = $this->config;
337 28
        $type = $config->get(Config::DISCOVERY_TYPE);
338 28
        $options = $this->camelizeKeys($config->get(Config::DISCOVERY));
339 28
        $options['root-dir'] = $this->rootDir;
340
341 28
        $generator = $this->generatorRegistry->getServiceGenerator(GeneratorRegistry::DISCOVERY, $type);
342 28
        $generator->generateNewInstance(self::DISCOVERY_VAR_NAME, $method, $this->generatorRegistry, $options);
343 28
    }
344
345
    /**
346
     * Adds the createUrlGenerator() method.
347
     *
348
     * @param Clazz $class The factory class model.
349
     */
350 28
    public function addCreateUrlGeneratorMethod(Clazz $class)
351
    {
352 28
        $class->addImport(new Import('Puli\Discovery\Api\Discovery'));
353 28
        $class->addImport(new Import('Puli\Manager\Api\Server\ServerCollection'));
354 28
        $class->addImport(new Import('Puli\UrlGenerator\Api\UrlGenerator'));
355 28
        $class->addImport(new Import('Puli\UrlGenerator\DiscoveryUrlGenerator'));
356 28
        $class->addImport(new Import('RuntimeException'));
357
358 28
        $method = new Method('createUrlGenerator');
359 28
        $method->setDescription('Creates the URL generator.');
360
361 28
        $arg = new Argument('discovery');
362 28
        $arg->setTypeHint('Discovery');
363 28
        $arg->setType('Discovery');
364 28
        $arg->setDescription('The discovery to read from.');
365 28
        $method->addArgument($arg);
366
367 28
        $method->setReturnValue(new ReturnValue('$generator', 'UrlGenerator', 'The created URL generator.'));
368
369 28
        $method->addBody(
370
<<<EOF
371
if (!interface_exists('Puli\UrlGenerator\Api\UrlGenerator')) {
372
    throw new RuntimeException('Please install puli/url-generator to create UrlGenerator instances.');
373
}
374
375
EOF
376 28
        );
377
378 28
        $urlFormatsString = '';
379
380 28
        foreach ($this->servers as $server) {
381 20
            $urlFormatsString .= sprintf(
382 20
                "\n    %s => %s,",
383 20
                var_export($server->getName(), true),
384 20
                var_export($server->getUrlFormat(), true)
385 20
            );
386 28
        }
387
388 28
        if ($urlFormatsString) {
389 20
            $urlFormatsString .= "\n";
390 20
        }
391
392 28
        $method->addBody("\$generator = new DiscoveryUrlGenerator(\$discovery, array($urlFormatsString));");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $urlFormatsString instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
393
394 28
        $class->addMethod($method);
395 28
    }
396
397
    /**
398
     * Adds the getPackageOrder() method.
399
     *
400
     * @param Clazz $class The factory class model.
401
     */
402 28
    public function addGetPackageOrderMethod(Clazz $class)
403
    {
404 28
        $class->addImport(new Import('Puli\Discovery\Api\Discovery'));
405 28
        $class->addImport(new Import('Puli\Manager\Api\Server\ServerCollection'));
406 28
        $class->addImport(new Import('Puli\UrlGenerator\Api\UrlGenerator'));
407 28
        $class->addImport(new Import('Puli\UrlGenerator\DiscoveryUrlGenerator'));
408 28
        $class->addImport(new Import('RuntimeException'));
409
410 28
        $method = new Method('getPackageOrder');
411 28
        $method->setDescription("Returns the order in which the installed packages should be loaded\naccording to the override statements.");
412
413 28
        $method->setReturnValue(new ReturnValue('$order', 'string[]', 'The sorted package names.'));
414
415 28
        $packageOrderString = '';
416
417 28
        if (count($this->packages) > 0) {
418 28
            $overrideGraph = OverrideGraph::forPackages($this->packages);
419
420 28
            foreach ($overrideGraph->getSortedPackageNames() as $packageName) {
421 28
                $packageOrderString .= sprintf(
422 28
                    "\n    %s,",
423 28
                    var_export($packageName, true)
424 28
                );
425 28
            }
426
427 28
            $packageOrderString .= "\n";
428 28
        }
429
430 28
        $method->addBody("\$order = array($packageOrderString);");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $packageOrderString instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
431
432 28
        $class->addMethod($method);
433 28
    }
434
435
    /**
436
     * Recursively camelizes the keys of an array.
437
     *
438
     * @param array $array The array to process.
439
     *
440
     * @return array The input array with camelized keys.
441
     */
442 28
    private function camelizeKeys(array $array)
443
    {
444 28
        $camelized = array();
445
446 28
        foreach ($array as $key => $value) {
447 28
            $camelized[$this->camelize($key)] = is_array($value)
448 28
                ? $this->camelizeKeys($value)
449 28
                : $value;
450 28
        }
451
452 28
        return $camelized;
453
    }
454
455
    /**
456
     * Camelizes a string.
457
     *
458
     * @param string $string A string.
459
     *
460
     * @return string The camelized string.
461
     */
462
    private function camelize($string)
463
    {
464 28
        return preg_replace_callback('/\W+([a-z])/', function ($matches) {
465
            return strtoupper($matches[1]);
466 28
        }, $string);
467
    }
468
}
469