Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 24 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,41 @@ The BIIGLE manual contains [some tutorials](https://biigle-admin-documentation.r

## How to use this template

First, [create a new repository](https://github.com/biigle/module/generate) based on this template. Then update the name of this module from `biigle/module` to whatever name you want to use for your module. The name has to be updated at the following locations:
First, [create a new repository](https://github.com/biigle/module/generate) (either private or public) based on this template.

1. [`QuotesController.php`](src/Http/Controllers/QuotesController.php#L16)
2. [`index.blade.php#L13`](src/resources/views/index.blade.php#L13)
3. [`index.blade.php#L16`](src/resources/views/index.blade.php#L16)
4. [`ModuleServiceProvider.php#L21`](src/ModuleServiceProvider.php#L21)
5. [`ModuleServiceProvider.php#L30`](src/ModuleServiceProvider.php#L30)
6. [`ModuleServiceProvider.php#L43`](src/ModuleServiceProvider.php#L43)
7. [`composer.json#L2`](composer.json#L2)
8. [`test.yml#L15`](.github/workflows/test.yml#L15)
## Development & Development Installation

Next, update the namespace of all PHP classes (`Biigle\Modules\Module`) and replace `Module` with the name of your module. Do this in [`webpack.mix.js#L23`](webpack.mix.js#L23), too. Also update this readme for your new module. You should remove the first two subsections and update the installation instructions. Now you can install the module and start developing.
Clone the module to a local directory. The name of this module needs to be updated in several locations. For your convenience, we provide a PHP script to automate this process.

In addition to the code of the [tutorials](https://biigle.de/manual#developer-tutorials) this repository already contains the configuration for [Laravel Mix](https://laravel.com/docs/9.x/mix) as build system. To install the build system, run and then run `npm install`. Now you can use the following commands:
Run `php setup.php YourModuleName` to update all locations at once. The name must start with an uppercase letter and may only contain letters and digits (e.g. `MyModule`).

- `npm run dev`: Starts the development server supporting hot module replacement.
- `npm run build`: Builds, minifies and publishes the assets once. Always do this before you commit new code.
If your module is not (yet) published on Packagist, you need to add the following to biigle's composer.json (found in the BIIGLE root directory; if repositories already exist, add the new one to the list):

```
"repositories": [
{
"type": "vcs",
"url": "git@github.com:<github_username>/<your_module_name>"
}
]
```

If the repository is private you need to have ssh-keys configured for your GitHub account. Run `composer require biigle/<yourmodulename>:dev-main --ignore-platform-reqs` in the BIIGLE root directory to install the module to BIIGLE. Note that Composer identifies the package by the `name` in its composer.json (which `setup.php` sets to `biigle/<yourmodulename>` in lowercase), not by the repository URL. Still in the BIIGLE root directory run `php artisan vendor:publish --tag=public`.

The module is now installed to `biigle/vendor/biigle/<yourmodulename>`. You can go there and develop your module. Run `npm install` once to set up the build system. Then you can use the following commands:

- `npm run dev`: Continuously builds the assets during development.
- `npm run build`: Builds, minifies and publishes the assets once.
- `npm run lint`: Run static analysis to check for errors.

Also update this readme for your new module. You should remove the first three subsections and update the installation instructions. And don't forget to change the author and GitHub address in `composer.json` as well.

## Installation

Note that you have to replace `biigle/module` with the actual name of your module/repository.

1. Run `composer require biigle/module`. *This requires your module to be published on [Packagist](https://packagist.org/). If you don't want to publish your package, read more on [alternative options](https://getcomposer.org/doc/05-repositories.md#vcs).*
2. Add `Biigle\Modules\Module\ModuleServiceProvider::class` to the `providers` array in `config/app.php`. *Replace `Module` in the class namespace with the name of your module.*
3. Run `php artisan vendor:publish --tag=public` to refresh the public assets of the modules. Do this for every update of this module.
2. Run `php artisan vendor:publish --tag=public` to refresh the public assets of the modules. Do this for every update of this module.

## Developing

Expand Down
7 changes: 7 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,12 @@
"psr-4": {
"Biigle\\Modules\\Module\\": "src"
}
},
"extra": {
"laravel": {
"providers": [
"Biigle\\Modules\\Module\\ModuleServiceProvider"
]
}
}
}
280 changes: 280 additions & 0 deletions setup.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
<?php

/**
* Update the module name in all relevant files.
*/

const PHP_DIRECTORIES = ['src', 'tests'];

/**
* Check that a replacement can be applied without modifying anything.
*
* @return bool Whether the replacement still needs to be applied.
*/
function checkText(string $path, string $old, string $new, int $expectedCount): bool
{
$content = file_get_contents($path);
$currentCount = substr_count($content, $old);

if ($currentCount === 0) {
if (str_contains($content, $new)) {
return false;
}
throw new RuntimeException("Did not find expected text in {$path}: {$old}");
}

if ($currentCount !== $expectedCount) {
throw new RuntimeException(
"Expected {$expectedCount} occurrence(s) in {$path}, found {$currentCount}: {$old}"
);
}

return true;
}

function applyText(string $path, string $old, string $new): void
{
file_put_contents($path, str_replace($old, $new, file_get_contents($path)));
}

function validateRename(string $oldPath, string $newPath): void
{
if (file_exists($oldPath) && file_exists($newPath)) {
throw new RuntimeException(
"Both files exist: {$oldPath} and {$newPath}. Please resolve this manually."
);
}

if (!file_exists($oldPath) && !file_exists($newPath)) {
throw new RuntimeException(
"File not found. Expected one of: {$oldPath} or {$newPath}"
);
}
}

/**
* @return string[] Paths of PHP files.
*/
function phpFiles(string $root): array
{
$files = [];

foreach (PHP_DIRECTORIES as $directory) {
$path = "{$root}/{$directory}";
if (!is_dir($path)) {
continue;
}

$iterator = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($path, FilesystemIterator::SKIP_DOTS)
);

foreach ($iterator as $file) {
if ($file->isFile() && $file->getExtension() === 'php') {
$files[] = $file->getPathname();
}
}
}

return $files;
}

/**
* @param array{0: string, 1: string}[] $pairs Old and new text.
*
* @return string[] Paths of changed files.
*/
function replaceTextInPhpFiles(string $root, array $pairs): array
{
$changedFiles = [];

foreach (phpFiles($root) as $path) {
$content = file_get_contents($path);
$newContent = $content;

foreach ($pairs as [$old, $new]) {
$newContent = str_replace($old, $new, $newContent);
}

if ($newContent !== $content) {
file_put_contents($path, $newContent);
$changedFiles[] = $path;
}
}

return $changedFiles;
}

/**
* @return array{0: string, 1: string, 2: string, 3: int}[] Path, old text, new
* text and expected occurrence count.
*/
function buildReplacements(
string $root,
string $module,
string $moduleLower,
string $providerClass,
string $serviceProviderPath
): array {
return [
[
"{$root}/src/Http/Controllers/QuotesController.php",
"view('module::index')",
"view('{$moduleLower}::index')",
1,
],
[
"{$root}/src/resources/views/index.blade.php",
'vendor/biigle/module/hot',
"vendor/biigle/{$moduleLower}/hot",
2,
],
[
"{$root}/src/resources/views/index.blade.php",
"'vendor/module')",
"'vendor/{$moduleLower}')",
2,
],
[
$serviceProviderPath,
"loadViewsFrom(__DIR__.'/resources/views', 'module');",
"loadViewsFrom(__DIR__.'/resources/views', '{$moduleLower}');",
1,
],
[
$serviceProviderPath,
"\$modules->register('module', [",
"\$modules->register('{$moduleLower}', [",
1,
],
[
$serviceProviderPath,
"public_path('vendor/module')",
"public_path('vendor/{$moduleLower}')",
1,
],
[
"{$root}/composer.json",
'"name": "biigle/module"',
"\"name\": \"biigle/{$moduleLower}\"",
1,
],
[
"{$root}/composer.json",
'"Biigle\\\\Modules\\\\Module\\\\": "src"',
"\"Biigle\\\\Modules\\\\{$module}\\\\\": \"src\"",
1,
],
[
"{$root}/composer.json",
'"Biigle\\\\Modules\\\\Module\\\\ModuleServiceProvider"',
"\"Biigle\\\\Modules\\\\{$module}\\\\{$providerClass}\"",
1,
],
[
"{$root}/package.json",
'"name": "biigle-module"',
"\"name\": \"biigle-{$moduleLower}\"",
1,
],
[
"{$root}/package.json",
'Module\\\\\\\\ModuleServiceProvider',
"{$module}\\\\\\\\{$providerClass}",
1,
],
[
"{$root}/.github/workflows/test.yml",
'MODULE_NAME: Module',
"MODULE_NAME: {$module}",
1,
],
];
}


if (count($argv) < 2) {
fwrite(STDERR, "Usage: php setup.php <name>\n");
exit(1);
}

$module = $argv[1];

if (!preg_match('/^[A-Z][A-Za-z0-9]*$/', $module)) {
fwrite(STDERR, "The module name must start with an uppercase letter and may only contain letters and digits (e.g. MyModule).\n");
exit(1);
}

$root = __DIR__;
$moduleLower = strtolower($module);
$providerClass = "{$module}ServiceProvider";

$providerOldPath = "{$root}/src/ModuleServiceProvider.php";
$providerNewPath = "{$root}/src/{$providerClass}.php";
$providerTestOldPath = "{$root}/tests/ModuleServiceProviderTest.php";
$providerTestNewPath = "{$root}/tests/{$providerClass}Test.php";

try {
// Validate everything before modifying any file so a failure cannot leave
// the module half renamed.
validateRename($providerOldPath, $providerNewPath);
validateRename($providerTestOldPath, $providerTestNewPath);

$providerCurrentPath = file_exists($providerOldPath)
? $providerOldPath
: $providerNewPath;

$replacements = buildReplacements(
$root, $module, $moduleLower, $providerClass, $providerCurrentPath
);

$pending = [];
foreach ($replacements as [$path, $old, $new, $expectedCount]) {
if (checkText($path, $old, $new, $expectedCount)) {
$pending[] = [$path, $old, $new];
}
}

$changedFiles = [];

if (file_exists($providerOldPath)) {
rename($providerOldPath, $providerNewPath);
$changedFiles[] = $providerNewPath;
}

if (file_exists($providerTestOldPath)) {
rename($providerTestOldPath, $providerTestNewPath);
$changedFiles[] = $providerTestNewPath;
}

foreach ($pending as [$path, $old, $new]) {
if ($path === $providerCurrentPath) {
$path = $providerNewPath;
}
applyText($path, $old, $new);
$changedFiles[] = $path;
}

// The test namespace prefix does not contain the regular namespace prefix
// as a substring, so it must be replaced explicitly.
$changedFiles = array_merge($changedFiles, replaceTextInPhpFiles($root, [
['Biigle\\Tests\\Modules\\Module', "Biigle\\Tests\\Modules\\{$module}"],
['Biigle\\Modules\\Module', "Biigle\\Modules\\{$module}"],
['ModuleServiceProvider', $providerClass],
]));
} catch (RuntimeException $e) {
fwrite(STDERR, $e->getMessage()."\n");
exit(1);
}

$changedFiles = array_unique($changedFiles);

if ($changedFiles) {
$count = count($changedFiles);
echo "Updated module name to '{$module}' in {$count} file(s):\n";
foreach ($changedFiles as $path) {
echo ' '.substr($path, strlen($root) + 1)."\n";
}
} else {
echo "No changes needed. Module name already set to '{$module}'.\n";
}