Skip to content
Open
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
6 changes: 6 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ root = true
end_of_line = lf
charset = utf-8

[**.md]
indent_style = tab
indent_size = tab
tab_width = 4
insert_final_newline = true

[**.php]
indent_style = tab
indent_size = tab
Expand Down
240 changes: 240 additions & 0 deletions bin/document-hooks
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/usr/bin/env php
<?php

declare( strict_types=1 );

/**
* Scans src/ for HookHandler::filter() and HookHandler::run() calls,
* extracts their PHPDoc metadata (@migrate-filter / @migrate-action), warns
* about undocumented calls, and writes doc/hooks.md.
*/

$srcDir = realpath( __DIR__ . '/../src' );
$docFile = realpath( __DIR__ . '/../doc' ) . '/hooks.md';

/**
* File discoverer
*
* @param string $dir
* @return list<string> List of PHP file paths
**/
function findPhpFiles( string $dir ): array {
$files = [];
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator( $dir, RecursiveDirectoryIterator::SKIP_DOTS )
);
foreach ( $it as $file ) {
if ( $file->getExtension() === 'php' ) {
$files[] = $file->getPathname();
}
}
sort( $files );
return $files;
}


/**
* Docblock parser
*
* @return array{hookName:string|null,hookType:string|null,description:string,since:string|null,params:list<array{type:string,name:string,description:string}>}
*/
function parseDocblock( string $docblock ): array {
$info = [
'hookName' => null,
'hookType' => null,
'description' => '',
'since' => null,
'params' => [],
];

$descLines = [];

foreach ( preg_split( '/\r?\n/', $docblock ) as $line ) {
// Strip /** ... */ framing characters
$line = preg_replace( '/^\s*\/\*+\s*/', '', $line );
$line = preg_replace( '/\s*\*+\/\s*$/', '', $line );
$line = preg_replace( '/^\s*\*\s?/', '', $line );
$line = trim( $line );

if ( $line === '' ) {
continue;
}

if ( preg_match( '/^@migrate-filter\s+(\S+)/', $line, $m ) ) {
$info['hookType'] = 'filter';
$info['hookName'] = $m[1];
} elseif ( preg_match( '/^@migrate-action\s+(\S+)/', $line, $m ) ) {
$info['hookType'] = 'action';
$info['hookName'] = $m[1];
} elseif ( preg_match( '/^@since\s+(.+)/', $line, $m ) ) {
$info['since'] = trim( $m[1] );
} elseif ( preg_match( '/^@param\s+(\S+)\s+(\$\S+)\s*(.*)/', $line, $m ) ) {
$info['params'][] = [
'type' => $m[1],
'name' => $m[2],
'description' => trim( $m[3] ),
];
} elseif ( !str_starts_with( $line, '@' ) ) {
$descLines[] = $line;
}
}

$info['description'] = implode( ' ', $descLines );
return $info;
}

// ---------------------------------------------------------------------------
// Main scan
// ---------------------------------------------------------------------------

/** Tokens that mark a statement or block boundary – stop looking backwards. */
const BOUNDARIES = [ ';', '{', '}' ];

$hooks = []; // keyed by hookName, last write wins (same name = same hook)
$warnings = [];

