Failed Conditions
Pull Request — master (#41)
by Bernhard
16:02
created

FactoryManagerImpl::addGetPackageOrderMethod()   B

Complexity

Conditions 3
Paths 2

Size

Total Lines 33
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 33
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     $packages          The loaded packages.
97
     * @param ServerCollection|null $servers           The configured servers.
98
     */
99 29
    public function __construct(ProjectContext $context, GeneratorRegistry $generatorRegistry, ClassWriter $classWriter, PackageCollection $packages, ServerCollection $servers = null)
100
    {
101 29
        $this->context = $context;
102 29
        $this->config = $context->getConfig();
103 29
        $this->rootDir = $context->getRootDirectory();
104 29
        $this->generatorRegistry = $generatorRegistry;
105 29
        $this->classWriter = $classWriter;
106 29
        $this->packages = $packages;
107 29
        $this->servers = $servers;
108 29
    }
109
110
    /**
111
     * Sets the servers included in the createUrlGenerator() method.
112
     *
113
     * @param ServerCollection $servers The configured servers.
114
     */
115
    public function setServers(ServerCollection $servers)
116
    {
117
        $this->servers = $servers;
118
    }
119
120
    /**
121
     * {@inheritdoc}
122
     */
123 5
    public function createFactory($path = null, $className = null)
124
    {
125 5
        Assert::nullOrStringNotEmpty($path, 'The path to the generated factory file must be a non-empty string or null. Got: %s');
126 5
        Assert::nullOrStringNotEmpty($className, 'The class name of the generated factory must be a non-empty string or null. Got: %s');
127
128 5
        $this->refreshFactoryClass($path, $className);
129
130 5
        $className = $className ?: $this->config->get(Config::FACTORY_IN_CLASS);
131 5
        $path = $path ?: $this->config->get(Config::FACTORY_IN_FILE);
132
133 5
        if (null !== $path && !class_exists($className, false)) {
134 4
            require_once Path::makeAbsolute($path, $this->rootDir);
135 4
        }
136
137 5
        return new $className();
138
    }
139
140
    /**
141
     * {@inheritdoc}
142
     */
143 1
    public function isFactoryClassAutoGenerated()
144
    {
145 1
        return $this->config->get(Config::FACTORY_AUTO_GENERATE);
146
    }
147
148
    /**
149
     * {@inheritdoc}
150
     */
151 23
    public function generateFactoryClass($path = null, $className = null)
152
    {
153 23
        Assert::nullOrStringNotEmpty($path, 'The path to the generated factory file must be a non-empty string or null. Got: %s');
154 21
        Assert::nullOrStringNotEmpty($className, 'The class name of the generated factory must be a non-empty string or null. Got: %s');
155
156 19
        $path = Path::makeAbsolute($path ?: $this->config->get(Config::FACTORY_OUT_FILE), $this->rootDir);
157 19
        $className = $className ?: $this->config->get(Config::FACTORY_OUT_CLASS);
158 19
        $dispatcher = $this->context->getEventDispatcher();
159
160 19
        $class = new Clazz($className);
161 19
        $class->setFilePath($path);
162 19
        $class->setDescription(
163
<<<EOF
164
Creates Puli's core services.
165
166
This class was auto-generated by Puli.
167
168
IMPORTANT: Before modifying the code below, set the "factory.auto-generate"
169
configuration key to false:
170
171
    $ puli config factory.auto-generate false
172
173
Otherwise any modifications will be overwritten!
174
EOF
175 19
        );
176
177 19
        $this->addCreateRepositoryMethod($class);
178 19
        $this->addCreateDiscoveryMethod($class);
179 19
        $this->addCreateUrlGeneratorMethod($class);
180 19
        $this->addGetPackageOrderMethod($class);
181
182 19
        if ($dispatcher->hasListeners(PuliEvents::GENERATE_FACTORY)) {
183 1
            $dispatcher->dispatch(PuliEvents::GENERATE_FACTORY, new GenerateFactoryEvent($class));
184 1
        }
185
186 19
        $this->classWriter->writeClass($class);
187 19
    }
188
189
    /**
190
     * {@inheritdoc}
191
     */
192 3
    public function autoGenerateFactoryClass($path = null, $className = null)
193
    {
194 3
        if (!$this->config->get(Config::FACTORY_AUTO_GENERATE)) {
195 1
            return;
196
        }
197
198 2
        $this->generateFactoryClass($path, $className);
199 2
    }
200
201
    /**
202
     * {@inheritdoc}
203
     */
204 16
    public function refreshFactoryClass($path = null, $className = null)
205
    {
206 16
        Assert::nullOrStringNotEmpty($path, 'The path to the generated factory file must be a non-empty string or null. Got: %s');
207 16
        Assert::nullOrStringNotEmpty($className, 'The class name of the generated factory must be a non-empty string or null. Got: %s');
208
209 16
        $path = Path::makeAbsolute($path ?: $this->config->get(Config::FACTORY_OUT_FILE), $this->rootDir);
210 16
        $className = $className ?: $this->config->get(Config::FACTORY_OUT_CLASS);
211
212 16
        if (!$this->config->get(Config::FACTORY_AUTO_GENERATE)) {
213 1
            return;
214
        }
215
216 15
        if (!file_exists($path)) {
217 9
            $this->generateFactoryClass($path, $className);
218
219 9
            return;
220
        }
221
222 6
        $rootPackageFile = $this->context->getRootPackageFile()->getPath();
223
224 6
        if (!file_exists($rootPackageFile)) {
225 1
            return;
226
        }
227
228
        // Regenerate file if the configuration has changed and
229
        // auto-generation is enabled
230 5
        clearstatcache(true, $rootPackageFile);
231 5
        $lastConfigChange = filemtime($rootPackageFile);
232
233 5
        $configFile = $this->context->getConfigFile()
234 5
            ? $this->context->getConfigFile()->getPath()
235 5
            : '';
236
237 5
        if (file_exists($configFile)) {
238 2
            clearstatcache(true, $configFile);
239 2
            $lastConfigChange = max(filemtime($configFile), $lastConfigChange);
240 2
        }
241
242 5
        clearstatcache(true, $path);
243 5
        $lastFactoryUpdate = filemtime($path);
244
245 5
        if ($lastConfigChange > $lastFactoryUpdate) {
246 3
            $this->generateFactoryClass($path, $className);
247 3
        }
248 5
    }
249
250
    /**
251
     * Adds the createRepository() method.
252
     *
253
     * @param Clazz $class The factory class model.
254
     */
255 19
    private function addCreateRepositoryMethod(Clazz $class)
256
    {
257 19
        $method = new Method('createRepository');
258 19
        $method->setDescription('Creates the resource repository.');
259 19
        $method->setReturnValue(new ReturnValue(
260 19
            '$'.self::REPO_VAR_NAME,
261 19
            'ResourceRepository',
262
            'The created resource repository.'
263 19
        ));
264 19
        $method->addBody(
265
<<<EOF
266
if (!interface_exists('Puli\Repository\Api\ResourceRepository')) {
267
    throw new RuntimeException('Please install puli/repository to create ResourceRepository instances.');
268
}
269
270
EOF
271 19
        );
272
273 19
        $class->addImport(new Import('Puli\Repository\Api\ResourceRepository'));
274 19
        $class->addImport(new Import('RuntimeException'));
275 19
        $class->addMethod($method);
276
277
        // Add method body
278 19
        $config = $this->config;
279 19
        $type = $config->get(Config::REPOSITORY_TYPE);
280 19
        $options = $this->camelizeKeys($config->get(Config::REPOSITORY));
281 19
        $options['rootDir'] = $this->rootDir;
282
283 19
        $generator = $this->generatorRegistry->getServiceGenerator(GeneratorRegistry::REPOSITORY, $type);
284 19
        $generator->generateNewInstance(self::REPO_VAR_NAME, $method, $this->generatorRegistry, $options);
285 19
    }
286
287
    /**
288
     * Adds the createDiscovery() method.
289
     *
290
     * @param Clazz $class The factory class model.
291
     */
292 19
    private function addCreateDiscoveryMethod(Clazz $class)
293
    {
294 19
        $method = new Method('createDiscovery');
295 19
        $method->setDescription('Creates the resource discovery.');
296
297 19
        $arg = new Argument(self::REPO_VAR_NAME);
298 19
        $arg->setTypeHint('ResourceRepository');
299 19
        $arg->setType('ResourceRepository');
300 19
        $arg->setDescription('The resource repository to read from.');
301
302 19
        $method->addArgument($arg);
303
304 19
        $method->setReturnValue(new ReturnValue(
305 19
            '$'.self::DISCOVERY_VAR_NAME,
306 19
            'Discovery',
307
            'The created discovery.'
308 19
        ));
309
310 19
        $method->addBody(
311
<<<EOF
312
if (!interface_exists('Puli\Discovery\Api\Discovery')) {
313
    throw new RuntimeException('Please install puli/discovery to create Discovery instances.');
314
}
315
316
EOF
317 19
        );
318
319 19
        $class->addImport(new Import('Puli\Repository\Api\ResourceRepository'));
320 19
        $class->addImport(new Import('Puli\Discovery\Api\Discovery'));
321 19
        $class->addImport(new Import('RuntimeException'));
322 19
        $class->addMethod($method);
323
324
        // Add method body
325 19
        $config = $this->config;
326 19
        $type = $config->get(Config::DISCOVERY_TYPE);
327 19
        $options = $this->camelizeKeys($config->get(Config::DISCOVERY));
328 19
        $options['rootDir'] = $this->rootDir;
329
330 19
        $generator = $this->generatorRegistry->getServiceGenerator(GeneratorRegistry::DISCOVERY, $type);
331 19
        $generator->generateNewInstance(self::DISCOVERY_VAR_NAME, $method, $this->generatorRegistry, $options);
332 19
    }
333
334
    /**
335
     * Adds the createUrlGenerator() method.
336
     *
337
     * @param Clazz $class The factory class model.
338
     */
339 19
    public function addCreateUrlGeneratorMethod(Clazz $class)
340
    {
341 19
        $class->addImport(new Import('Puli\Discovery\Api\Discovery'));
342 19
        $class->addImport(new Import('Puli\Manager\Api\Server\ServerCollection'));
343 19
        $class->addImport(new Import('Puli\UrlGenerator\Api\UrlGenerator'));
344 19
        $class->addImport(new Import('Puli\UrlGenerator\DiscoveryUrlGenerator'));
345 19
        $class->addImport(new Import('RuntimeException'));
346
347 19
        $method = new Method('createUrlGenerator');
348 19
        $method->setDescription('Creates the URL generator.');
349
350 19
        $arg = new Argument('discovery');
351 19
        $arg->setTypeHint('Discovery');
352 19
        $arg->setType('Discovery');
353 19
        $arg->setDescription('The discovery to read from.');
354 19
        $method->addArgument($arg);
355
356 19
        $method->setReturnValue(new ReturnValue('$generator', 'UrlGenerator', 'The created URL generator.'));
357
358 19
        $method->addBody(
359
<<<EOF
360
if (!interface_exists('Puli\UrlGenerator\Api\UrlGenerator')) {
361
    throw new RuntimeException('Please install puli/url-generator to create UrlGenerator instances.');
362
}
363
364
EOF
365 19
        );
366
367 19
        $urlFormatsString = '';
368
369 19
        foreach ($this->servers as $server) {
370 19
            $urlFormatsString .= sprintf(
371 19
                "\n    %s => %s,",
372 19
                var_export($server->getName(), true),
373 19
                var_export($server->getUrlFormat(), true)
374 19
            );
375 19
        }
376
377 19
        if ($urlFormatsString) {
378 19
            $urlFormatsString .= "\n";
379 19
        }
380
381 19
        $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...
382
383 19
        $class->addMethod($method);
384 19
    }
385
386
    /**
387
     * Adds the getPackageOrder() method.
388
     *
389
     * @param Clazz $class The factory class model.
390
     */
391 19
    public function addGetPackageOrderMethod(Clazz $class)
392
    {
393 19
        $class->addImport(new Import('Puli\Discovery\Api\Discovery'));
394 19
        $class->addImport(new Import('Puli\Manager\Api\Server\ServerCollection'));
395 19
        $class->addImport(new Import('Puli\UrlGenerator\Api\UrlGenerator'));
396 19
        $class->addImport(new Import('Puli\UrlGenerator\DiscoveryUrlGenerator'));
397 19
        $class->addImport(new Import('RuntimeException'));
398
399 19
        $method = new Method('getPackageOrder');
400 19
        $method->setDescription("Returns the order in which the installed packages should be loaded\naccording to the override statements.");
401
402 19
        $method->setReturnValue(new ReturnValue('$order', 'string[]', 'The sorted package names.'));
403
404 19
        $packageOrderString = '';
405
406 19
        if (count($this->packages) > 0) {
407 19
            $overrideGraph = OverrideGraph::forPackages($this->packages);
408
409 19
            foreach ($overrideGraph->getSortedPackageNames() as $packageName) {
410 19
                $packageOrderString .= sprintf(
411 19
                    "\n    %s,",
412 19
                    var_export($packageName, true)
413 19
                );
414 19
            }
415
416 19
            $packageOrderString .= "\n";
417 19
        }
418
419
420 19
        $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...
421
422 19
        $class->addMethod($method);
423 19
    }
424
425
    /**
426
     * Recursively camelizes the keys of an array.
427
     *
428
     * @param array $array The array to process.
429
     *
430
     * @return array The input array with camelized keys.
431
     */
432 19
    private function camelizeKeys(array $array)
433
    {
434 19
        $camelized = array();
435
436 19
        foreach ($array as $key => $value) {
437 19
            $camelized[$this->camelize($key)] = is_array($value)
438 19
                ? $this->camelizeKeys($value)
439 19
                : $value;
440 19
        }
441
442 19
        return $camelized;
443
    }
444
445
    /**
446
     * Camelizes a string.
447
     *
448
     * @param string $string A string.
449
     *
450
     * @return string The camelized string.
451
     */
452
    private function camelize($string)
453
    {
454 19
        return preg_replace_callback('/\W+([a-z])/', function ($matches) {
455
            return strtoupper($matches[1]);
456 19
        }, $string);
457
    }
458
}
459