diff --git a/.github/workflows/tests.yaml b/.github/workflows/tests.yaml index 84c0f3e..09e450e 100644 --- a/.github/workflows/tests.yaml +++ b/.github/workflows/tests.yaml @@ -14,36 +14,19 @@ jobs: max-parallel: 4 matrix: operatingSystem: [ubuntu-latest] - phpVersion: ['8.1', '8.2', '8.3'] + phpVersion: ['8.2', '8.3', '8.4'] fail-fast: false - env: - extensions: curl, fileinfo, gd, mbstring, openssl, pdo, pdo_sqlite, sqlite3, xml, zip concurrency: - group: ${{ github.ref }}-${{ github.workflow }}-${{ matrix.operatingSystem }}-${{ matrix.phpVersion }} + group: tests-${{ github.ref }}-${{ github.workflow }}-${{ matrix.operatingSystem }}-${{ matrix.phpVersion }} cancel-in-progress: true steps: - - name: Checkout Winter CMS - uses: actions/checkout@v4 - with: - repository: wintercms/winter - ref: develop - - - name: Checkout Winter Docs plugin - uses: actions/checkout@v4 - with: - path: plugins/winter/docs - - - name: Install PHP - uses: shivammathur/setup-php@v2 + - name: Setup Winter CMS + uses: wintercms/setup-winter-action@v1 with: php-version: ${{ matrix.phpVersion }} - tools: composer:v2 - extensions: ${{ env.extensions }} - - - name: Install Composer dependencies - run: | - sed -i 's|plugins/myauthor/\*/composer.json|plugins/*/*/composer.json|g' composer.json - composer install --no-interaction --no-progress --no-scripts + winter-ref: wip/1.3 + plugin-author: winter + plugin-name: docs - name: Run tests run: php artisan winter:test -p Winter.Docs diff --git a/.gitignore b/.gitignore index 2526679..392937a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ composer.lock -vendor +vendor/ .phpunit.result.cache +.phpunit.cache/ coverage.xml .DS_Store diff --git a/classes/BaseDocumentation.php b/classes/BaseDocumentation.php index 1e94753..c0e715d 100644 --- a/classes/BaseDocumentation.php +++ b/classes/BaseDocumentation.php @@ -345,7 +345,16 @@ public function extract(): void } } - $zip->extractTo($this->getDownloadPath('extracted'), $toExtract); + if (!$zip->extractTo($this->getDownloadPath('extracted'), $toExtract)) { + throw new ApplicationException( + sprintf( + 'Could not extract the documentation for "%s" from the remote source "%s" - %s', + $this->identifier, + $this->source, + $zip->getStatusString() + ) + ); + } // Move remaining files into location $extractPath = $this->getDownloadPath('extracted/' . $this->zipFolder); diff --git a/classes/PHPApiDocumentation.php b/classes/PHPApiDocumentation.php index 360734d..249fb9e 100644 --- a/classes/PHPApiDocumentation.php +++ b/classes/PHPApiDocumentation.php @@ -2,10 +2,10 @@ namespace Winter\Docs\Classes; -use File; use Illuminate\Support\Facades\App; use Twig\TemplateWrapper; use Winter\Storm\Exception\ApplicationException; +use Winter\Storm\Support\Facades\File; /** * PHP API Documentation instance. @@ -32,6 +32,11 @@ class PHPApiDocumentation extends BaseDocumentation */ protected string $template; + /** + * Path to the Twig template for rendering event API docs. + */ + protected string $eventTemplate; + /** * Prepared template for rendering API docs. */ @@ -167,12 +172,24 @@ protected function processClassLevel(PHPApiParser $parser, array $classMap, arra 'title' => $key, ]; + try { + $rendered = $this->preparedTemplate->render([ + 'class' => $class, + ]); + } catch (\Throwable $e) { + throw new ApplicationException( + sprintf( + 'An error occurred while rendering the API documentation for class "%s": %s', + $class['name'], + $e->getMessage() + ) + ); + } + // Create docs $this->getStorageDisk()->put( $this->getProcessedPath(ltrim($baseNamespace . '/' . $key . '.htm')), - $this->prependFrontMatter($class, $this->preparedTemplate->render([ - 'class' => $class, - ])) + $this->prependFrontMatter($class, $rendered) ); $nav[] = $navItem; diff --git a/classes/PHPApiParser.php b/classes/PHPApiParser.php index a4f34d8..c4272b8 100644 --- a/classes/PHPApiParser.php +++ b/classes/PHPApiParser.php @@ -104,7 +104,7 @@ public function __construct(string $basePath, array|string $sourcePaths = [], ar public function parse(): void { // Create parser and node finder - $parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7); + $parser = (new ParserFactory)->createForHostVersion(); $nodeFinder = new NodeFinder; // Add name resolver diff --git a/composer.json b/composer.json index a403e09..fba9e1e 100644 --- a/composer.json +++ b/composer.json @@ -25,8 +25,8 @@ "require": { "php": ">=7.2.9", "composer/installers": "~1.0", - "nikic/php-parser": "^4.11.0", - "phpdocumentor/reflection-docblock": "^5.2.2" + "nikic/php-parser": "^5.6.0", + "phpdocumentor/reflection-docblock": "^5.6.2" }, "extra": { "installer-name": "docs", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f06746c..dedebc4 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,25 +1,22 @@ - - - ./tests - - - - - - classes - Plugin.php - - + + + ./tests + + + + + classes + Plugin.php + + diff --git a/tests/classes/ApiParserTest.php b/tests/classes/ApiParserTest.php index 82c27a8..675cd82 100644 --- a/tests/classes/ApiParserTest.php +++ b/tests/classes/ApiParserTest.php @@ -1,12 +1,15 @@ -assertCount(7, $this->apiParser->getPaths()); @@ -44,10 +44,7 @@ public function testGetPaths() ], $filenames); } - /** - * @covers \Winter\Docs\Classes\PHPApiParser::parse() - * @testdox can parse all PHP files and present the schema in an array. - */ + #[TestDox('can parse all PHP files and present the schema in an array..')] public function testParse() { $this->apiParser->parse(); diff --git a/tests/classes/BaseDocumentationTest.php b/tests/classes/BaseDocumentationTest.php index 2a452f4..2c89e3d 100644 --- a/tests/classes/BaseDocumentationTest.php +++ b/tests/classes/BaseDocumentationTest.php @@ -2,124 +2,73 @@ namespace Winter\Docs\Tests\Classes; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\TestDox; use System\Tests\Bootstrap\TestCase; +use Winter\Docs\Classes\BaseDocumentation; use Winter\Storm\Exception\ApplicationException; -/** - * @covers \Winter\Docs\Classes\BaseDocumentation - * @testdox The Base Documentation abstract (\Winter\Docs\Classes\BaseDocumentation) - */ +#[CoversClass(\Winter\Docs\Classes\BaseDocumentation::class)] +#[TestDox('The Base Documentation abstract (\Winter\Docs\Classes\BaseDocumentation)')] class BaseDocumentationTest extends TestCase { - /** - * @covers \Winter\Docs\Classes\BaseDocumentation::download() - * @covers \Winter\Docs\Classes\BaseDocumentation::isDownloaded() - * @testdox can download a remote documentation ZIP file and indicate that it is downloaded. - */ - public function testDownload(): void + #[TestDox('can download, extract a downloaded docs ZIP file and clean-up afterwards.')] + public function testDownloadExtractAndCleanUp(): void { - $doc = $this->getMockForAbstractClass( - 'Winter\Docs\Classes\BaseDocumentation', - [ + $doc = $this->getMockBuilder(BaseDocumentation::class) + ->setConstructorArgs([ 'Winter.Docs.Test', [ 'name' => 'Winter Docs Test', - 'type' => 'user', + 'type' => 'md', 'source' => 'remote', - 'url' => 'https://github.com/wintercms/docs/archive/refs/heads/main.zip', - 'zipFolder' => 'docs-main', - ] - ] - ); + 'url' => 'https://github.com/wintercms/docs/archive/refs/heads/develop.zip', + 'zipFolder' => 'docs-develop', + ], + ]) + ->onlyMethods(['process', 'getPageList']) + ->getMock(); $doc->download(); + $this->assertFileExists($doc->getDownloadPath('archive.zip')); $this->assertTrue($doc->isDownloaded()); - } - /** - * @covers \Winter\Docs\Classes\BaseDocumentation::download() - * @testdox will throw an exception if the documentation URL is invalid when downloading. - */ - public function testDownloadInvalidUrl(): void - { - $this->expectException(ApplicationException::class); - $this->expectExceptionMessageMatches('/Could not retrieve the documentation/i'); + $doc->extract(); - $doc = $this->getMockForAbstractClass( - 'Winter\Docs\Classes\BaseDocumentation', - [ - 'Winter.Docs.Test', - [ - 'name' => 'Winter Docs Test', - 'type' => 'md', - 'source' => 'remote', - 'url' => 'https://wintercms.com/missing/docs.zip', - 'zipFolder' => 'docs-main', - ] - ] - ); + $this->assertDirectoryExists($doc->getDownloadPath('collated')); + $this->assertFileExists($doc->getDownloadPath('collated/snowboard/introduction.md')); + // Re-download the file $doc->download(); - } - /** - * @covers \Winter\Docs\Classes\BaseDocumentation::extract() - * @testdox can extract a downloaded docs ZIP file. - */ - public function testExtract(): void - { - $doc = $this->getMockForAbstractClass( - 'Winter\Docs\Classes\BaseDocumentation', - [ - 'Winter.Docs.Test', - [ - 'name' => 'Winter Docs Test', - 'type' => 'md', - 'source' => 'remote', - 'url' => 'https://github.com/wintercms/docs/archive/refs/heads/develop.zip', - 'zipFolder' => 'docs-develop', - ] - ] - ); - - $doc->download(); - $doc->extract(); + // Clean up + $doc->cleanupDownload(); - $this->assertDirectoryExists($doc->getDownloadPath('collated')); - $this->assertFileExists($doc->getDownloadPath('collated/snowboard/introduction.md')); + $this->assertFileDoesNotExist($doc->getDownloadPath('archive.zip')); + $this->assertDirectoryDoesNotExist($doc->getDownloadPath('extracted')); } -/** - * @covers \Winter\Docs\Classes\BaseDocumentation::cleanupDownload() - * @testdox can clean up downloaded and extracted assets. - */ - public function testCleanupDownload(): void + #[TestDox('will throw an exception if the documentation URL is invalid when downloading.')] + public function testDownloadInvalidUrl(): void { - $doc = $this->getMockForAbstractClass( - 'Winter\Docs\Classes\BaseDocumentation', - [ + $this->expectException(ApplicationException::class); + $this->expectExceptionMessageMatches('/Could not retrieve the documentation/i'); + + $doc = $this->getMockBuilder(BaseDocumentation::class) + ->setConstructorArgs([ 'Winter.Docs.Test', [ 'name' => 'Winter Docs Test', 'type' => 'md', 'source' => 'remote', - 'url' => 'https://github.com/wintercms/docs/archive/refs/heads/develop.zip', - 'zipFolder' => 'docs-develop', - ] - ] - ); - - $doc->download(); - $doc->extract(); + 'url' => 'https://wintercms.com/missing/docs.zip', + 'zipFolder' => 'docs-main', + ], + ]) + ->onlyMethods(['process', 'getPageList']) + ->getMock(); - // Re-download the file $doc->download(); - - // Clean up - $doc->cleanupDownload(); - - $this->assertFileDoesNotExist($doc->getDownloadPath('archive.zip')); - $this->assertDirectoryDoesNotExist($doc->getDownloadPath('extracted')); } } diff --git a/tests/classes/DocsManagerTest.php b/tests/classes/DocsManagerTest.php index 5881168..981bba4 100644 --- a/tests/classes/DocsManagerTest.php +++ b/tests/classes/DocsManagerTest.php @@ -2,14 +2,15 @@ namespace Winter\Docs\Tests\Classes; +use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\TestDox; use System\Tests\Bootstrap\PluginTestCase; use System\Classes\PluginManager; use Winter\Docs\Classes\DocsManager; -/** - * @covers \Winter\Docs\Classes\DocsManager - * @testdox The Documentation Manager (\Winter\Docs\Classes\DocsManager) - */ + +#[CoversClass(\Winter\Docs\Classes\DocsManager::class)] +#[TestDox('The Documentation Manager (\Winter\Docs\Classes\DocsManager)')] class DocsManagerTest extends PluginTestCase { protected $docsManager; @@ -33,10 +34,7 @@ public function setUp(): void $this->docsManager = DocsManager::instance(); } - /** - * @covers \Winter\Docs\Classes\DocsManager::makeIdentifier() - * @testdox can make valid identifiers for docs. - */ + #[TestDox('can make valid identifiers for docs.')] public function testMakeIdentifier() { $this->assertEquals( @@ -60,12 +58,7 @@ public function testMakeIdentifier() ); } - /** - * @covers \Winter\Docs\Classes\DocsManager::addDocumentation() - * @covers \Winter\Docs\Classes\DocsManager::removeDocumentation() - * @covers \Winter\Docs\Classes\DocsManager::hasDocumentation() - * @testdox can manually add and remove documentation. - */ + #[TestDox('can manually add and remove documentation.')] public function testAddDocumentation() { $this->assertFalse($this->docsManager->hasDocumentation('Docs.Test', 'user')); diff --git a/updates/version.yaml b/updates/version.yaml index 83bf9d3..d577554 100644 --- a/updates/version.yaml +++ b/updates/version.yaml @@ -1,2 +1,3 @@ "1.0.0": Initial version of the Winter Docs plugin. "1.0.1": Allow processing of documentation through job queue. +"2.0.0": "Support Winter v1.3" diff --git a/views/api-doc.twig b/views/api-doc.twig index 12eebf2..abb0912 100644 --- a/views/api-doc.twig +++ b/views/api-doc.twig @@ -1,4 +1,18 @@ {% endif %} + {% if class.extendedBy %} +
  • + + Extended by + +
  • + {% endif %} + {% if class.implementedBy %} +
  • + + Implemented by + +
  • + {% endif %} + {% if class.usedBy %} +
  • + + Used by + +
  • + {% endif %} -
    +

    {{ class.name }} -

    - -
    - {% if class.final %} - - final - - {% else %} - - abstract - - {% endif %} - - - class - - - - {{ class.name }} - - - {% if class.extends %} - - extends - - - - {{ class.extends }} - - {% endif %} -
    + {% if class.docs.summary %}
    @@ -113,8 +132,487 @@
    {% endif %} + + {%- if class.final -%} + final + {%- elseif class.abstract -%} + abstract + {%- endif %} {{ class.type }} {{ class.namespace }}\{{ class.name -}} + + {%- if class.extends.class %}{{ "\n" }}extends {{class.extends.class }}{% endif -%} + + {%- if class.implements %}{{ "\n" }}implements {% endif -%} + {%- for interface in class.implements %}{{ "\n" }} {{ interface.class }}{% if not loop.last %},{% endif%}{% endfor -%} + + {% if class.docs.body %}
    {{ class.docs.body | raw }}
    {% endif %} + +{% if class.extends %} +

    + # + Extends +

    + + + + + + + + + + + + + + +
    ClassDescription
    + + {{ class.extends.name }} + + + {{ class.extends.summary | raw }} +
    +{% endif %} + +{% if class.traits %} +

    + # + Traits +

    + + + + + + + + + + {% for trait in class.traits %} + + + + + {% endfor %} + +
    TraitDescription
    + + {{ trait.name }} + + + {{ trait.summary | raw }} +
    +{% endif %} + +{% if class.constants %} +

    + # + Constants +

    + + + + + + + + + + + + {% for constant in class.constants %} + + + + + + + {% endfor %} + +
    ConstantTypeValueDescription
    + {{ constant.name }} + + {% if constant.type.definition == 'reference' and constant.type.type.linked %} + + {{ constant.type.type.name }} + + {% elseif constant.type.definition == 'reference' %} + {{ constant.type.type.name }} + {% else %} + {{ constant.type.type }} + {% endif %} + + {{ constant.value }} + + {{ constant.docs.summary | raw }} +
    +{% endif %} + +{% if class.properties %} +

    + # + Properties +

    + + {% for property in class.properties %} +

    + # + + {% if property.inherited %} + + inherited + + {% endif %} + + + {{ property.visibility }} + + + {% if property.static %} + + static + + {% endif %} + + ${{ property.name }} + + + : + {% if property.type.definition == 'union' %} + {% for type in property.type.types %} + {% if not loop.first %} + | + {% endif %} + {% if type.definition == 'reference' and type.type.linked %} + + {{- type.type.name -}} + + {% elseif type.definition == 'reference' %} + {{ type.type.name }} + {% else %} + {{ type.type }} + {% endif %} + {% endfor %} + {% else %} + {% if property.type.definition == 'reference' and property.type.type.linked %} + + {{- property.type.type.name -}} + + {% elseif property.type.definition == 'reference' %} + {{ property.type.type.name }} + {% else %} + {{ property.type.type }} + {% endif %} + {% endif %} + {% if property.default %} + + = {{ property.default }} + + {% endif %} + +

    + + {% if property.inherited %} +
    + Inherited from + + {{ property.inherited.name }} + +
    + {% endif %} + +
    + {{ property.docs.summary | raw }} + + {{ property.docs.body | raw }} +
    + {% endfor %} +{% endif %} + +{% if class.methods %} +

    + # + Methods +

    + + {% for method in class.methods %} +

    + # + + {% if method.inherited %} + + inherited + + {% endif %} + + + {{ method.visibility }} + + + {% if method.static %} + + static + + {% endif %} + {% if method.final %} + + final + + {% endif %} + + {{ method.name }} + ( + {%- if method.params|length > 0 -%} + + {%- for param in method.params -%} + {%- if param.type.definition == 'union' -%} + {%- for type in param.type.types -%} + {%- if not loop.first %} | {% endif -%} + {%- if type.definition == 'reference' and type.type.linked -%} + + {{- type.type.name -}} + + {%- elseif type.definition == 'reference'-%} + {{ type.type.name }} + {%- else -%} + {{ type.type }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- if param.type.definition == 'reference' and param.type.type.linked -%} + + {{- param.type.type.name -}} + + {%- elseif param.type.definition == 'reference' -%} + {{ param.type.type.name }} + {%- elseif param.type.definition == 'scalar' and param.type.type != 'mixed' -%} + {{ param.type.type }} + {%- endif -%} + {%- endif -%} + {%- if param.type.definition != 'scalar' or param.type.type != 'mixed' %} {% endif -%} + ${{ param.name }} + + {%- if param.default %} = {{ param.default }}{% endif -%} + {%- if not loop.last %}, {% endif -%} + {%- endfor -%} + + {%- endif -%} + ) + + {% if method.returns.type.definition != 'scalar' or method.returns.type.type != 'mixed' %} + : {%- if method.returns.type.definition == 'union' -%} + {%- for type in method.returns.type.types -%} + {%- if not loop.first %} | {% endif -%} + {%- if type.definition == 'reference' and type.type.linked -%} + + {{- type.type.name -}} + + {%- elseif type.definition == 'reference'-%} + {{ type.type.name }} + {%- else -%} + {{ type.type }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- if method.returns.type.definition == 'reference' and method.returns.type.type.linked -%} + + {{- method.returns.type.type.name -}} + + {%- elseif method.returns.type.definition == 'reference' -%} + {{ method.returns.type.type.name }} + {%- elseif method.returns.type.definition == 'scalar' -%} + {{ method.returns.type.type }} + {%- endif -%} + {%- endif -%} + {% endif %} +

    + + {% if method.inherited %} +
    + Inherited from + + {{ method.inherited.name }} + +
    + {% endif %} + +
    + {{ method.docs.summary | raw }} + + {{ method.docs.body | raw }} + + {% if method.params|length > 0 %} + Parameters + + + + + + + + + + + {% for param in method.params %} + + + + + + {% endfor %} + +
    PropertyTypeDescription
    + ${{ param.name }} + + {%- if param.type.definition == 'union' -%} + {%- for type in param.type.types -%} + {%- if not loop.first %} | {% endif -%} + {%- if type.definition == 'reference' and type.type.linked -%} + + {{- type.type.name -}} + + {%- elseif type.definition == 'reference'-%} + {{ type.type.name }} + {%- else -%} + {{ type.type }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- if param.type.definition == 'reference' and param.type.type.linked -%} + + {{- param.type.type.name -}} + + {%- elseif param.type.definition == 'reference' -%} + {{ param.type.type.name }} + {%- elseif param.type.definition == 'scalar' -%} + {{ param.type.type }} + {%- endif -%} + {%- endif -%} + + {{ param.summary | raw }} +
    + {% endif %} + + Returns + +
    + {%- if method.returns.type.definition == 'union' -%} + {%- for type in method.returns.type.types -%} + {%- if not loop.first %} | {% endif -%} + {%- if type.definition == 'reference' and type.type.linked -%} + + {{- type.type.name -}} + + {%- elseif type.definition == 'reference'-%} + {{ type.type.name }} + {%- else -%} + {{ type.type }} + {%- endif -%} + {%- endfor -%} + {%- else -%} + {%- if method.returns.type.definition == 'reference' and method.returns.type.type.linked -%} + + {{- method.returns.type.type.name -}} + + {%- elseif method.returns.type.definition == 'reference' -%} + {{ method.returns.type.type.name }} + {%- elseif method.returns.type.definition == 'scalar' -%} + {{ method.returns.type.type }} + {%- endif -%} + {%- endif -%} +
    +
    + {{ method.docs.return.summary | raw }} +
    +
    + {% endfor %} +{% endif %} + +{% if class.extendedBy %} +

    + # + Extended by +

    + + + + + + + + + + {% for extendedBy in class.extendedBy %} + + + + + {% endfor %} + +
    ClassDescription
    + + {{ extendedBy.name }} + + {{ extendedBy.summary | raw }}
    +{% endif %} + +{% if class.implementedBy %} +

    + # + Implemented by +

    + + + + + + + + + + {% for implementedBy in class.implementedBy %} + + + + + {% endfor %} + +
    ClassDescription
    + + {{ implementedBy.name }} + + {{ implementedBy.summary | raw }}
    +{% endif %} + +{% if class.usedBy %} +

    + # + Used by +

    + + + + + + + + + + {% for usedBy in class.usedBy %} + + + + + {% endfor %} + +
    ClassDescription
    + + {{ usedBy.name }} + + {{ usedBy.summary | raw }}
    +{% endif %}