foreach ( findPhpFiles( $srcDir ) as $filepath ) {
$relPath = ltrim( str_replace( $srcDir, '', $filepath ), DIRECTORY_SEPARATOR );
$raw = token_get_all( file_get_contents( $filepath ) );
$tokens = [];
foreach ( $raw as $tok ) {
if ( is_array( $tok ) ) {
if ( $tok[0] === T_WHITESPACE ) {
continue;
}
$tokens[] = [ 'type' => $tok[0], 'value' => $tok[1], 'line' => $tok[2] ];
} else {
$tokens[] = [ 'type' => null, 'value' => $tok, 'line' => null ];
}
}
$count = count( $tokens );

for ( $i = 0; $i < $count; $i++ ) {
$tok = $tokens[$i];

// Match the start of HookHandler::filter( or HookHandler::run(
if ( $tok['type'] !== T_STRING || $tok['value'] !== 'HookHandler' ) {
continue;
}
if ( ( $tokens[$i + 1]['value'] ?? '' ) !== '::' ) {
continue;
}
$methodToken = $tokens[$i + 2] ?? null;
if ( $methodToken === null
|| $methodToken['type'] !== T_STRING
|| !in_array( $methodToken['value'], [ 'filter', 'run' ], true ) ) {
continue;
}
if ( ( $tokens[$i + 3]['value'] ?? '' ) !== '(' ) {
continue;
}

$callMethod = $methodToken['value'];
$callLine = $tok['line'];

// ------------------------------------------------------------------
// Walk backwards to find the nearest PHPDoc comment.
// A statement boundary (; { }) means the docblock is too far away.
// ------------------------------------------------------------------
$docblock = null;
for ( $j = $i - 1; $j >= 0; $j-- ) {
$prev = $tokens[$j];
if ( $prev['type'] === T_DOC_COMMENT ) {
$docblock = $prev['value'];
break;
}
if ( in_array( $prev['value'], BOUNDARIES, true ) ) {
break;
}
}

if ( $docblock === null ) {
$warnings[] = "WARNING: HookHandler::{$callMethod}() at {$relPath}:{$callLine}"
. " has no preceding PHPDoc block.";
continue;
}

$info = parseDocblock( $docblock );

if ( $info['hookName'] === null ) {
$warnings[] = "WARNING: PHPDoc block before HookHandler::{$callMethod}()"
. " at {$relPath}:{$callLine} has no @migrate-filter / @migrate-action tag.";
continue;
}

$hooks[$info['hookName']] = array_merge( $info, [
'file' => $relPath,
'line' => $callLine,
] );
}
}

// ---------------------------------------------------------------------------
// Write warnings to STDERR
// ---------------------------------------------------------------------------

foreach ( $warnings as $warning ) {
fwrite( STDERR, $warning . PHP_EOL );
}

// ---------------------------------------------------------------------------
// Render Markdown
// ---------------------------------------------------------------------------

$filters = array_filter( $hooks, static fn( $h ) => $h['hookType'] === 'filter' );
$actions = array_filter( $hooks, static fn( $h ) => $h['hookType'] === 'action' );
ksort( $filters );
ksort( $actions );

$md = "# Available Hooks\n\n";
$md .= "> This file is auto-generated by `bin/document-hooks`. Do not edit manually.\n\n";
$md .= "Hooks allow to attach code snippets, referenced from your configuration file, to intercept and adapt the normal run of the tool.\n\n";
$md .= "See the [documentation](how_to_use_hooks.md) for a detailed explanation.\n\n";

function renderHooks( array $hooks ): string {
$out = '';
foreach ( $hooks as $hook ) {
$out .= "### `{$hook['hookName']}`\n\n";

if ( $hook['description'] !== '' ) {
$out .= "{$hook['description']}\n\n";
}

if ( $hook['since'] !== null ) {
$out .= "**Since:** [{$hook['since']}](https://github.com/hallowelt/migrate-confluence/releases/tag/{$hook['since']})\n\n";
}

if ( !empty( $hook['params'] ) ) {
$out .= "**Parameters:**\n\n";
foreach ( $hook['params'] as $param ) {
$desc = $param['description'] !== '' ? " — {$param['description']}" : '';
$out .= "- `{$param['name']}` _{$param['type']}_{$desc}\n";
}
$out .= "\n";
}

$out .= "_Defined in [`{$hook['file']}`](../src/{$hook['file']}#L{$hook['line']}) at line {$hook['line']}._\n\n";
}
return $out;
}

if ( !empty( $filters ) ) {
$md .= "## Filters\n\n";
$md .= "Filters allow modifying a concrete value.\n\n";
$md .= renderHooks( $filters );
}

if ( !empty( $actions ) ) {
$md .= "## Actions\n\n";
$md .= "Actions allow executing side-effect code at a specific point.\n\n";
$md .= renderHooks( $actions );
}

if ( empty( $filters ) && empty( $actions ) ) {
$md .= "_No hooks found._\n";
}

file_put_contents( $docFile, $md );
fwrite( STDOUT, "Generated {$docFile}\n" );

exit( empty( $warnings ) ? 0 : 1 );
53 changes: 53 additions & 0 deletions doc/hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Available Hooks

> This file is auto-generated by `bin/document-hooks`. Do not edit manually.

Hooks allow to attach code snippets, referenced from your configuration file, to intercept and adapt the normal run of the tool.

See the [documentation](how_to_use_hooks.md) for a detailed explanation.

## Filters

Filters allow modifying a concrete value.

### `convert-content-post`

Allow modification of the output after processing wikiPageTitle, confluencePageTitle, currentSpace, and contentType

