Skip to content

Commit be1080b

Browse files
committed
Added support for a cleanup callback.
1 parent 9b51230 commit be1080b

File tree

5 files changed

+280
-2
lines changed

5 files changed

+280
-2
lines changed

.customizer.php

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class CustomizerConfig {
1515
* Messages used by the command.
1616
*
1717
* @param CustomizeCommand $customizer
18-
* The command instance.
18+
* The Customizer instance.
1919
*
2020
* @return array<string,string|array<string>>
2121
* An associative array of messages with message name as key and the message
@@ -168,7 +168,7 @@ public static function questions(CustomizeCommand $customizer): array {
168168
* @param array<string,string> $answers
169169
* All answers received so far.
170170
* @param CustomizeCommand $customizer
171-
* The command instance.
171+
* The Customizer instance.
172172
*
173173
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
174174
*/
@@ -177,4 +177,19 @@ public static function processLicense(string $title, string $answer, array $answ
177177
$customizer->writeComposerJson($customizer->packageData);
178178
}
179179

180+
/**
181+
* A callback to process cleanup.
182+
*
183+
* @param array<string,mixed> $composerjson
184+
* The composer.json file content passed by reference.
185+
* @param \AlexSkrypnyk\Customizer\CustomizeCommand $customizer
186+
* The Customizer instance.
187+
*/
188+
public static function cleanup(array &$composerjson, CustomizeCommand $customizer): void {
189+
// Here you can remove any sections from the composer.json file that are not
190+
// needed for the project before all dependencies are updated.
191+
//
192+
// You can also additionally process files.
193+
}
194+
180195
}

CustomizeCommand.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,13 @@ class CustomizeCommand extends BaseCommand {
9797
*/
9898
protected array $messagesMap;
9999

100+
/**
101+
* Additional cleanup callback.
102+
*
103+
* @var callable|null
104+
*/
105+
protected mixed $cleanupCallback;
106+
100107
/**
101108
* {@inheritdoc}
102109
*/
@@ -227,6 +234,10 @@ protected function cleanup(): void {
227234
static::arrayUnsetDeep($json, ['require-dev', 'alexskrypnyk/customizer']);
228235
static::arrayUnsetDeep($json, ['autoload-dev', 'psr-4', 'AlexSkrypnyk\\Customizer\\Tests\\']);
229236

237+
if (!empty($this->cleanupCallback)) {
238+
call_user_func_array($this->cleanupCallback, [&$json, $this]);
239+
}
240+
230241
// If the package data has changed, update the composer.json file.
231242
if (strcmp(serialize($this->packageData), serialize($json)) !== 0) {
232243
$this->writeComposerJson($json);
@@ -312,6 +323,11 @@ protected function initConfig(): void {
312323
if (method_exists($config_class, 'questions')) {
313324
$questions = $config_class::questions($this);
314325
}
326+
if (method_exists($config_class, 'cleanup')) {
327+
$this->cleanupCallback = function (array &$composerjson) use ($config_class) {
328+
return $config_class::cleanup($composerjson, $this);
329+
};
330+
}
315331
}
316332

317333
// Validate messages structure.

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,30 @@ class CustomizerConfig {
240240
}
241241
```
242242

243+
### `cleanup()`
244+
245+
Using the `cleanup()` method, the template project authors can additionally
246+
process the `composer.json` file content before all dependencies are updated and
247+
process files. This runs after all answers are received and the user confirms
248+
the intended changes.
249+
250+
```php
251+
/**
252+
* A callback to process cleanup.
253+
*
254+
* @param array<string,mixed> $composerjson
255+
* The composer.json file content passed by reference.
256+
* @param \AlexSkrypnyk\Customizer\CustomizeCommand $customizer
257+
* The Customizer instance.
258+
*/
259+
public static function cleanup(array &$composerjson, CustomizeCommand $customizer): void {
260+
// Here you can remove any sections from the composer.json file that are not
261+
// needed for the project before all dependencies are updated.
262+
//
263+
// You can also additionally process files.
264+
}
265+
```
266+
243267
### `messages()`
244268

245269
Using the `messages()` method, the template project authors can overwrite
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use AlexSkrypnyk\Customizer\CustomizeCommand;
6+
7+
/**
8+
* Customizer configuration.
9+
*
10+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
11+
*/
12+
class CustomizerConfig {
13+
14+
/**
15+
* Messages used by the command.
16+
*
17+
* @param CustomizeCommand $customizer
18+
* The command instance.
19+
*
20+
* @return array<string,string|array<string>>
21+
* An associative array of messages with message name as key and the message
22+
* test as a string or an array of strings.
23+
*
24+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
25+
*/
26+
public static function messages(CustomizeCommand $customizer): array {
27+
return [
28+
// This is an example of a custom message that overrides the default
29+
// message with name `welcome`.
30+
'welcome' => 'Welcome to the {{ package.name }} project customizer',
31+
];
32+
}
33+
34+
/**
35+
* Question definitions.
36+
*
37+
* Define questions and their processing callbacks. Questions will be asked
38+
* in the order they are defined. Questions can use answers from previous
39+
* questions received so far.
40+
*
41+
* Answers will be processed in the order they are defined. Process callbacks
42+
* have access to all answers and Customizer's class public properties and
43+
* methods.
44+
*
45+
* If a question does not have a process callback, a static method prefixed
46+
* with `process` and a camel-cased question title will be called. If the
47+
* method does not exist, there will be no processing.
48+
*
49+
* @code
50+
* $questions['Machine name'] = [
51+
* // Question callback function.
52+
* 'question' => fn(array $answers) => $this->io->ask(
53+
* // Question text to show to the user.
54+
* 'What is your machine name',
55+
* // Default answer. Using `Str2Name` 3rd-party library to convert value.
56+
* Str2Name::machine(basename($this->cwd)),
57+
* // Answer validation function.
58+
* static fn(string $string): string => strtolower($string)
59+
* ),
60+
* // Process callback function.
61+
* 'process' => function (string $title, string $answer, array $answers): void {
62+
* // Remove a directory using 'fs' and `cwd` class properties.
63+
* $this->fs->removeDirectory($this->cwd . '/somedir');
64+
* // Replace a string in a file using `cwd` class property and
65+
* // `replaceInPath` method.
66+
* $this->replaceInPath($this->cwd . '/somefile', 'old', 'new');
67+
* // Replace a string in all files in a directory.
68+
* $this->replaceInPath($this->cwd . '/somedir', 'old', 'new');
69+
* },
70+
* ];
71+
* @endcode
72+
*
73+
* @param CustomizeCommand $customizer
74+
* The CustomizeCommand object. Can be used to access the command properties
75+
* and methods to prepare questions. Note that the questions callbacks
76+
* already receive the command object as an argument, so this argument is
77+
* used to prepare questions array itself.
78+
*
79+
* @return array<string,array<string,string|callable>>
80+
* An associative array of questions with question title as a key and the
81+
* value of array with the following keys:
82+
* - question: The question callback function used to ask the question.
83+
* The callback receives the following arguments:
84+
* - answers: An associative array of all answers received so far.
85+
* - command: The CustomizeCommand object.
86+
* - process: The callback function used to process the answer. Callback
87+
* can be an anonymous function or a method of this class as
88+
* process<PascalCasedQuestion>. The callback receives the following
89+
* arguments:
90+
* - title: The current question title.
91+
* - answer: The answer to the current question.
92+
* - answers: An associative array of all answers.
93+
* - command: The CustomizeCommand object.
94+
*
95+
* @SuppressWarnings(PHPMD.ExcessiveMethodLength)
96+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
97+
*/
98+
public static function questions(CustomizeCommand $customizer): array {
99+
// This an example of questions that can be asked to customize the project.
100+
// You can adjust this method to ask questions that are relevant to your
101+
// project.
102+
//
103+
// In this example, we ask for the package name, description, and license.
104+
//
105+
// You may remove all the questions below and replace them with your own.
106+
return [
107+
'Name' => [
108+
// The question callback function defines how the question is asked.
109+
// In this case, we ask the user to provide a package name as a string.
110+
'question' => static fn(array $answers, CustomizeCommand $customizer): mixed => $customizer->io->ask('Package name', NULL, static function (string $value): string {
111+
// This is a validation callback that checks if the package name is
112+
// valid. If not, an exception is thrown with a message shown to the
113+
// user.
114+
if (!preg_match('/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/', $value)) {
115+
throw new \InvalidArgumentException(sprintf('The package name "%s" is invalid, it should be lowercase and have a vendor name, a forward slash, and a package name.', $value));
116+
}
117+
118+
return $value;
119+
}),
120+
// The process callback function defines how the answer is processed.
121+
// The processing takes place only after all answers are received and
122+
// the user confirms the intended changes.
123+
'process' => static function (string $title, string $answer, array $answers, CustomizeCommand $customizer): void {
124+
$name = is_string($customizer->packageData['name'] ?? NULL) ? $customizer->packageData['name'] : '';
125+
// Update the package data.
126+
$customizer->packageData['name'] = $answer;
127+
// Write the updated composer.json file.
128+
$customizer->writeComposerJson($customizer->packageData);
129+
// Replace the package name in the project files.
130+
$customizer->replaceInPath($customizer->cwd, $name, $answer);
131+
},
132+
],
133+
'Description' => [
134+
// For this question, we are using an answer from the previous question
135+
// in the title of the question.
136+
'question' => static fn(array $answers, CustomizeCommand $customizer): mixed => $customizer->io->ask(sprintf('Description for %s', $answers['Name'])),
137+
'process' => static function (string $title, string $answer, array $answers, CustomizeCommand $customizer): void {
138+
$description = is_string($customizer->packageData['description'] ?? NULL) ? $customizer->packageData['description'] : '';
139+
$customizer->packageData['description'] = $answer;
140+
$customizer->writeComposerJson($customizer->packageData);
141+
$customizer->replaceInPath($customizer->cwd, $description, $answer);
142+
},
143+
],
144+
'License' => [
145+
// For this question, we are using a pre-defined list of options.
146+
// For processing, we are using a separate method named 'processLicense'
147+
// (only for the demonstration purposes; it could have been an
148+
// anonymous function).
149+
'question' => static fn(array $answers, CustomizeCommand $customizer): mixed => $customizer->io->choice('License type', [
150+
'MIT',
151+
'GPL-3.0-or-later',
152+
'Apache-2.0',
153+
], 'GPL-3.0-or-later'),
154+
],
155+
];
156+
}
157+
158+
/**
159+
* A callback to process the `License` question.
160+
*
161+
* This is an example callback, and it can be safely removed if this question
162+
* is not needed.
163+
*
164+
* @param string $title
165+
* The question title.
166+
* @param string $answer
167+
* The answer to the question.
168+
* @param array<string,string> $answers
169+
* All answers received so far.
170+
* @param CustomizeCommand $customizer
171+
* The command instance.
172+
*
173+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
174+
*/
175+
public static function processLicense(string $title, string $answer, array $answers, CustomizeCommand $customizer): void {
176+
$customizer->packageData['license'] = $answer;
177+
$customizer->writeComposerJson($customizer->packageData);
178+
}
179+
180+
/**
181+
* A callback to process cleanup.
182+
*/
183+
public static function cleanup(array &$composerjson, CustomizeCommand $customizer): void {
184+
unset($composerjson['config']['sort-packages']);
185+
}
186+
187+
}

tests/phpunit/Functional/CreateProjectPluginTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,42 @@ public function testPluginInstall(): void {
5656
$this->assertComposerLockUpToDate();
5757
}
5858

59+
#[RunInSeparateProcess]
60+
#[Group('install')]
61+
#[Group('plugin')]
62+
public function testPluginInstallAdditionalCleanup(): void {
63+
$this->dirs->fs->copy($this->dirs->fixtures . DIRECTORY_SEPARATOR . 'plugin' . DIRECTORY_SEPARATOR . CustomizeCommand::CONFIG_FILE, $this->dirs->repo . DIRECTORY_SEPARATOR . CustomizeCommand::CONFIG_FILE, TRUE);
64+
65+
$this->customizerSetAnswers([
66+
'testorg/testpackage',
67+
'Test description',
68+
'MIT',
69+
self::TUI_ANSWER_NOTHING,
70+
]);
71+
$this->composerCreateProject();
72+
73+
$this->assertComposerCommandSuccessOutputContains('Welcome to the yourorg/yourtempaltepackage project customizer');
74+
$this->assertComposerCommandSuccessOutputContains('Project was customized');
75+
76+
$this->assertFileExists('composer.json');
77+
$this->assertFileExists('composer.lock');
78+
$this->assertDirectoryExists('vendor');
79+
// Plugin will only clean up after itself if there were questions.
80+
$this->assertDirectoryDoesNotExist('vendor/alexskrypnyk/customizer');
81+
82+
$json = $this->composerJsonRead('composer.json');
83+
$this->assertEquals('testorg/testpackage', $json['name']);
84+
$this->assertEquals('Test description', $json['description']);
85+
$this->assertEquals('MIT', $json['license']);
86+
87+
$this->assertArrayNotHasKey('require-dev', $json);
88+
$this->assertArrayNotHasKey('AlexSkrypnyk\\Customizer\\Tests\\', $json['autoload-dev']['psr-4']);
89+
$this->assertArrayNotHasKey('config', $json);
90+
$this->assertFileDoesNotExist($this->customizerFile);
91+
92+
$this->assertComposerLockUpToDate();
93+
}
94+
5995
#[RunInSeparateProcess]
6096
#[Group('install')]
6197
#[Group('plugin')]

0 commit comments

Comments
 (0)