定制 alexskrypnyk/customizer 二次开发

按需修改功能、优化性能、对接业务系统,提供一站式技术支持

邮箱:yvsm@zunyunkeji.com | QQ:316430983 | 微信:yvsm316

alexskrypnyk/customizer

Composer 安装命令:

composer require alexskrypnyk/customizer

包简介

Interactive customization for template projects

README 文档

README

Customizer logo

Interactive customization for template projects

GitHub Issues GitHub Pull Requests Test PHP codecov GitHub release (latest by date) LICENSE Renovate

The Customizer allows template project authors to ask users questions during the composer create-project command and then update the newly created project based on the received 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

  • Simple installation into template project
  • Runs customization on composer create-project
  • Runs customization on composer create-project --no-install via composer customize command
  • Configuration file for questions and processing logic
  • Test harness for the template project to test questions and processing logic
  • No additional dependencies for minimal footprint

Installation

  1. Add to the template project as a Composer dependency:

    "require-dev": {
        "alexskrypnyk/customizer": "^0.5"
    },
    "config": {
        "allow-plugins": {
            "alexskrypnyk/customizer": true
        }
    }

    These entries will be removed by the Customizer after your project's users run the composer create-project command.

  2. Copy customize.php file with questions and processing logic in any location within your template project and adjust it as needed.

See the Configuration section below for more information.

Usage example

When your users run the composer create-project command, the Customizer will ask them questions and 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

In this example, the demonstration questions will ask you to provide a package name, description, and license type. The answers are then processed by updating the composer.json file and replacing the package name in other project files.

--no-install

Your users may run the composer create-project --no-install command if they want to adjust the project before installing dependencies, for example. Customizer will not run in this case as it is not being installed yet and it's dependencies entries will stay in the composer.json file.

The user will have to run composer customize manually to run the Customizer. It could be useful to let your users know about this command in your project's README file.

Configuration

You can configure how the Customizer processes your user’s template project by providing an arbitrary class (with any namespace) in a customize.php file. This includes defining questions and processing logic.

The class has to implement public static methods :

  • questions() - defines questions; required
  • process() - defines processing logic based on received answers; required
  • cleanup() - defines processing logic for the composer.json file; optional
  • messages() - defines custom messages seen by the user; optional

questions()

Defines questions, their discovery and validation callbacks. Questions will be asked in the order they are defined. Questions can use answers from previous questions received so far.

The discovery callback is optional and runs before the question is asked. It can be used to discover the default answer based on the current state of the project. The discovered value is passed to the question callback. It can be an anonymous function or a method of the configuration class named discover<QuestionName>.

The validation callback should return the validated answer or throw an exception with a message to be shown to the user. This uses inbuilt SymfonyStyle's ask() method for asking questions.

customize.php has an example of the questions() method.

Note that while the Customizer examples use SymfonyStyle's ask() method, you can build your own question asking logic using any other TUI interaction methods. For example, you can use Laravel Prompts.

process()

Defines processing logic for all answers. This method will be called after all answers are received and the user confirms the intended changes. It has access to all answers and Customizer's class public properties and methods.

All file manipulations should be done within this method.

customize.php has an example of the process() method.

cleanup()

Defines the cleanup() method after all files were processed but before all dependencies are updated.

The Customizer will remove itself from the project and will update the composer.json as required. This method allows to alter that process as needed and, if necessary, cancel the original self-cleanup.

customize.php has an example of the cleanup() method.

messages()

Defines overrides for the Customizer's messages shown to the user.

customize.php has an example of the messages() method.

Example configuration

Click to expand an example configuration customize.php file
<?php

declare(strict_types=1);

use AlexSkrypnyk\Customizer\CustomizeCommand;

/**
 * Customizer configuration.
 *
 * Example configuration for the Customizer command.
 *
 * phpcs:disable Drupal.Classes.ClassFileName.NoMatch
 */
class Customize {

