Files
michaelschiemer/src/Framework/Admin
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- Add comprehensive health check system with multiple endpoints
- Add Prometheus metrics endpoint
- Add production logging configurations (5 strategies)
- Add complete deployment documentation suite:
  * QUICKSTART.md - 30-minute deployment guide
  * DEPLOYMENT_CHECKLIST.md - Printable verification checklist
  * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle
  * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference
  * production-logging.md - Logging configuration guide
  * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation
  * README.md - Navigation hub
  * DEPLOYMENT_SUMMARY.md - Executive summary
- Add deployment scripts and automation
- Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment
- Update README with production-ready features

All production infrastructure is now complete and ready for deployment.
2025-10-25 19:18:37 +02:00
..

Admin Framework

Streamlined, attribute-based admin panel framework for the Custom PHP Framework.

Overview

The Admin Framework provides a unified, consistent approach to building admin interfaces with:

  • Attribute-Driven Configuration - Use #[AdminResource] for controller-level metadata
  • Pure Composition - No inheritance, all functionality via dependency injection
  • Factory Pattern - AdminTableFactory, AdminFormFactory for building components
  • Interactive JavaScript - Auto-initializing data tables with sorting, searching, pagination
  • Auto-Generated APIs - CRUD endpoints with AdminApiHandler
  • Consistent Rendering - AdminPageRenderer for unified page layouts

Architecture

┌─────────────────────┐
│  AdminResource      │  Controller Attribute
│  (Metadata)         │
└──────────┬──────────┘
           │
           ├──────────────────────┬──────────────────────┬─────────────────────┐
           │                      │                      │                     │
┌──────────▼──────────┐  ┌───────▼──────────┐  ┌───────▼──────────┐  ┌──────▼────────┐
│ AdminTableFactory   │  │ AdminFormFactory │  │ AdminPageRenderer│  │ AdminApiHandler│
│ (Build Tables)      │  │ (Build Forms)    │  │ (Render Pages)   │  │ (CRUD API)    │
└──────────┬──────────┘  └───────┬──────────┘  └───────┬──────────┘  └──────┬────────┘
           │                      │                      │                     │
           ├──────────────────────┴──────────────────────┘                     │
           │                                                                    │
┌──────────▼──────────┐  ┌────────────────────┐  ┌─────────────────────────┐ │
│ Table Module        │  │ FormBuilder Module │  │ Templates               │ │
│ (View/Table)        │  │ (View/FormBuilder) │  │ (admin-index/form)      │ │
└─────────────────────┘  └────────────────────┘  └─────────────────────────┘ │
                                                                               │
┌──────────────────────────────────────────────────────────────────────────┐ │
│ JavaScript AdminDataTable Module                                          │ │
│ - Auto-initialization via data attributes                                 │◄┘
│ - AJAX loading with sorting, searching, pagination                        │
└────────────────────────────────────────────────────────────────────────────┘

Core Components

1. AdminResource Attribute

Controller-level attribute for admin resource configuration.

#[AdminResource(
    name: 'users',              // Resource name (route prefix)
    singularName: 'User',       // Singular display name
    pluralName: 'Users',        // Plural display name
    icon: 'user',               // Icon identifier
    enableApi: true,            // Auto-generate CRUD API
    enableCrud: true            // Enable CRUD operations
)]
final readonly class UserAdminController
{
    // ...
}

2. AdminTableConfig Value Object

Type-safe table configuration.

$tableConfig = AdminTableConfig::create(
    resource: 'users',
    columns: [
        'id' => [
            'label' => 'ID',
            'sortable' => true,
            'class' => 'text-center',
        ],
        'email' => [
            'label' => 'Email',
            'sortable' => true,
            'searchable' => true,
        ],
        'status' => [
            'label' => 'Status',
            'formatter' => 'status',  // badge, date, boolean, etc.
        ],
    ],
    sortable: true,
    searchable: true
);

Available Formatters:

  • date - DateFormatter for date/time values
  • status - StatusFormatter with badges
  • boolean - BooleanFormatter (German: Ja/Nein with badges)
  • currency - CurrencyFormatter
  • number - NumberFormatter
  • masked - MaskedFormatter for sensitive data

3. AdminFormConfig Value Object

Type-safe form configuration.

$formConfig = new AdminFormConfig(
    resource: 'users',
    action: '/admin/users',
    method: Method::POST,
    fields: [
        'email' => [
            'type' => 'email',
            'label' => 'Email Address',
            'required' => true,
            'placeholder' => 'user@example.com',
        ],
        'name' => [
            'type' => 'text',
            'label' => 'Full Name',
            'required' => true,
        ],
        'role' => [
            'type' => 'select',
            'label' => 'Role',
            'options' => [
                'user' => 'User',
                'admin' => 'Administrator',
            ],
        ],
        'active' => [
            'type' => 'checkbox',
            'label' => 'Active',
        ],
    ]
);