**Since:** [8.0.0](https://github.com/hallowelt/migrate-confluence/releases/tag/8.0.0)

**Parameters:**

- `$sContent` _string_ — content after all processing
- `$context` _array_ — context information including pageId,
- `$dataLookup` _DBConversionDataLookup_ — data lookup object

_Defined in [`Converter/ConfluenceConverterBase.php`](../src/Converter/ConfluenceConverterBase.php#L405) at line 405._

### `convert-content-pre`

Allow modification of the raw input before processing it. currentSpace, and contentType

**Since:** [8.0.0](https://github.com/hallowelt/migrate-confluence/releases/tag/8.0.0)

**Parameters:**

- `$sContent` _string_ — content before any processing
- `$context` _array_ — context information including pageId, wikiPageTitle, confluencePageTitle,
- `$dataLookup` _DBConversionDataLookup_ — data lookup object

_Defined in [`Converter/ConfluenceConverterBase.php`](../src/Converter/ConfluenceConverterBase.php#L851) at line 851._

### `include-import-file`

Control whether the current entities.xml file should be considered.

**Since:** [5.0.0](https://github.com/hallowelt/migrate-confluence/releases/tag/5.0.0)

**Parameters:**

- `$value` _bool_ — whether to include the file. Defaults to true.
- `$file` _SplFileInfo_ — the import XML file currently processed

_Defined in [`Analyzer/ConfluenceAnalyzer.php`](../src/Analyzer/ConfluenceAnalyzer.php#L77) at line 77._

46 changes: 46 additions & 0 deletions doc/how_to_use_hooks.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Fine-Tuning the Output with Hooks

The migration tool provides a hook system that allows you to run your own code
at defined places. Create a PHP file, for example named `hooks.php`, alongside
your configuration file:

```php
<?php

return [
'filters' => [
'include-import-file' => function( $value, $file ) {
if ( str_contains( $file->getPathname(), '/some/path/' ) ) {
// we want to ignore imports from ./input/some/path/entities.xml
return false;
}
return $value;
},
]
]
```

Then reference the file in your configuration:

```yaml
config:
hook-handler: hooks.php
```

and the hook handler code is executed.

See the [list of available hooks](./hooks.md) for details on hooks.

## Actions and Filters

If you happen to have worked with WordPress’ hook system you might feel at home
instantly. In a nutshell:

* _action_ allow custom code to run at specific points during the execution.
They can stop the processing, log information, or manipulate some state.
* _filters_ receive a value as first parameter and are expected to return it
either unchanged or manipulated. The other parameters are helpful context
for the hook to decide, whether to act, like the page title or the content
type.

Register the appropriate type in your hooks PHP file.
3 changes: 2 additions & 1 deletion doc/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ please open an issue first with the intended changes that you’d like to provid
* [Output profiles](./output_profiles.md)
* [Adding author info](./how_to_add_author_info.md)
* [Structure of output files](./composer_output_structure.md)
* [Behavior and design decisions](./behavior.md)
* [Behavior and design decisions](./behavior.md)
* [Using custom code](./how_to_use_hooks.md)
16 changes: 16 additions & 0 deletions src/Analyzer/ConfluenceAnalyzer.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use HalloWelt\MigrateConfluence\Analyzer\Processor\SpaceDescription;
use HalloWelt\MigrateConfluence\Analyzer\Processor\Spaces;
use HalloWelt\MigrateConfluence\Analyzer\Processor\Users;
use HalloWelt\MigrateConfluence\Utility\HookHandler;
use HalloWelt\MigrateConfluence\Utility\MigrationConfig;
use HalloWelt\MigrateConfluence\Utility\WikisConfig;
use Psr\Log\LoggerAwareInterface;
Expand Down Expand Up @@ -64,6 +65,21 @@ public function analyze( SplFileInfo $file ): bool {
return true;
}

/**
* @migrate-filter include-import-file
*
* Control whether the current entities.xml file should be considered.
*
* @since 8.0.0
* @param bool $value whether to include the file. Defaults to true.
* @param SplFileInfo $file the import XML file currently processed
*/
$includeFile = HookHandler::filter( 'include-import-file', true, $file );
if ( !$includeFile ) {
$this->output->writeln( "Skipping file due to config: " . $file->getPathname() );
return true;
}

$sourcePath = $file->getPathname();

$this->output->writeln( "\nProcessing: $sourcePath" );
Expand Down
Loading
Loading