Failed Conditions
Pull Request — master (#41)
by Bernhard
12:50
created

FactoryManagerImpl::setPackages()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 1
Bugs 0 Features 1
Metric Value
c 1
b 0
f 1
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
crap 1
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 43
     *                                                 the generators for the
92
     *                                                 services returned by the
93 43
     *                                                 factory.
94 43
     * @param ClassWriter           $classWriter       The writer that writes
95 43
     *                                                 the class to a file.
96 43
     * @param PackageCollection     $packages          The loaded packages.
0 ignored issues
show
Documentation introduced by
Should the type for parameter $packages not be null|PackageCollection?

This check looks for @param annotations where the type inferred by our type inference engine differs from the declared type.

It makes a suggestion as to what type it considers more descriptive.

Most often this is a case of a parameter that can be null in addition to its declared types.

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