Supported Field Types:

  • text - Text input
  • email - Email input
  • password - Password input
  • textarea - Multi-line text
  • file - File upload
  • hidden - Hidden field
  • select - Dropdown select
  • checkbox - Checkbox input

4. AdminTableFactory

Creates interactive tables using the existing Table module.

final readonly class ExampleController
{
    public function __construct(
        private AdminTableFactory $tableFactory,
    ) {}

    public function index(): ViewResult
    {
        $tableConfig = AdminTableConfig::create(/* ... */);
        $data = $this->repository->findAll();

        $table = $this->tableFactory->create($tableConfig, $data);

        // Table automatically includes data attributes for JavaScript:
        // data-resource="users"
        // data-api-endpoint="/admin/api/users"
        // data-sortable="true"
        // data-searchable="true"
    }
}

5. AdminFormFactory

Creates forms using the existing FormBuilder module.

final readonly class ExampleController
{
    public function __construct(
        private AdminFormFactory $formFactory,
    ) {}

    public function create(): ViewResult
    {
        $formConfig = new AdminFormConfig(/* ... */);
        $form = $this->formFactory->create($formConfig);

        // For edit forms, provide existing data:
        $formConfig = $formConfig->withData($user->toArray());
        $form = $this->formFactory->create($formConfig);
    }
}

6. AdminPageRenderer

Consistent rendering for admin pages.

final readonly class ExampleController
{
    public function __construct(
        private AdminPageRenderer $pageRenderer,
    ) {}

    // Index/list page
    public function index(): ViewResult
    {
        return $this->pageRenderer->renderIndex(
            resource: 'users',
            table: $table,
            title: 'Users',
            actions: [
                [
                    'url' => '/admin/users/create',
                    'label' => 'Create User',
                    'icon' => 'plus',
                ],
            ]
        );
    }

    // Form page (create/edit)
    public function create(): ViewResult
    {
        return $this->pageRenderer->renderForm(
            resource: 'users',
            form: $form,
            title: 'Create User',
            subtitle: 'Add a new user to the system'
        );
    }

    // Detail page
    public function show(): ViewResult
    {
        return $this->pageRenderer->renderShow(
            resource: 'users',
            title: 'User Details',
            data: [
                'user' => $user->toArray(),
                'stats' => $userStats,
            ]
        );
    }
}

7. AdminApiHandler

Auto-generated CRUD API endpoints.

final readonly class ExampleController
{
    public function __construct(
        private AdminApiHandler $apiHandler,
        private UserRepository $repository,
    ) {}

    #[Route('/admin/api/users', Method::GET)]
    public function apiList(HttpRequest $request): JsonResult
    {
        // Handles: page, per_page, sort_by, sort_dir, search
        return $this->apiHandler->handleList($request, $this->repository);
    }

    #[Route('/admin/api/users/{id}', Method::GET)]
    public function apiGet(string $id): JsonResult
    {
        return $this->apiHandler->handleGet($id, $this->repository);
    }

    #[Route('/admin/api/users', Method::POST)]
    public function apiCreate(HttpRequest $request): JsonResult
    {
        return $this->apiHandler->handleCreate($request, $this->repository);
    }

    #[Route('/admin/api/users/{id}', Method::PUT)]
    public function apiUpdate(string $id, HttpRequest $request): JsonResult
    {
        return $this->apiHandler->handleUpdate($id, $request, $this->repository);
    }

    #[Route('/admin/api/users/{id}', Method::DELETE)]
    public function apiDelete(string $id): JsonResult
    {
        return $this->apiHandler->handleDelete($id, $this->repository);
    }
}

8. JavaScript AdminDataTable Module

Interactive data table with AJAX operations.

Auto-Initialization:

// Automatically initializes on elements with both attributes:
// [data-resource][data-api-endpoint]

document.addEventListener('DOMContentLoaded', () => {
    document.querySelectorAll('[data-resource][data-api-endpoint]').forEach(table => {
        new AdminDataTable(table).init();
    });
});

Features:

  • AJAX loading with loading states
  • Column sorting (click headers)
  • Search/filtering (debounced)
  • Pagination
  • Empty state handling
  • Error handling
  • HTML escaping for security

Data Attributes:

<table
    data-resource="users"
    data-api-endpoint="/admin/api/users"
    data-sortable="true"
    data-searchable="true"
    data-paginated="true"
    data-per-page="25"
>
    <!-- Table content -->
</table>

Complete Example

See src/Application/Admin/UserAdminController.php for a complete working example.

Step 1: Create Controller with AdminResource