  /**
   * A required callback with question definitions.
   *
   * Place questions into this method if you are using Customizer as a
   * single-file drop-in for your scaffold project. Otherwise - place them into
   * the configuration class.
   *
   * Any questions defined in the `questions()` method of the configuration
   * class will **fully override** the questions defined here. This means that
   * the configuration class must provide a full set of questions.
   *
   * See `customize.php` for an example of how to define questions.
   *
   * @return array<string,array<string,string|callable>>
   *   An associative array of questions with question title as a key and the
   *   value of array with the following keys:
   *   - question: Required question callback function used to ask the question.
   *     The callback receives the following arguments:
   *     - discovered: A value discovered by the discover callback or NULL.
   *     - answers: An associative array of all answers received so far.
   *     - command: The CustomizeCommand object.
   *   - discover: Optional callback function used to discover the value from
   *     the environment. Can be an anonymous function or a method of this class
   *     as discover<PascalCasedQuestion>. If not provided, empty string will
   *     be passed to the question callback. The callback receives the following
   *     arguments:
   *     - command: The CustomizeCommand object.
   */
  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 discover callback function is used to discover the value from the
        // environment. In this case, we use the current directory name
        // and the GITHUB_ORG environment variable to generate the package name.
        'discover' => static function (CustomizeCommand $c): string {
          $name = basename((string) getcwd());
          $org = getenv('GITHUB_ORG') ?: 'acme';

          return $org . '/' . $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.
        // The discovery callback is used to provide a default value.
        // The question callback provides a capability to validate the answer
        // before it can be accepted by providing a validation callback.
        'question' => static fn(string $discovered, array $answers, CustomizeCommand $c): mixed => $c->io->ask('Package name', $discovered, static function (string $value): string {
          // This is a validation callback that checks if the package name is
          // valid. If not, an \InvalidArgumentException 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;
        }),
      ],
      'Description' => [
        // For this question, we use an answer from the previous question
        // in the title of the question.
        'question' => static fn(string $discovered, array $answers, CustomizeCommand $c): mixed => $c->io->ask(sprintf('Description for %s', $answers['Name'])),
      ],
      'License' => [
        // For this question, we use a pre-defined list of options.
        // For discovery, we use a separate method named 'discoverLicense'
        // (only for the demonstration purposes; it could have been an
        // anonymous function).
        'question' => static fn(string $discovered, array $answers, CustomizeCommand $c): mixed => $c->io->choice('License type',
          [
            'MIT',
            'GPL-3.0-or-later',
            'Apache-2.0',
          ],
          // Note that the default value is the value discovered by the
          // 'discoverLicense' method. If the discovery did not return a value,
          // the default value of 'GPL-3.0-or-later' is used.
          empty($discovered) ? 'GPL-3.0-or-later' : $discovered
        ),
      ],
    ];
  }

  /**
   * A callback to discover the `License` value from the environment.
   *
   * This is an example of discovery function as a class method.
   *
   * @param \AlexSkrypnyk\Customizer\CustomizeCommand $c
   *   The Customizer instance.
   */
  public static function discoverLicense(CustomizeCommand $c): string {
    return isset($c->composerjsonData['license']) && is_string($c->composerjsonData['license']) ? $c->composerjsonData['license'] : '';
  }

  /**
   * A required callback to process all answers.
   *
   * This method is called after all questions have been answered and a user
   * has confirmed the intent to proceed with the customization.
   *
   * Note that any manipulation of the composer.json file should be done here
   * and then written back to the file system.
   *
   * @param array<string,string> $answers
   *   Gathered answers.
   * @param \AlexSkrypnyk\Customizer\CustomizeCommand $c
   *   The Customizer instance.
   */
  public static function process(array $answers, CustomizeCommand $c): void {
    $c->debug('Updating composer configuration');
    $json = $c->readComposerJson($c->composerjson);
    $json['name'] = $answers['Name'];
    $json['description'] = $answers['Description'];
    $json['license'] = $answers['License'];
    $c->writeComposerJson($c->composerjson, $json);

    $c->debug('Removing an arbitrary file.');
    $files = $c->finder($c->cwd)->files()->name('LICENSE');
    foreach ($files as $file) {
      $c->fs->remove($file->getRealPath());
    }
  }

  /**
   * Cleanup after the customization.
   *
   * By the time this method is called, all the necessary changes have been made
   * to the project.
   *
   * The Customizer will remove itself from the project and will update the
   * composer.json as required. This method allows to alter that process as
   * needed and, if necessary, cancel the original self-cleanup.
   *
   * @param \AlexSkrypnyk\Customizer\CustomizeCommand $c
   *   The CustomizeCommand object.
   *
   * @return bool
   *   Return FALSE to skip the further self-cleanup. Returning TRUE will
   *   proceed with the self-cleanup.
   */
  public static function cleanup(CustomizeCommand $c): bool {
    if ($c->isComposerDependenciesInstalled) {
      $c->debug('Add an example flag to composer.json.');
      $json = $c->readComposerJson($c->composerjson);
      $json['extra'] = is_array($json['extra']) ? $json['extra'] : [];
      $json['extra']['custom_field'] = TRUE;
      $c->writeComposerJson($c->composerjson, $json);
    }

    return TRUE;
  }

  /**
   * Override some of the messages displayed to the user by Customizer.
   *
   * @param \AlexSkrypnyk\Customizer\CustomizeCommand $c
   *   The Customizer instance.
   *
   * @return array<string,string|array<string>>
   *   An associative array of messages with message name as key and the message
   *   test as a string or an array of strings.
   */
  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',
    ];
  }

}

