- 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.
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 valuesstatus- StatusFormatter with badgesboolean- BooleanFormatter (German: Ja/Nein with badges)currency- CurrencyFormatternumber- NumberFormattermasked- 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 inputemail- Email inputpassword- Password inputtextarea- Multi-line textfile- File uploadhidden- Hidden fieldselect- Dropdown selectcheckbox- 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
- Use Value Objects for Configuration - Always use AdminTableConfig and AdminFormConfig instead of raw arrays
- Leverage Formatters - Use built-in formatters for dates, status badges, booleans, etc.
- Provide Rich Context - Include helpful labels, placeholders, and help text in forms
- Enable JavaScript Features - Use sortable, searchable, paginated for better UX
- Implement All API Endpoints - Required for JavaScript table functionality
- Use Consistent Naming - Resource name should match routes and repository
- Inject Dependencies - Always inject factories, renderers, handlers via constructor
- 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: truein AdminTableConfig - Mark specific columns as searchable:
'searchable' => true - Implement search logic in repository
findAll($filters)method
Sorting not working:
- Enable
sortable: truein AdminTableConfig - Mark specific columns as sortable:
'sortable' => true - Implement sorting in repository or API handler