#[AdminResource(
    name: 'users',
    singularName: 'User',
    pluralName: 'Users',
    icon: 'user',
    enableApi: true,
    enableCrud: true
)]
final readonly class UserAdminController
{
    public function __construct(
        private AdminPageRenderer $pageRenderer,
        private AdminTableFactory $tableFactory,
        private AdminFormFactory $formFactory,
        private AdminApiHandler $apiHandler,
        private UserRepository $repository,
    ) {}
}

Step 2: Implement Index Route

#[Route('/admin/users', Method::GET)]
public function index(): ViewResult
{
    $tableConfig = AdminTableConfig::create(
        resource: 'users',
        columns: [
            'id' => ['label' => 'ID', 'sortable' => true],
            'email' => ['label' => 'Email', 'sortable' => true, 'searchable' => true],
            'name' => ['label' => 'Name', 'sortable' => true, 'searchable' => true],
            'created_at' => ['label' => 'Created', 'formatter' => 'date'],
            'status' => ['label' => 'Status', 'formatter' => 'status'],
        ]
    );

    $users = $this->repository->findAll();
    $table = $this->tableFactory->create($tableConfig, array_map(
        fn($user) => $user->toArray(),
        $users
    ));

    return $this->pageRenderer->renderIndex(
        resource: 'users',
        table: $table,
        title: 'Users',
        actions: [
            ['url' => '/admin/users/create', 'label' => 'Create User', 'icon' => 'plus'],
        ]
    );
}

Step 3: Implement Create/Store Routes

#[Route('/admin/users/create', Method::GET)]
public function create(): ViewResult
{
    $formConfig = new AdminFormConfig(
        resource: 'users',
        action: '/admin/users',
        method: Method::POST,
        fields: [
            'email' => ['type' => 'email', 'label' => 'Email', 'required' => true],
            'name' => ['type' => 'text', 'label' => 'Name', 'required' => true],
            'password' => ['type' => 'password', 'label' => 'Password', 'required' => true],
        ]
    );

    $form = $this->formFactory->create($formConfig);

    return $this->pageRenderer->renderForm(
        resource: 'users',
        form: $form,
        title: 'Create User'
    );
}

#[Route('/admin/users', Method::POST)]
public function store(HttpRequest $request): Redirect
{
    $data = $request->parsedBody->toArray();

    $this->repository->create([
        'email' => $data['email'],
        'name' => $data['name'],
        'password' => password_hash($data['password'], PASSWORD_DEFAULT),
    ]);

    return new Redirect('/admin/users', status: 303, flashMessage: 'User created');
}

Step 4: Implement Edit/Update Routes

#[Route('/admin/users/{id}/edit', Method::GET)]
public function edit(string $id): ViewResult
{
    $user = $this->repository->findById($id);

    $formConfig = (new AdminFormConfig(
        resource: 'users',
        action: "/admin/users/{$id}",
        method: Method::PUT,
        fields: [
            'email' => ['type' => 'email', 'label' => 'Email', 'required' => true],
            'name' => ['type' => 'text', 'label' => 'Name', 'required' => true],
        ]
    ))->withData($user->toArray());

    $form = $this->formFactory->create($formConfig);

    return $this->pageRenderer->renderForm(
        resource: 'users',
        form: $form,
        title: 'Edit User'
    );
}

#[Route('/admin/users/{id}', Method::PUT)]
public function update(string $id, HttpRequest $request): Redirect
{
    $data = $request->parsedBody->toArray();

    $this->repository->update($id, [
        'email' => $data['email'],
        'name' => $data['name'],
    ]);

    return new Redirect('/admin/users', status: 303, flashMessage: 'User updated');
}

Step 5: Implement Delete Route

#[Route('/admin/users/{id}', Method::DELETE)]
public function destroy(string $id): Redirect
{
    $this->repository->delete($id);
    return new Redirect('/admin/users', status: 303, flashMessage: 'User deleted');
}

Step 6: Implement API Routes

#[Route('/admin/api/users', Method::GET)]
public function apiList(HttpRequest $request): JsonResult
{
    return $this->apiHandler->handleList($request, $this->repository);
}

#[Route('/admin/api/users/{id}', Method::GET)]
public function apiGet(string $id): JsonResult
{
    return $this->apiHandler->handleGet($id, $this->repository);
}

#[Route('/admin/api/users', Method::POST)]
public function apiCreate(HttpRequest $request): JsonResult
{
    return $this->apiHandler->handleCreate($request, $this->repository);
}

#[Route('/admin/api/users/{id}', Method::PUT)]
public function apiUpdate(string $id, HttpRequest $request): JsonResult
{
    return $this->apiHandler->handleUpdate($id, $request, $this->repository);
}

#[Route('/admin/api/users/{id}', Method::DELETE)]
public function apiDelete(string $id): JsonResult
{
    return $this->apiHandler->handleDelete($id, $this->repository);
}

