alexskrypnyk / customizer
Interactive customization for template projects
Fund package maintenance!
alexskrypnyk
Patreon
Installs: 92
Dependents: 1
Suggesters: 0
Security: 0
Stars: 1
Watchers: 1
Forks: 0
Type:composer-plugin
Requires
- php: >=8.2
- composer-plugin-api: ^2.0
Requires (Dev)
- composer/composer: ^2.7
- dealerdirect/phpcodesniffer-composer-installer: ^1.0
- drupal/coder: ^8.3
- ergebnis/composer-normalize: ^2.42
- phpcompatibility/php-compatibility: ^9.3
- phpmd/phpmd: ^2.15
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^11.1
- rector/rector: ^1.0
This package is auto-updated.
Last update: 2024-06-25 23:44:47 UTC
README
Interactive customization for template projects
The Customizer allows template project authors to ask users questions during
the composer create-project
command and then update the code base based on
their answers.
TL;DR
Run the command below to create a new project from the template project example and see the Customizer in action:
composer create-project alexskrypnyk/template-project-example my-project
Features
- Can be included as a dependency
- Can be used without dependencies to support
composer create-project --no-install
- Questions and processing logic are defined in a standalone file
- Provides a set of helpers for processing answers
- Provides a test harness that can be used in the template project to test questions and processing logic
Installation
Customizer can be installed into the template project in two ways:
- As a Composer dependency: easier to manage, but does not work with
composer create-project --no-install
- As a standalone class: harder to manage, but works with
composer create-project --no-install
As a Composer dependency
When creating projects from other template projects, users typically
use composer create-project
(without the --no-install
), which installs all
required dependencies. This means that Customizer can be used as a dependency,
allowing template project authors to focus on the questions and processing
logic without managing the Customizer's code itself.
- Add the following to your
composer.json
file ( see this example):
"require-dev": { "alexskrypnyk/customizer": "^1.0" }, "config": { "allow-plugins": { "alexskrypnyk/customizer": true } }
- Create
customize.php
file with questions and processing logic relevant to your template project and place it in anywhere in your project.
These entries will be removed by the Customizer after your project's users run
the composer create-project
command.
See the Configuration section below for more information.
As a standalone class
There may be cases where template project authors want to ensure customization takes place even if the user doesn't install dependencies. In this situation, the Customizer class needs to be stored within the template project so that Composer can access the code without installing dependencies.
The Customizer provides a single file that can be copied to your project and only relies on Composer.
- Copy the
CustomizeCommand.php
file to the root,src
or any other directory of your project. - Adjust the namespace within the class.
- Add the following to your
composer.json
file ( see this example):
"autoload": { "classmap": [ "src/CustomizeCommand.php" ] }, "scripts": { "customize": [ "YourNamespace\\Customizer\\CustomizeCommand" ], "post-create-project-cmd": [ "@customize" ] }
Make sure to adjust the path in the classmap
and update
YourNamespace\\Customizer\\CustomizeCommand
with the correct namespace.
These entries will be removed by the Customizer after your project's users run
the composer create-project
command.
- Create
customize.php
file with questions and processing logic relevant to your template project and place it in anywhere in your project.
See the Configuration section below for more information.
Usage example
After the installation into the template project, the Customizer will be
triggered automatically after a user runs the composer create-project
command.
It will ask the user a series of questions, and will process the answers to customize their instance of the template project.
Run the command below to create a new project from the template project example and see the Customizer in action:
composer create-project alexskrypnyk/template-project-example my-project
The demonstration questions provided in the customize.php
file will ask you to provide a package name, description, and license.
The answers are then processed by updating the composer.json
file and
replacing the package name in other project files.
Configuration
The template project authors can configure the Customizer, including defining
questions and processing logic, by providing a an arbitrary class (with any
namespace) in a customize.php
file.
The class has to implement public static
methods to perform the configuration.
questions()
Define questions and their processing callbacks. Questions will be asked in the order they are defined. Questions can use answers from previous questions received so far.
Answers will be processed in the order they are defined. Process callbacks have access to all answers and Customizer's class public properties and methods.
If a question does not have a processAnswers()
callback explicitly specified, a static
method prefixed with processAnswers
and a camel-cased question title will be called.
If the method does not exist, there will be no processing.
customize.php
has an example of the questions()
method.
<?php declare(strict_types=1); use AlexSkrypnyk\Customizer\CustomizeCommand; public static function questions(CustomizeCommand $c): array { // This an example of questions that can be asked to customize the project. // You can adjust this method to ask questions that are relevant to your // project. // // In this example, we ask for the package name, description, and license. // // You may remove all the questions below and replace them with your own. return [ 'Name' => [ // The question callback function defines how the question is asked. // In this case, we ask the user to provide a package name as a string. 'question' => static fn(array $answers, CustomizeCommand $c): mixed => $c->io->ask('Package name', NULL, static function (string $value): string { // This is a validation callback that checks if the package name is // valid. If not, an exception is thrown with a message shown to the // user. if (!preg_match('/^[a-z0-9_.-]+\/[a-z0-9_.-]+$/', $value)) { 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)); } return $value; }), // The `processAnswers()` callback function defines how the answer is processed. // The processing takes place only after all answers are received and // the user confirms the intended changes. 'processAnswers' => static function (string $title, string $answer, array $answers, CustomizeCommand $c): void { $name = is_string($c->composerjsonData['name'] ?? NULL) ? $c->composerjsonData['name'] : ''; // Update the package data. $c->composerjsonData['name'] = $answer; // Write the updated composer.json file. CustomizeCommand::writeComposerJson($c->cwd . '/composer.json', $c->composerjsonData); // Replace the package name in the project files. $c->replaceInPath($c->cwd, $name, $answer); }, ], 'Description' => [ // For this question, we are using an answer from the previous question // in the title of the question. 'question' => static fn(array $answers, CustomizeCommand $c): mixed => $c->io->ask(sprintf('Description for %s', $answers['Name'])), 'processAnswers' => static function (string $title, string $answer, array $answers, CustomizeCommand $c): void { $description = is_string($c->composerjsonData['description'] ?? NULL) ? $c->composerjsonData['description'] : ''; $c->composerjsonData['description'] = $answer; CustomizeCommand::writeComposerJson($c->cwd . '/composer.json', $c->composerjsonData); $c->replaceInPath($c->cwd, $description, $answer); }, ], 'License' => [ // For this question, we are using a pre-defined list of options. // For processing, we are using a separate method named 'processLicense' // (only for the demonstration purposes; it could have been an // anonymous function). 'question' => static fn(array $answers, CustomizeCommand $c): mixed => $c->io->choice('License type', [ 'MIT', 'GPL-3.0-or-later', 'Apache-2.0', ], 'GPL-3.0-or-later'), ], ]; } public static function processLicense(string $title, string $answer, array $answers, CustomizeCommand $c): void { $c->composerjsonData['license'] = $answer; CustomizeCommand::writeComposerJson($c->cwd . '/composer.json', $c->composerjsonData); } }
cleanupSelf()
Using the cleanupSelf()
method, the template project authors can additionally
process the composer.json
file content before all dependencies are updated.
This runs after all answers are received and the user confirms
the intended changes.
Use $composerjson = [];
to prevent dependencies updates by the Customizer.
This essentially means that you are managing that process outside of this
method.
/** * A callback to process cleanup. * * @param array<string,mixed> $composerjson * The composer.json file content passed by reference. * @param \AlexSkrypnyk\Customizer\CustomizeCommand $c * The Customizer instance. */ public static function cleanupSelf(array &$composerjson, CustomizeCommand $c): void { // Here you can remove any sections from the composer.json file that are not // needed for the project before all dependencies are updated. // // You can also additionally process files. }
messages()
Using the messages()
method, the template project authors can overwrite
messages provided by the Customizer.
public static function messages(CustomizeCommand $c): array { return [ // This is an example of a custom message that overrides the default // message with name `welcome`. 'title' => 'Welcome to the {{ package.name }} project customizer', ]; }
Advanced configuration
In case when a template repository authors want to make the Customizer to be
truly drop-in single-file solution (installation option 2
without customize.php
file), they can define the questions and processing
logic in the CustomizeCommand.php
file itself. In this case, customize.php
will not be required (but is still supported).
Note that if the customize.php
file is present in the project, the questions
defined in the CustomizeCommand.php
file will be ignored in favour of the
questions provided in the customize.php
file.
Helpers
The Customizer provides a few helpers to make processing answers easier.
These are available as properties and methods of the $c
instance
passed to the processing callbacks:
cwd
- current working directory.fs
- SymfonyFilesystem
instance.io
- Symfony input/output instance.isComposerDependenciesInstalled
- whether the Composer dependencies were installed before the Customizer started.readComposerJson()
- Read the contents of thecomposer.json
file into an array.writeComposerJson()
- Write the contents of the array to thecomposer.json
file.replaceInPath()
- Replace a string in a file or all files in a directory.replaceInPathBetween()
- Replace a string in a file or all files in a directory between two markers.uncommentLine()
- Uncomment a line in a file or all files in a directory.arrayUnsetDeep()
- Unset a fully or partially matched value in a nested array, removing empty arrays.
Question validation helpers are not provided in this class, but you can easily create them using custom regular expression or add them from the AlexSkrypnyk/str2name package.
Developing and testing your questions
Testing manually
- Install the Customizer into your template project as described in the Installation section.
- Create a new testing directory and change into it.
- Create a project in this directory:
composer create-project yournamespace/yourscaffold="@dev" --repository '{"type": "path", "url": "/path/to/yourscaffold", "options": {"symlink": false}}' .
- The Customizer screen should appear.
Repeat the process as many times as needed to test your questions and processing logic.
Add export COMPOSER_ALLOW_XDEBUG=1
before running the composer create-project
command to enable debugging with XDebug.
Automated functional tests
This project uses automated functional tests to
check that composer create-project
asks the questions and processes the
answers correctly.
You can setup PHPUnit in your template project to run these tests. Once done,
use CustomizerTestCase.php
as a base class for your tests. See this example within the
template project example.
Maintenance
composer install
composer lint
composer test
This repository was created using the getscaffold.dev project scaffold template