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
93 changes: 93 additions & 0 deletions app/Console/Commands/AuditDocsCoverage.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

declare(strict_types=1);

namespace App\Console\Commands;

use App\Enums\DocsPlatform;
use App\Models\DocsCoverageGap;
use App\Services\DocsCoverageAuditor;
use Illuminate\Console\Command;

final class AuditDocsCoverage extends Command
{
protected $signature = 'docs:audit-coverage {platform? : desktop or mobile — every platform if omitted}';

protected $description = "Cross-reference each platform's config keys and console commands against the docs, and record what's undocumented";

public function handle(DocsCoverageAuditor $auditor): int
{
if ($this->argument('platform')) {
$platform = DocsPlatform::tryFrom($this->argument('platform'));

if ($platform === null) {
$this->error("Unknown platform: {$this->argument('platform')}");

return self::FAILURE;
}

$platforms = [$platform];
} else {
$platforms = DocsPlatform::cases();
}

foreach ($platforms as $platform) {
$this->auditPlatform($auditor, $platform);
}

return self::SUCCESS;
}

private function auditPlatform(DocsCoverageAuditor $auditor, DocsPlatform $platform): void
{
$results = $auditor->audit($platform);

if ($results->isEmpty()) {
$this->error("{$platform->label()}: audit returned nothing (a fetch likely failed) — leaving stored gaps untouched.");

return;
}

$checkedAt = now();

foreach ($results as $result) {
DocsCoverageGap::query()->updateOrCreate(
[
'platform' => $platform,
'category' => $result['category'],
'identifier' => $result['identifier'],
],
[
'documented' => $result['documented'],
'source_path' => $result['source_path'],
'checked_at' => $checkedAt,
]
);
}

// Stale identifiers (renamed, removed) are dropped per category —
// scoped this way so a category the auditor didn't touch this run
// (e.g. no command files matched) can't have its untouched rows
// wiped by another category's identifier list.
foreach ($results->groupBy(fn (array $result) => $result['category']->value) as $category => $categoryResults) {
DocsCoverageGap::query()
->where('platform', $platform)
->where('category', $category)
->whereNotIn('identifier', $categoryResults->pluck('identifier'))
->delete();
}

$undocumented = $results->reject(fn (array $result) => $result['documented']);

$this->info(sprintf(
'%s: %d checked, %d undocumented',
$platform->label(),
$results->count(),
$undocumented->count()
));

foreach ($undocumented as $gap) {
$this->line(sprintf(' - [%s] %s', $gap['category']->label(), $gap['identifier']));
}
}
}
6 changes: 6 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ protected function schedule(Schedule $schedule): void
$schedule->command('payouts:process-eligible')
->dailyAt('11:00')
->onOneServer();

// Flag docs that have drifted from the packages' config keys and console commands
$schedule->command('docs:audit-coverage')
->weekly()
->onOneServer()
->runInBackground();
}