Helpers

The Customizer provides a few helpers to make processing answers easier. These are available as properties and methods of the Customizer instance passed to the processing callbacks:

  • cwd - current working directory.
  • fs - Symfony Filesystem instance.
  • io - Symfony input/output instance.
  • isComposerDependenciesInstalled - whether the Composer dependencies were installed before the Customizer started.
  • readComposerJson() - Read the contents of the composer.json file into an array.
  • writeComposerJson() - Write the contents of the array to the composer.json file.
  • replaceInPath() - Replace a string in a file or all files in a directory.
  • replaceInPathBetweenMarkers() - 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.

Validation helpers for questions 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

  1. Install the Customizer into your template project as described in the Installation section.
  2. Create a new testing directory and change into it.
  3. Create a project in this directory:
composer create-project yournamespace/yourscaffold="@dev" --repository '{"type": "path", "url": "/path/to/yourscaffold", "options": {"symlink": false}}' .
  1. 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 when running Composer commands.

Automated functional tests

The Customizer provides a test harness to help you, as a template project author, to test the questions and processing with ease.

To use the test harness:

  1. Setup PHPUnit in your template project to run tests.
  2. Inherit your test classes from CustomizerTestCase.php (this file is included into distribution when you add Customizer to your template project).
  3. Add path to CustomizerTestCase.php into the autoload-dev section of your template project's composer.json file:
     "autoload-dev": {
         "psr-4": {
             "AlexSkrypnyk\\Customizer\\Tests\\": "vendor/alexskrypnyk/customizer/tests/phpunit"
         }
     },
  4. Create a directory in your project with the name tests/phpunit/Fixtures/<name_of_test_snake_case> and place your test fixtures there. If you use data providers, you can create a sub-directory with the name of the data set within the provider (the top-level key within the data provider).
  5. Add fixtures as base/expected directory structures (see below) and assert for the expected results in your test.

See examples within the template project example.

Comparing fixture directories

The base test class CustomizerTestCase.php provides the assertFixtureDirectoryEqualsSut() method to compare a directory under test with the expected results.

The method uses base and expected directories to compare the results: base is used as a state of the project you are testing before the customization ran, and expected is used as an expected result, which will be compared to the actual result after the customization.

Because the projects can have dependencies added during composer install and other files that are not related to the customization, the method allows you to specify the list of files to ignore during the comparison using .gitignore-like syntax with the addition to ignore content changes but still assess the file presence.

See the description in CustomizerTestCase::assertDirectoriesEqual() for more information about the comparison process.

Maintenance

composer install   # Install dependencies.
composer lint      # Check coding standards.
composer lint-fix  # Fix coding standards.
composer test      # Run tests.

This repository was created using the getscaffold.dev project scaffold template

alexskrypnyk/customizer 适用场景与选型建议

alexskrypnyk/customizer 是一款 基于 PHP 开发的 Composer 扩展包,目前已累计 475 次下载、GitHub Stars 达 1, 最近一次更新时间为 2024 年 05 月 21 日, 在 PHP 生态内属于活跃度较高的组件。

我们在过去多个企业项目中使用过 alexskrypnyk/customizer 或与其功能相近的方案,如果你在选型或落地过程中遇到问题,例如 版本兼容、二次改造、私有化封装、与内部系统对接、生产 BUG 排查,欢迎联系我们协助评估。

围绕 alexskrypnyk/customizer 我们能提供哪些服务?
定制开发 / 二次开发

基于 alexskrypnyk/customizer 在你已有业务上做功能扩展、字段裁剪、UI 适配、与内部账号 / 权限 / 日志系统的深度对接。

BUG 修复 & 性能优化

线上偶发问题、内存泄漏、慢查询、并发异常等排查修复;针对高流量场景做缓存、队列、索引层面的调优。

项目外包 & 长期维护

承接完整的项目从需求 → 设计 → 开发 → 上线 → 长期运维;也可按月提供技术保姆服务。

yvsm@zunyunkeji.com QQ:316430983 微信:yvsm316 西安尊云信息科技 · 专注 PHP / Go / 分布式系统研发

统计信息

  • 总下载量: 475
  • 月度下载量: 0
  • 日度下载量: 0
  • 收藏数: 1
  • 点击次数: 25
  • 依赖项目数: 1
  • 推荐数: 0

GitHub 信息

  • Stars: 1
  • Watchers: 2
  • Forks: 0
  • 开发语言: PHP

其他信息

  • 授权协议: GPL-2.0-or-later
  • 更新时间: 2024-05-21