php-framework/packages/core-php/TODO.md
Snider 65dd9af950 refactor: consolidate migrations and clean up core packages
- Remove old incremental migrations (now consolidated into create_* files)
- Clean up cached view files
- Various fixes across core-api, core-mcp, core-php packages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 22:28:58 +00:00

7.6 KiB

Core-PHP TODO

Implemented

Actions Pattern ✓

Core\Actions\Action trait for single-purpose business logic classes.

use Core\Actions\Action;

class CreateThing
{
    use Action;

    public function handle(User $user, array $data): Thing
    {
        // Complex business logic here
    }
}

// Usage
$thing = CreateThing::run($user, $data);

Location: src/Core/Actions/Action.php, src/Core/Actions/Actionable.php


Seeder Auto-Discovery

Priority: Medium Context: Currently apps need a database/seeders/DatabaseSeeder.php that manually lists module seeders in order. This is boilerplate that core-php could handle.

Requirements

  • Auto-discover seeders from registered modules (*/Database/Seeders/*Seeder.php)
  • Support priority ordering via property or attribute (e.g., public int $priority = 50)
  • Support dependency ordering via $after or $before arrays
  • Provide base DatabaseSeeder class that apps can extend or use directly
  • Allow apps to override/exclude specific seeders if needed

Example

// In a module seeder
class FeatureSeeder extends Seeder
{
    public int $priority = 10; // Run early

    public function run(): void { ... }
}

class PackageSeeder extends Seeder
{
    public array $after = [FeatureSeeder::class]; // Run after features

    public function run(): void { ... }
}

Notes

  • Current Host Hub DatabaseSeeder has ~20 seeders with implicit ordering
  • Key dependencies: features → packages → workspaces → system user → content
  • Could use Laravel's service container to resolve seeder graph

Team-Scoped Caching

Priority: Medium Context: Repeated queries for workspace-scoped resources. Cache workspace-scoped queries with auto-invalidation.

Implementation

Extend BelongsToWorkspace trait:

trait BelongsToWorkspace
{
    public static function ownedByCurrentWorkspaceCached(int $ttl = 300)
    {
        $workspace = currentWorkspace();
        if (!$workspace) return collect();

        return Cache::remember(
            static::workspaceCacheKey($workspace->id),
            $ttl,
            fn() => static::ownedByCurrentWorkspace()->get()
        );
    }

    protected static function bootBelongsToWorkspace(): void
    {
        static::saved(fn($m) => static::clearWorkspaceCache($m->workspace_id));
        static::deleted(fn($m) => static::clearWorkspaceCache($m->workspace_id));
    }
}

Usage

// Cached for 5 minutes, auto-clears on changes
$biolinks = Biolink::ownedByCurrentWorkspaceCached();

Activity Logging

Priority: Low Context: No audit trail of user actions across modules.

Implementation

Add spatie/laravel-activitylog integration:

// Core trait for models
trait LogsActivity
{
    use \Spatie\Activitylog\Traits\LogsActivity;

    public function getActivitylogOptions(): LogOptions
    {
        return LogOptions::defaults()
            ->logOnlyDirty()
            ->dontSubmitEmptyLogs();
    }
}

Requirements

  • Base trait modules can use
  • Activity viewer Livewire component for admin
  • Workspace-scoped activity queries

Multi-Tenant Data Isolation

Priority: High (Security) Context: Multiple modules have workspace isolation issues.

Issues

  1. Fallback workspace_id - Some code falls back to workspace_id = 1 when no context
  2. Global queries - Some commands query globally without workspace scope
  3. Session trust - Using session('workspace_id', 1) with hardcoded fallback

Solution

  • BelongsToWorkspace trait should throw exception when workspace context is missing (not fallback)
  • Add WorkspaceScope global scope that throws on missing context
  • Audit all models for proper scoping
  • Add middleware that ensures workspace context before any workspace-scoped operation

Bouncer Request Whitelisting

Priority: Medium Context: Every controller action must be explicitly permitted. Unknown actions are blocked (production) or prompt for approval (training mode).

Philosophy: If it wasn't trained, it doesn't exist.

Concept

Training Mode (Development):
1. Developer hits /admin/products
2. Clicks "Create Product"
3. System: "BLOCKED - No permission defined for:"
   - Role: admin
   - Action: product.create
   - Route: POST /admin/products
4. Developer clicks [Allow for admin]
5. Permission recorded
6. Continue working

Production Mode:
If permission not in whitelist → 403 Forbidden
No exceptions. No fallbacks. No "default allow".

Database Schema

// core_action_permissions
Schema::create('core_action_permissions', function (Blueprint $table) {
    $table->id();
    $table->string('action');                     // product.create, order.refund
    $table->string('scope')->nullable();          // Resource type or specific ID
    $table->string('guard')->default('web');      // web, api, admin
    $table->string('role')->nullable();           // admin, editor, or null for any auth
    $table->boolean('allowed')->default(false);
    $table->string('source');                     // 'trained', 'seeded', 'manual'
    $table->string('trained_route')->nullable();
    $table->foreignId('trained_by')->nullable();
    $table->timestamp('trained_at')->nullable();
    $table->timestamps();

    $table->unique(['action', 'scope', 'guard', 'role']);
});

// core_action_requests (audit log)
Schema::create('core_action_requests', function (Blueprint $table) {
    $table->id();
    $table->string('method');
    $table->string('route');
    $table->string('action');
    $table->string('scope')->nullable();
    $table->string('guard');
    $table->string('role')->nullable();
    $table->foreignId('user_id')->nullable();
    $table->string('ip_address')->nullable();
    $table->string('status');                     // allowed, denied, pending
    $table->boolean('was_trained')->default(false);
    $table->timestamps();

    $table->index(['action', 'status']);
});

Action Resolution

// Explicit via route attribute
Route::post('/products', [ProductController::class, 'store'])
    ->action('product.create');

// Or via controller attribute
#[Action('product.create')]
public function store(Request $request) { ... }

// Or auto-resolved from controller@method
// ProductController@store → product.store

Integration with Existing Auth

Request
    │
    ▼
BouncerGate (action whitelisting)
    │ "Is this action permitted at all?"
    ▼
Laravel Gate/Policy (authorisation)
    │ "Can THIS USER do this to THIS RESOURCE?"
    ▼
Controller

Implementation Phases

Phase 1: Core

  • Database migrations
  • ActionPermission model
  • BouncerService with check() method
  • BouncerGate middleware

Phase 2: Training Mode

  • Training UI (modal prompt)
  • Training controller/routes
  • Request logging

Phase 3: Tooling

  • bouncer:export command
  • bouncer:list command
  • Admin UI for viewing/editing permissions

Phase 4: Integration

  • Apply to admin routes
  • Apply to API routes
  • Documentation

Artisan Commands

php artisan bouncer:export   # Export trained permissions to seeder
php artisan bouncer:seed     # Import from seeder
php artisan bouncer:list     # List all defined actions
php artisan bouncer:reset    # Clear training data

Benefits

  1. Complete audit trail - Know exactly what actions exist in your app
  2. No forgotten routes - If it's not trained, it doesn't work
  3. Role-based by default - Actions scoped to guards and roles
  4. Deployment safety - Export/import permissions between environments
  5. Discovery tool - Training mode maps your entire app's surface area