/**
Expand Down
19 changes: 19 additions & 0 deletions app/Enums/DocsCoverageCategory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum DocsCoverageCategory: string
{
case ConfigKey = 'config_key';
case ConsoleCommand = 'console_command';

public function label(): string
{
return match ($this) {
self::ConfigKey => 'Config key',
self::ConsoleCommand => 'Console command',
};
}
}
24 changes: 24 additions & 0 deletions app/Enums/DocsPlatform.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace App\Enums;

enum DocsPlatform: string
{
case Desktop = 'desktop';
case Mobile = 'mobile';

public function label(): string
{
return match ($this) {
self::Desktop => 'NativePHP for Desktop',
self::Mobile => 'NativePHP for Mobile',
};
}

public static function tryFromRoute(): ?self
{
return self::tryFrom((string) request()->route('platform'));
}
}
96 changes: 96 additions & 0 deletions app/Filament/Resources/DocsCoverageGapResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources;

use App\Enums\DocsCoverageCategory;
use App\Enums\DocsPlatform;
use App\Filament\Resources\DocsCoverageGapResource\Pages;
use App\Models\DocsCoverageGap;
use Filament\Actions;
use Filament\Resources\Resource;
use Filament\Tables;
use Filament\Tables\Table;

final class DocsCoverageGapResource extends Resource
{
protected static ?string $model = DocsCoverageGap::class;

protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-magnifying-glass';

protected static ?string $navigationLabel = 'Docs Coverage';

protected static ?string $pluralModelLabel = 'Docs Coverage Gaps';

public static function canCreate(): bool
{
return false;
}

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('platform')
->formatStateUsing(fn (DocsPlatform $state): string => $state->label())
->sortable(),

Tables\Columns\TextColumn::make('category')
->formatStateUsing(fn (DocsCoverageCategory $state): string => $state->label())
->badge()
->sortable(),

Tables\Columns\TextColumn::make('identifier')
->searchable()
->sortable(),

Tables\Columns\IconColumn::make('documented')
->boolean()
->sortable(),

Tables\Columns\TextColumn::make('source_path')
->label('Source')
->toggleable(),

Tables\Columns\TextColumn::make('checked_at')
->label('Last checked')
->dateTime()
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('platform')
->options(collect(DocsPlatform::cases())->mapWithKeys(
fn (DocsPlatform $platform) => [$platform->value => $platform->label()]
)),
Tables\Filters\SelectFilter::make('category')
->options(collect(DocsCoverageCategory::cases())->mapWithKeys(
fn (DocsCoverageCategory $category) => [$category->value => $category->label()]
)),
Tables\Filters\TernaryFilter::make('documented'),
])
->actions([
Actions\DeleteAction::make(),
])
->bulkActions([
Actions\BulkActionGroup::make([
Actions\DeleteBulkAction::make(),
]),
])
->defaultSort('documented');
}

public static function getRelations(): array
{
return [
//
];
}

public static function getPages(): array
{
return [
'index' => Pages\ListDocsCoverageGaps::route('/'),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\DocsCoverageGapResource\Pages;

use App\Filament\Resources\DocsCoverageGapResource;
use Filament\Resources\Pages\ListRecords;

final class ListDocsCoverageGaps extends ListRecords
{
protected static string $resource = DocsCoverageGapResource::class;
}
117 changes: 117 additions & 0 deletions app/Filament/Resources/DocsFeedbackResource.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources;

use App\Enums\DocsPlatform;
use App\Filament\Resources\DocsFeedbackResource\Pages;
use App\Models\DocsFeedback;
use Filament\Actions;
use Filament\Infolists;
use Filament\Resources\Resource;
use Filament\Schemas;
use Filament\Schemas\Schema;
use Filament\Tables;
use Filament\Tables\Table;

final class DocsFeedbackResource extends Resource
{
protected static ?string $model = DocsFeedback::class;

protected static \BackedEnum|string|null $navigationIcon = 'heroicon-o-hand-thumb-up';

protected static ?string $navigationLabel = 'Docs Feedback';

protected static ?string $pluralModelLabel = 'Docs Feedback';

public static function canCreate(): bool
{
return false;
}

public static function infolist(Schema $schema): Schema
{
return $schema
->inlineLabel()
->columns(1)
->schema([
Schemas\Components\Section::make('Feedback')
->inlineLabel()
->columns(1)
->schema([
Infolists\Components\TextEntry::make('page')
->label('Page'),
Infolists\Components\TextEntry::make('platform'),
Infolists\Components\TextEntry::make('version'),
Infolists\Components\IconEntry::make('helpful')
->boolean(),
Infolists\Components\TextEntry::make('comment')
->placeholder('—'),
Infolists\Components\TextEntry::make('created_at')
->label('Submitted')
->dateTime(),
]),
]);
}

public static function table(Table $table): Table
{
return $table
->columns([
Tables\Columns\TextColumn::make('page')
->searchable()
->sortable(),

Tables\Columns\TextColumn::make('platform')
->sortable(),

Tables\Columns\TextColumn::make('version')
->sortable(),

Tables\Columns\IconColumn::make('helpful')
->boolean()
->sortable(),

Tables\Columns\TextColumn::make('comment')
->limit(60)
->placeholder('—'),

Tables\Columns\TextColumn::make('created_at')
->label('Submitted')
->dateTime()
->sortable(),
])
->filters([
Tables\Filters\SelectFilter::make('platform')
->options(collect(DocsPlatform::cases())->mapWithKeys(
fn (DocsPlatform $platform) => [$platform->value => ucfirst($platform->value)]
)),
Tables\Filters\TernaryFilter::make('helpful'),
])
->actions([
Actions\ViewAction::make(),
])
->bulkActions([
Actions\BulkActionGroup::make([
Actions\DeleteBulkAction::make(),
]),
])
->defaultSort('created_at', 'desc');
}

public static function getRelations(): array
{
return [
//
];
}

public static function getPages(): array
{
return [
'index' => Pages\ListDocsFeedback::route('/'),
'view' => Pages\ViewDocsFeedback::route('/{record}'),
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace App\Filament\Resources\DocsFeedbackResource\Pages;

use App\Filament\Resources\DocsFeedbackResource;
use Filament\Resources\Pages\ListRecords;

final class ListDocsFeedback extends ListRecords
{
protected static string $resource = DocsFeedbackResource::class;
}
Loading