Scaffold Walkthrough
Build a complete WPZylos plugin from scratch.
What We'll Build
A "Task Manager" plugin featuring:
- Custom database table
- Admin CRUD interface
- Nonce + capability protection
- Validation + sanitization
- Database migrations
- PHP-Scoper isolated build
Prerequisites
- PHP 8.0+
- WordPress 6.0+
- Composer 2.0+
- WP-CLI (optional)
Step 1: Create Plugin
cd /path/to/wordpress/wp-content/plugins
composer create-project MyCompanystudio/wpzylos-scaffold task-manager
cd task-manager
Initialize using the Scaffold CLI:
Windows (PowerShell):
.\scaffold.ps1 init
Linux/Mac:
chmod +x scaffold.sh
./scaffold.sh init
Answer the prompts:
Plugin Name: Task Manager
Plugin Slug: task-manager
PHP Namespace: TaskManager
Scoper Prefix: task_manager
Database Prefix: taskmanager_
Step 2: Review Generated Structure
task-manager/
+-- task-manager.php # Main plugin file
+-- bootstrap/
| +-- app.php # Bootstraps application
+-- config/
| +-- app.php # Provider registration
+-- src/
| +-- Providers/
| +-- AppServiceProvider.php
+-- database/
| +-- migrations/ # Database migrations
+-- resources/
| +-- views/ # Templates
+-- routes/
| +-- admin.php # Admin routes
+-- scoper.inc.php # PHP-Scoper config
+-- composer.json
Step 3: Create Migration
php zylos make:migration create_tasks_table
Edit database/migrations/xxxx_create_tasks_table.php:
<?php
use WPZylos\Framework\Migrations\Migration;
return new class extends Migration
{
public function up(): void
{
$table = $this->context->tableName('tasks');
$sql = "CREATE TABLE {$table} (
id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
title varchar(255) NOT NULL,
description text,
status varchar(50) NOT NULL DEFAULT 'pending',
priority int(11) NOT NULL DEFAULT 0,
user_id bigint(20) unsigned NOT NULL,
due_date date DEFAULT NULL,
created_at datetime DEFAULT CURRENT_TIMESTAMP,
updated_at datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY user_id (user_id),
KEY status (status)
) {$this->charset};";
$this->dbDelta($sql);
}
public function down(): void
{
$this->dropTable('tasks');
}
};
Step 4: Create Model & Service
php zylos make:model Task
php zylos make:service TaskService
Edit src/Services/TaskService.php:
<?php
namespace TaskManager\Services;
use WPZylos\Framework\Database\Connection;
use WPZylos\Framework\Core\Contracts\ContextInterface;
class TaskService
{
private string $table;
public function __construct(
private Connection $db,
private ContextInterface $context
) {
$this->table = $context->tableName('tasks');
}
public function all(): array
{
return $this->db->getResults(
"SELECT * FROM {$this->table} ORDER BY created_at DESC"
);
}
public function find(int $id): ?object
{
return $this->db->getRow(
$this->db->prepare(
"SELECT * FROM {$this->table} WHERE id = %d",
$id
)
);
}
public function create(array $data): int
{
return $this->db->insert($this->table, $data);
}
public function update(int $id, array $data): int
{
return $this->db->update($this->table, $data, ['id' => $id]);
}
public function delete(int $id): int
{
return $this->db->delete($this->table, ['id' => $id]);
}
}
Step 5: Create Controller
php zylos make:controller TaskController
Edit src/Http/Controllers/TaskController.php:
<?php
namespace TaskManager\Http\Controllers;
use WPZylos\Framework\Views\ViewFactory;
use WPZylos\Framework\Security\Nonce;
use WPZylos\Framework\Security\Gate;
use WPZylos\Framework\Security\Sanitizer;
use WPZylos\Framework\Validation\Validator;
use WPZylos\Framework\Core\Contracts\ContextInterface;
use TaskManager\Services\TaskService;
class TaskController
{
public function __construct(
private ViewFactory $view,
private TaskService $tasks,
private Nonce $nonce,
private Gate $gate,
private Sanitizer $sanitizer,
private ContextInterface $context
) {}
public function index(): void
{
$tasks = $this->tasks->all();
echo $this->view->render('admin/tasks/index', [
'title' => __('Tasks', $this->context->textDomain()),
'tasks' => $tasks,
'deleteNonce' => $this->nonce->create('delete_task'),
]);
}
public function create(): void
{
echo $this->view->render('admin/tasks/create', [
'title' => __('Add Task', $this->context->textDomain()),
'nonce' => $this->nonce->create('create_task'),
]);
}
public function store(): void
{
// 1. Verify nonce
if (!$this->nonce->verify($_POST['_nonce'] ?? '', 'create_task')) {
wp_die(__('Invalid request', $this->context->textDomain()));
}
// 2. Check capability
if (!$this->gate->can('manage_options')) {
wp_die(__('Unauthorized', $this->context->textDomain()));
}
// 3. Validate
$validator = new Validator();
$result = $validator->validate($_POST, [
'title' => 'required|string|max:255',
'description' => 'nullable|string',
'status' => 'required|in:pending,in_progress,completed',
'priority' => 'nullable|numeric|min:0|max:10',
'due_date' => 'nullable|date',
]);
if ($result->fails()) {
set_transient(
$this->context->transientKey('form_errors'),
$result->errors(),
30
);
wp_redirect(admin_url('admin.php?page=' . $this->context->slug() . '-add'));
exit;
}
// 4. Sanitize
$data = [
'title' => $this->sanitizer->text($_POST['title']),
'description' => $this->sanitizer->textarea($_POST['description'] ?? ''),
'status' => $this->sanitizer->text($_POST['status']),
'priority' => absint($_POST['priority'] ?? 0),
'due_date' => $this->sanitizer->text($_POST['due_date'] ?? null),
'user_id' => get_current_user_id(),
];
// 5. Create
$this->tasks->create($data);
wp_redirect(admin_url('admin.php?page=' . $this->context->slug() . '&saved=1'));
exit;
}
}
Step 6: Register Routes
Edit routes/admin.php:
<?php
use TaskManager\Http\Controllers\TaskController;
/** @var \WPZylos\Framework\Routing\Router $router */
$router->admin('', [TaskController::class, 'index']);
$router->admin('add', [TaskController::class, 'create']);
$router->post('store', [TaskController::class, 'store']);
Step 7: Create Views
Create resources/views/admin/tasks/index.php:
<?php
/** @var string $title */
/** @var array $tasks */
/** @var string $deleteNonce */
/** @var \WPZylos\Framework\Core\Contracts\ContextInterface $context */
?>
<div class="wrap">
<h1>
<?php echo esc_html($title); ?>
<a href="<?php echo esc_url(admin_url('admin.php?page=' . $context->slug() . '-add')); ?>"
class="page-title-action">
<?php esc_html_e('Add New', $context->textDomain()); ?>
</a>
</h1>
<?php if (isset($_GET['saved'])): ?>
<div class="notice notice-success is-dismissible">
<p><?php esc_html_e('Task saved!', $context->textDomain()); ?></p>
</div>
<?php endif; ?>
<table class="wp-list-table widefat striped">
<thead>
<tr>
<th><?php esc_html_e('Title', $context->textDomain()); ?></th>
<th><?php esc_html_e('Status', $context->textDomain()); ?></th>
<th><?php esc_html_e('Priority', $context->textDomain()); ?></th>
<th><?php esc_html_e('Due Date', $context->textDomain()); ?></th>
<th><?php esc_html_e('Actions', $context->textDomain()); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($tasks as $task): ?>
<tr>
<td><strong><?php echo esc_html($task->title); ?></strong></td>
<td><?php echo esc_html(ucfirst($task->status)); ?></td>
<td><?php echo esc_html($task->priority); ?></td>
<td><?php echo $task->due_date ? esc_html($task->due_date) : '—'; ?></td>
<td>
<a href="<?php echo esc_url(admin_url('admin.php?page=' . $context->slug() . '-edit&id=' . $task->id)); ?>">
<?php esc_html_e('Edit', $context->textDomain()); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
Step 8: Run Migrations
wp zylos migrate
Step 9: Activate and Test
wp plugin activate task-manager
Visit WordPress Admin → Task Manager.
Step 10: Build for Production
Use the Scaffold CLI for production builds:
Windows (PowerShell):
.\scaffold.ps1 build
Linux/Mac:
./scaffold.sh build
The build script will:
- Clean previous build artifacts
- Run code style checks (phpcbf)
- Run static analysis (phpstan)
- Install production dependencies
- Run PHP-Scoper for namespace isolation
- Create versioned ZIP in
dist/
Step 11: Verify Isolation
Install two instances to verify isolation:
# Copy plugin
cp -r task-manager task-manager-clone
# Edit clone's identity
# - Change slug to task-manager-clone
# - Change prefix to task_clone_
# - Activate both plugins
Both should work independently with separate database tables.
Next Steps
- Add edit/delete functionality
- Implement AJAX actions
- Add REST API endpoints
- Write unit tests
- Set up CI/CD
Related
- wpzylos-core — Application foundation
- wpzylos-database — Database abstraction
- wpzylos-security — Security utilities
- wpzylos.com — Complete framework documentation