• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

CPS-IT / project-builder / 26434836865

26 May 2026 05:49AM UTC coverage: 98.204% (+1.7%) from 96.481%
26434836865

push

github

eliashaeussler
[TASK] Drop `3.x` branch references

2515 of 2561 relevant lines covered (98.2%)

14.87 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.18
/src/Template/Provider/BaseProvider.php
1
<?php
2

3
declare(strict_types=1);
4

5
/*
6
 * This file is part of the Composer package "cpsit/project-builder".
7
 *
8
 * Copyright (C) 2022 Elias Häußler <e.haeussler@familie-redlich.de>
9
 *
10
 * This program is free software: you can redistribute it and/or modify
11
 * it under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, either version 3 of the License, or
13
 * (at your option) any later version.
14
 *
15
 * This program is distributed in the hope that it will be useful,
16
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
17
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18
 * GNU General Public License for more details.
19
 *
20
 * You should have received a copy of the GNU General Public License
21
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22
 */
23

24
namespace CPSIT\ProjectBuilder\Template\Provider;
25

26
use Composer\Factory;
27
use Composer\IO as ComposerIO;
28
use Composer\Package;
29
use Composer\Repository;
30
use Composer\Semver;
31
use Composer\Util;
32
use CPSIT\ProjectBuilder\Exception;
33
use CPSIT\ProjectBuilder\Helper;
34
use CPSIT\ProjectBuilder\IO;
35
use CPSIT\ProjectBuilder\Paths;
36
use CPSIT\ProjectBuilder\Resource;
37
use CPSIT\ProjectBuilder\Template;
38
use Symfony\Component\Console;
39
use Symfony\Component\Filesystem;
40
use Twig\Environment;
41
use Twig\Loader;
42
use UnexpectedValueException;
43

44
use function getenv;
45
use function sprintf;
46

47
/**
48
 * BaseProvider.
49
 *
50
 * @author Elias Häußler <e.haeussler@familie-redlich.de>
51
 * @license GPL-3.0-or-later
52
 */