Templates

admin-index.view.php

Used by AdminPageRenderer::renderIndex().

<layout name="layouts/admin" />

<div class="admin-page">
    <div class="page-header">
        <h1>{{ title }}</h1>
        <div class="page-actions">
            <if condition="{{ actions }}">
                <for items="{{ actions }}" value="action">
                    <a href="{{ action.url }}" class="btn btn-primary">
                        <if condition="{{ action.icon }}">
                            <i class="icon-{{ action.icon }}"></i>
                        </if>
                        {{ action.label }}
                    </a>
                </for>
            </if>
        </div>
    </div>

    <if condition="{{ searchable }}">
        <div class="table-controls">
            <input type="text"
                   class="form-control search-input"
                   data-table-search="{{ resource }}"
                   placeholder="Search...">
        </div>
    </if>

    <div class="table-container">
        {{ table }}
    </div>

    <div class="pagination-container" data-table-pagination="{{ resource }}"></div>
</div>

admin-form.view.php

Used by AdminPageRenderer::renderForm().

<layout name="layouts/admin" />

<div class="admin-page">
    <div class="page-header">
        <h1>{{ title }}</h1>
        <if condition="{{ subtitle }}">
            <p class="subtitle">{{ subtitle }}</p>
        </if>
    </div>

    <div class="form-container">
        <div class="card">
            <div class="card-body">
                {{ form }}
            </div>
        </div>
    </div>

    <div class="form-actions">
        <a href="/admin/{{ resource }}" class="btn btn-secondary">Cancel</a>
    </div>
</div>

Framework Compliance

The Admin Framework follows all framework principles:

No Inheritance - Pure composition, all controllers are final readonly Readonly Everywhere - All framework classes are readonly Value Objects - AdminTableConfig, AdminFormConfig instead of arrays Explicit DI - All dependencies via constructor injection Attribute-Driven - #[AdminResource] for metadata Existing Module Integration - Reuses Table and FormBuilder modules Type Safety - Strong typing throughout, no primitive obsession Immutability - Value objects with transformation methods

Best Practices

  1. Use Value Objects for Configuration - Always use AdminTableConfig and AdminFormConfig instead of raw arrays
  2. Leverage Formatters - Use built-in formatters for dates, status badges, booleans, etc.
  3. Provide Rich Context - Include helpful labels, placeholders, and help text in forms
  4. Enable JavaScript Features - Use sortable, searchable, paginated for better UX
  5. Implement All API Endpoints - Required for JavaScript table functionality
  6. Use Consistent Naming - Resource name should match routes and repository
  7. Inject Dependencies - Always inject factories, renderers, handlers via constructor
  8. Type-Safe Data - Convert entities to arrays with toArray() method

Testing

// Test table creation
it('creates admin table with correct configuration', function () {
    $config = AdminTableConfig::create(
        resource: 'test',
        columns: ['id' => ['label' => 'ID']]
    );

    $factory = new AdminTableFactory();
    $table = $factory->create($config, [['id' => 1]]);

    expect($table)->toBeInstanceOf(Table::class);
    expect($table->render())->toContain('data-resource="test"');
});

// Test form creation
it('creates admin form with correct fields', function () {
    $config = new AdminFormConfig(
        resource: 'test',
        action: '/test',
        method: Method::POST,
        fields: [
            'name' => ['type' => 'text', 'label' => 'Name']
        ]
    );

    $factory = new AdminFormFactory(new FormIdGenerator());
    $form = $factory->create($config);

    expect($form)->toBeInstanceOf(FormBuilder::class);
    expect($form->build())->toContain('name="name"');
});

// Test API handler
it('handles list API requests', function () {
    $handler = new AdminApiHandler();
    $repository = new InMemoryUserRepository();

    $request = HttpRequest::fromGlobals();
    $result = $handler->handleList($request, $repository);

    expect($result)->toBeInstanceOf(JsonResult::class);
    expect($result->data['success'])->toBeTrue();
    expect($result->data)->toHaveKey('pagination');
});

Troubleshooting

Tables not loading data:

  • Check API endpoint is correct: /admin/api/{resource}
  • Verify API route is registered with correct HTTP method
  • Ensure repository implements findAll() method
  • Check browser console for JavaScript errors

Forms not submitting:

  • Verify form action URL matches route
  • Check HTTP method matches (POST for create, PUT for update)
  • Ensure CSRF token is included (handled by FormBuilder automatically)

Search not working:

  • Enable searchable: true in AdminTableConfig
  • Mark specific columns as searchable: 'searchable' => true
  • Implement search logic in repository findAll($filters) method

Sorting not working:

  • Enable sortable: true in AdminTableConfig
  • Mark specific columns as sortable: 'sortable' => true
  • Implement sorting in repository or API handler