53
abstract class BaseProvider implements ProviderInterface
54
{
55
    protected Resource\Local\Composer $composer;
56
    protected Environment $renderer;
57
    protected ComposerIO\IOInterface $io;
58
    protected Package\Version\VersionParser $versionParser;
59
    protected bool $acceptInsecureConnections = false;
60
    protected bool $disableCache = false;
61

62
    public function __construct(
47✔
63
        protected IO\Messenger $messenger,
64
        protected Filesystem\Filesystem $filesystem,
65
    ) {
66
        $this->composer = new Resource\Local\Composer($this->filesystem);
47✔
67
        $this->renderer = new Environment(
47✔
68
            new Loader\FilesystemLoader([
47✔
69
                Filesystem\Path::join(
47✔
70
                    Helper\FilesystemHelper::getPackageDirectory(),
47✔
71
                    Paths::PROJECT_INSTALLER,
47✔
72
                ),
47✔
73
            ]),
47✔
74
        );
47✔
75
        $this->io = new ComposerIO\BufferIO();
47✔
76
        $this->versionParser = new Package\Version\VersionParser();
47✔
77
    }
78

79
    public function listTemplateSources(): array
11✔
80
    {
81
        $maintainedPackageTemplateSources = [];
11✔
82
        $abandonedPackageTemplateSources = [];
11✔
83

84
        $repository = $this->createRepository();
11✔
85

86
        $constraint = new Semver\Constraint\MatchAllConstraint();
11✔
87
        $searchResult = $repository->search(
11✔
88
            '',
11✔
89
            Repository\RepositoryInterface::SEARCH_FULLTEXT,
11✔
90
            self::PACKAGE_TYPE,
11✔
91
        );
11✔
92

93
        foreach ($searchResult as ['name' => $packageName]) {
11✔
94
            $package = $repository->findPackage($packageName, $constraint);
7✔
95

96
            if (null !== $package && $this->isPackageSupported($package)) {
7✔
97
                if (
98
                    $package instanceof Package\CompletePackageInterface
7✔
99
                    && $package->isAbandoned()
7✔
100
                ) {
101
                    $abandonedPackageTemplateSources[] = $this->createTemplateSource($package);
1✔
102
                    continue;
1✔
103
                }
104

105
                $maintainedPackageTemplateSources[] = $this->createTemplateSource($package);
7✔
106
            }
107
        }
108

109
        return array_merge($maintainedPackageTemplateSources, $abandonedPackageTemplateSources);
11✔
110
    }
111

112
    /**
113
     * @throws Exception\IOException
114
     * @throws Exception\InvalidTemplateSourceException
115
     * @throws Exception\MisconfiguredValidatorException
116
     */
117
    public function installTemplateSource(Template\TemplateSource $templateSource): void
9✔
118
    {
119
        $package = $templateSource->getPackage();
9✔
120

121
        // @codeCoverageIgnoreStart
122
        if ($package instanceof Package\AliasPackage) {
123
            $package = $package->getAliasOf();
124
            $templateSource->setPackage($package);
125
        }
126
        // @codeCoverageIgnoreEnd
127

128
        if ($package instanceof Package\Package) {
9✔
129
            $this->requestPackageVersionConstraint($templateSource);
9✔
130
        }
131

132
        $composerJson = $this->createComposerJson([$templateSource]);
8✔
133
        $output = new Console\Output\BufferedOutput();
8✔
134

135
        $this->messenger->progress(
8✔
136
            sprintf(
8✔
137
                'Installing project template%s...',
8✔
138
                $templateSource->shouldUseDynamicVersionConstraint()
8✔
139
                    ? ''
6✔
140
                    : sprintf(' (<info>%s</info>)', $templateSource->getPackage()->getPrettyVersion()),
8✔
141
            ),
8✔
142
            ComposerIO\IOInterface::NORMAL,
8✔
143
        );
8✔
144

145
        $exitCode = $this->composer->install($composerJson, false, $output);
8✔
146

147
        if (0 !== $exitCode) {
8✔
148
            $this->messenger->failed();
3✔
149
            $this->messenger->write($output->fetch());
3✔
150

151
            throw Exception\InvalidTemplateSourceException::forFailedInstallation($templateSource);
3✔
152
        }
153

154
        // Make sure installed sources are handled by Composer's class loader
155
        $loader = Resource\Local\Composer::createClassLoader(dirname($composerJson));
6✔
156
        $loader->register(true);
6✔
157

158
        // Look up installed package
159
        $composer = Resource\Local\Composer::createComposer(dirname($composerJson));
6✔
160
        $repository = $composer->getRepositoryManager()->getLocalRepository();
6✔
161
        $installedPackage = $repository->findPackage($package->getName(), new Semver\Constraint\MatchAllConstraint());
6✔
162

163
        // Overwrite package from template source with actually installed template
164
        if (null !== $installedPackage) {
6✔
165
            $templateSource->setPackage($installedPackage);
6✔
166
        }
167

168
        // Show installed template version
169
        $this->messenger->progress(
6✔
170
            sprintf(
6✔
171
                'Installing project template (<info>%s</info>)...',
6✔
172
                $templateSource->getPackage()->getPrettyVersion(),
6✔
173
            ),
6✔
174
            ComposerIO\IOInterface::NORMAL,
6✔
175
            true,
6✔
176
        );
6✔
177
        $this->messenger->done();
6✔
178
        $this->messenger->newLine();
6✔
179
    }
180

181
    /**
182
     * @throws Exception\IOException
183
     * @throws Exception\InvalidTemplateSourceException
184
     * @throws Exception\MisconfiguredValidatorException
185
     */
186
    protected function requestPackageVersionConstraint(Template\TemplateSource $templateSource): void
9✔
187
    {
188
        $inputReader = $this->messenger->createInputReader();
9✔
189
        $repository = $templateSource->getPackage()->getRepository() ?? $this->createRepository();
9✔
190

191
        $this->messenger->writeWithEmoji(
9✔
192
            IO\Emoji::WhiteHeavyCheckMark->value,
9✔
193
            sprintf('Well done! You\'ve selected <comment>%s</comment>.', $templateSource->getPackage()->getName()),
9✔
194
        );
9✔
195

196
        $this->messenger->newLine();
9✔
197
        $this->messenger->write(
9✔
198
            sprintf('Do you require a specific version of <comment>%s</comment>?', $templateSource->getPackage()->getName()),
9✔
199
        );
9✔
200
        $this->messenger->comment(
9✔
201
            'If so, you may specify it here. Leave it empty and we\'ll find a current version for you.',
9✔
202
        );
9✔
203
        $this->messenger->newLine();
9✔
204
        $this->messenger->comment('Example: <fg=cyan>2.1.0</> or <fg=cyan>dev-feature/xyz</>');
9✔
205
        $this->messenger->newLine();
9✔
206

207
        $constraint = $inputReader->staticValue(
9✔
208
            'Enter the version constraint to require: ',
9✔
209
            validator: new IO\Validator\CallbackValidator([
9✔
210
                'callback' => $this->validateConstraint(...),
9✔
211
            ]),
9✔
212
        );
9✔
213

214
        $this->messenger->newLine();
9✔
215

216
        if (null === $constraint) {
9✔
217
            $templateSource->useDynamicVersionConstraint();
6✔
218

219
            return;
6✔
220
        }
221

222
        $package = $repository->findPackage($templateSource->getPackage()->getName(), $constraint);
4✔
223

224
        if ($package instanceof Package\BasePackage) {
4✔
225
            $templateSource->setPackage($package);
2✔
226

227
            return;
2✔
228
        }
229

230
        $this->messenger->error('Unable to find a package version for the given constraint.');
2✔
231

232
        if (!$inputReader->ask('Do you want to try another version constraint instead?')) {
2✔
233
            throw Exception\InvalidTemplateSourceException::forInvalidPackageVersionConstraint($templateSource, $constraint);
1✔
234
        }
235

236
        $this->messenger->newLine();
1✔
237

238
        $this->requestPackageVersionConstraint($templateSource);
1✔
239
    }
240

241
    protected function isPackageSupported(Package\BasePackage $package): bool
3✔
242
    {
243
        if (self::PACKAGE_TYPE !== $package->getType()) {
3✔
244
            return false;
×
245
        }
246

247
        /** @var array<string, mixed> $extra */
248
        $extra = $package->getExtra();
3✔
249
        $excludeFromListing = (bool) Helper\ArrayHelper::getValueByPath(
3✔
250
            $extra,
3✔
251
            'cpsit/project-builder.exclude-from-listing',
3✔
252
        );
3✔
253

254
        return !$excludeFromListing;
3✔
255
    }
256

257
    protected function createTemplateSource(Package\BasePackage $package): Template\TemplateSource
7✔
258
    {
259
        return new Template\TemplateSource($this, $package);
7✔
260
    }
261

262
    /**
263
     * @param list<Template\TemplateSource>          $templateSources
264
     * @param list<array{type: string, url: string}> $repositories
265
     */
266
    protected function createComposerJson(array $templateSources, array $repositories = []): string
8✔
267
    {
268
        $repositories = [
8✔
269
            [
8✔
270
                'type' => $this->getRepositoryType(),
8✔
271
                'url' => $this->getUrl(),
8✔
272
            ],
8✔
273
            ...$repositories,
8✔
274
        ];
8✔
275

276
        $targetDirectory = Helper\FilesystemHelper::getNewTemporaryDirectory();
8✔
277
        $targetFile = Filesystem\Path::join($targetDirectory, 'composer.json');
8✔
278
        $composerJson = $this->renderer->render('composer.json.twig', [
8✔
279
            'templateSources' => $templateSources,
8✔
280
            'rootDir' => Helper\FilesystemHelper::getPackageDirectory(),
8✔
281
            'tempDir' => $targetDirectory,
8✔
282
            'repositories' => $repositories,
8✔
283
            'acceptInsecureConnections' => $this->acceptInsecureConnections,
8✔
284
            'simulatedRootPackageVersion' => getenv('PROJECT_BUILDER_SIMULATE_VERSION'),
8✔
285
        ]);
8✔
286

287
        $this->filesystem->dumpFile($targetFile, $composerJson);
8✔
288

289
        return $targetFile;
8✔
290
    }
291

292
    protected function createRepository(): Repository\RepositoryInterface
8✔
293
    {
294
        $customConfiguration = [
8✔
295
            'config' => [
8✔
296
                'secure-http' => !$this->acceptInsecureConnections,
8✔
297
            ],
8✔
298
        ];
8✔
299

300
        if ($this->disableCache) {
8✔
301
            $customConfiguration['config']['cache-dir'] = Util\Platform::isWindows() ? 'nul' : '/dev/null';
1✔
302
        }
303

304
        $config = Factory::createConfig($this->io);
8✔
305
        $config->merge($customConfiguration);
8✔
306

307
        return Repository\RepositoryFactory::createRepo(
8✔
308
            $this->io,
8✔
309
            $config,
8✔
310
            [
8✔
311
                'type' => $this->getRepositoryType(),
8✔
312
                'url' => $this->getUrl(),
8✔
313
            ],
8✔
314
            Repository\RepositoryFactory::manager($this->io, $config, Factory::createHttpDownloader($this->io, $config)),
8✔
315
        );
8✔
316
    }
317

318
    /**
319
     * @throws Exception\ValidationException
320
     *
321
     * @internal
322
     */
323
    public function validateConstraint(?string $input): ?string
8✔
324
    {
325
        if (null === $input) {
8✔
326
            return null;
5✔
327
        }
328

329
        try {
330
            $this->versionParser->parseConstraints($input);
5✔
331
        } catch (UnexpectedValueException $exception) {
1✔
332
            throw Exception\ValidationException::create($exception->getMessage());
1✔
333
        }
334

335
        return $input;
4✔
336
    }
337

338
    public function disableCache(): void
2✔
339
    {
340
        $this->disableCache = true;
2✔
341
    }
342

343
    public function enableCache(): void
×
344
    {
345
        $this->disableCache = false;
×
346
    }
347

348
    /**
349
     * Get supported Composer repository type for the configured URL.
350
     *
351
     * @see https://getcomposer.org/doc/05-repositories.md#types
352
     */
353
    abstract protected function getRepositoryType(): string;
354
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc