@mytechtoday/augment-extensions 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/augment-extensions/domain-rules/wordpress/README.md +163 -0
- package/augment-extensions/domain-rules/wordpress/module.json +32 -0
- package/augment-extensions/domain-rules/wordpress/rules/coding-standards.md +617 -0
- package/augment-extensions/domain-rules/wordpress/rules/directory-structure.md +270 -0
- package/augment-extensions/domain-rules/wordpress/rules/file-patterns.md +423 -0
- package/augment-extensions/domain-rules/wordpress/rules/gutenberg-blocks.md +493 -0
- package/augment-extensions/domain-rules/wordpress/rules/performance.md +568 -0
- package/augment-extensions/domain-rules/wordpress/rules/plugin-development.md +510 -0
- package/augment-extensions/domain-rules/wordpress/rules/project-detection.md +251 -0
- package/augment-extensions/domain-rules/wordpress/rules/rest-api.md +501 -0
- package/augment-extensions/domain-rules/wordpress/rules/security.md +564 -0
- package/augment-extensions/domain-rules/wordpress/rules/theme-development.md +388 -0
- package/augment-extensions/domain-rules/wordpress/rules/woocommerce.md +441 -0
- package/augment-extensions/domain-rules/wordpress-plugin/README.md +139 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/ajax-plugin.md +1599 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/custom-post-type-plugin.md +1727 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/gutenberg-block-plugin.md +428 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/gutenberg-block.md +422 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/mvc-plugin.md +1623 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/object-oriented-plugin.md +1343 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/rest-endpoint.md +734 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/settings-page-plugin.md +1350 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/simple-procedural-plugin.md +503 -0
- package/augment-extensions/domain-rules/wordpress-plugin/examples/singleton-plugin.md +971 -0
- package/augment-extensions/domain-rules/wordpress-plugin/module.json +53 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/activation-hooks.md +770 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/admin-interface.md +874 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/ajax-handlers.md +629 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/asset-management.md +559 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/context-providers.md +709 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/cron-jobs.md +736 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/database-management.md +1057 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/documentation-standards.md +463 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/frontend-functionality.md +478 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/gutenberg-blocks.md +818 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/internationalization.md +416 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/migration.md +667 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/performance-optimization.md +878 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/plugin-architecture.md +693 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/plugin-structure.md +352 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/rest-api.md +818 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/scaffolding-workflow.md +624 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/security-best-practices.md +866 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/testing-patterns.md +1165 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/testing.md +414 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/vscode-integration.md +751 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/woocommerce-integration.md +949 -0
- package/augment-extensions/domain-rules/wordpress-plugin/rules/wordpress-org-submission.md +458 -0
- package/augment-extensions/examples/gutenberg-block-plugin/README.md +101 -0
- package/augment-extensions/examples/gutenberg-block-plugin/examples/testimonial-block.md +428 -0
- package/augment-extensions/examples/gutenberg-block-plugin/module.json +40 -0
- package/augment-extensions/examples/rest-api-plugin/README.md +98 -0
- package/augment-extensions/examples/rest-api-plugin/examples/task-manager-api.md +1299 -0
- package/augment-extensions/examples/rest-api-plugin/module.json +40 -0
- package/augment-extensions/examples/woocommerce-extension/README.md +98 -0
- package/augment-extensions/examples/woocommerce-extension/examples/product-customizer.md +763 -0
- package/augment-extensions/examples/woocommerce-extension/module.json +40 -0
- package/augment-extensions/workflows/wordpress-plugin/README.md +232 -0
- package/augment-extensions/workflows/wordpress-plugin/ai-prompts.md +839 -0
- package/augment-extensions/workflows/wordpress-plugin/bead-decomposition-patterns.md +854 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/complete-plugin-example.md +540 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/custom-post-type-example.md +1083 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/feature-addition-workflow.md +669 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/plugin-creation-workflow.md +597 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/secure-form-handler-example.md +925 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/security-audit-workflow.md +752 -0
- package/augment-extensions/workflows/wordpress-plugin/examples/wordpress-org-submission-workflow.md +773 -0
- package/augment-extensions/workflows/wordpress-plugin/module.json +49 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/best-practices.md +942 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/development-workflow.md +702 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/submission-workflow.md +728 -0
- package/augment-extensions/workflows/wordpress-plugin/rules/testing-workflow.md +775 -0
- package/cli/dist/cli.js +5 -1
- package/cli/dist/cli.js.map +1 -1
- package/cli/dist/commands/show.d.ts.map +1 -1
- package/cli/dist/commands/show.js +41 -0
- package/cli/dist/commands/show.js.map +1 -1
- package/modules.md +52 -0
- package/package.json +1 -1
|
@@ -0,0 +1,1623 @@
|
|
|
1
|
+
# MVC Plugin Example
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
This example demonstrates **Model-View-Controller (MVC) Pattern** - a clean separation of data, presentation, and business logic for WordPress plugins.
|
|
6
|
+
|
|
7
|
+
**Complexity**: Medium-High
|
|
8
|
+
**File Count**: 15-40 files
|
|
9
|
+
**Team Size**: 2-5 developers
|
|
10
|
+
**Use Case**: Complex business logic, multiple views, testable code, clear separation of concerns
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Complete Plugin: "Task Manager MVC"
|
|
15
|
+
|
|
16
|
+
A complete task management plugin demonstrating MVC architecture with models, views, controllers, and routing.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Directory Structure
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
task-manager-mvc/
|
|
24
|
+
├── task-manager-mvc.php # Main plugin file (bootstrap)
|
|
25
|
+
├── uninstall.php # Uninstall cleanup
|
|
26
|
+
├── readme.txt # WordPress.org readme
|
|
27
|
+
├── app/
|
|
28
|
+
│ ├── models/
|
|
29
|
+
│ │ ├── Task.php # Task model
|
|
30
|
+
│ │ ├── Category.php # Category model
|
|
31
|
+
│ │ └── BaseModel.php # Base model class
|
|
32
|
+
│ ├── controllers/
|
|
33
|
+
│ │ ├── TaskController.php # Task controller
|
|
34
|
+
│ │ ├── CategoryController.php # Category controller
|
|
35
|
+
│ │ └── BaseController.php # Base controller class
|
|
36
|
+
│ ├── views/
|
|
37
|
+
│ │ ├── tasks/
|
|
38
|
+
│ │ │ ├── index.php # Task list view
|
|
39
|
+
│ │ │ ├── create.php # Create task view
|
|
40
|
+
│ │ │ ├── edit.php # Edit task view
|
|
41
|
+
│ │ │ └── show.php # Single task view
|
|
42
|
+
│ │ ├── categories/
|
|
43
|
+
│ │ │ ├── index.php # Category list view
|
|
44
|
+
│ │ │ └── form.php # Category form view
|
|
45
|
+
│ │ └── layouts/
|
|
46
|
+
│ │ ├── admin.php # Admin layout
|
|
47
|
+
│ │ └── public.php # Public layout
|
|
48
|
+
│ └── core/
|
|
49
|
+
│ ├── Router.php # URL routing
|
|
50
|
+
│ ├── View.php # View renderer
|
|
51
|
+
│ ├── Database.php # Database abstraction
|
|
52
|
+
│ └── Validator.php # Input validation
|
|
53
|
+
├── assets/
|
|
54
|
+
│ ├── css/
|
|
55
|
+
│ │ ├── admin.css
|
|
56
|
+
│ │ └── public.css
|
|
57
|
+
│ └── js/
|
|
58
|
+
│ ├── admin.js
|
|
59
|
+
│ └── public.js
|
|
60
|
+
└── languages/
|
|
61
|
+
└── task-manager-mvc.pot
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Main Plugin File
|
|
67
|
+
|
|
68
|
+
### File: `task-manager-mvc.php`
|
|
69
|
+
|
|
70
|
+
```php
|
|
71
|
+
<?php
|
|
72
|
+
/**
|
|
73
|
+
* Plugin Name: Task Manager MVC
|
|
74
|
+
* Plugin URI: https://example.com/task-manager-mvc
|
|
75
|
+
* Description: Task management with MVC architecture
|
|
76
|
+
* Version: 1.0.0
|
|
77
|
+
* Author: Your Name
|
|
78
|
+
* Author URI: https://example.com
|
|
79
|
+
* License: GPL-2.0+
|
|
80
|
+
* License URI: http://www.gnu.org/licenses/gpl-2.0.txt
|
|
81
|
+
* Text Domain: task-manager-mvc
|
|
82
|
+
* Domain Path: /languages
|
|
83
|
+
*
|
|
84
|
+
* @package Task_Manager_MVC
|
|
85
|
+
*/
|
|
86
|
+
|
|
87
|
+
// Exit if accessed directly
|
|
88
|
+
if (!defined('ABSPATH')) {
|
|
89
|
+
exit;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Define plugin constants
|
|
93
|
+
define('TMM_VERSION', '1.0.0');
|
|
94
|
+
define('TMM_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
|
95
|
+
define('TMM_PLUGIN_URL', plugin_dir_url(__FILE__));
|
|
96
|
+
define('TMM_APP_DIR', TMM_PLUGIN_DIR . 'app/');
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* PSR-4 Autoloader
|
|
100
|
+
*/
|
|
101
|
+
spl_autoload_register(function ($class) {
|
|
102
|
+
// Only autoload our classes
|
|
103
|
+
if (strpos($class, 'TMM\\') !== 0) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Remove namespace prefix
|
|
108
|
+
$class = str_replace('TMM\\', '', $class);
|
|
109
|
+
|
|
110
|
+
// Convert namespace separators to directory separators
|
|
111
|
+
$class = str_replace('\\', DIRECTORY_SEPARATOR, $class);
|
|
112
|
+
|
|
113
|
+
// Build file path
|
|
114
|
+
$file = TMM_APP_DIR . $class . '.php';
|
|
115
|
+
|
|
116
|
+
if (file_exists($file)) {
|
|
117
|
+
require $file;
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Activation hook
|
|
123
|
+
*/
|
|
124
|
+
register_activation_hook(__FILE__, function() {
|
|
125
|
+
require_once TMM_PLUGIN_DIR . 'install.php';
|
|
126
|
+
TMM_Install::activate();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Deactivation hook
|
|
131
|
+
*/
|
|
132
|
+
register_deactivation_hook(__FILE__, function() {
|
|
133
|
+
require_once TMM_PLUGIN_DIR . 'install.php';
|
|
134
|
+
TMM_Install::deactivate();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Initialize the plugin
|
|
139
|
+
*/
|
|
140
|
+
add_action('plugins_loaded', function() {
|
|
141
|
+
// Load text domain
|
|
142
|
+
load_plugin_textdomain('task-manager-mvc', false, dirname(plugin_basename(__FILE__)) . '/languages');
|
|
143
|
+
|
|
144
|
+
// Initialize router
|
|
145
|
+
$router = new TMM\Core\Router();
|
|
146
|
+
$router->init();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Admin menu
|
|
151
|
+
*/
|
|
152
|
+
add_action('admin_menu', function() {
|
|
153
|
+
add_menu_page(
|
|
154
|
+
__('Task Manager', 'task-manager-mvc'),
|
|
155
|
+
__('Tasks', 'task-manager-mvc'),
|
|
156
|
+
'manage_options',
|
|
157
|
+
'tmm-tasks',
|
|
158
|
+
'tmm_render_admin_page',
|
|
159
|
+
'dashicons-list-view',
|
|
160
|
+
25
|
|
161
|
+
);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Render admin page (entry point for router)
|
|
166
|
+
*/
|
|
167
|
+
function tmm_render_admin_page() {
|
|
168
|
+
$router = new TMM\Core\Router();
|
|
169
|
+
$router->dispatch();
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Enqueue assets
|
|
174
|
+
*/
|
|
175
|
+
add_action('admin_enqueue_scripts', function($hook) {
|
|
176
|
+
if (strpos($hook, 'tmm-') !== 0) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
wp_enqueue_style('tmm-admin', TMM_PLUGIN_URL . 'assets/css/admin.css', array(), TMM_VERSION);
|
|
181
|
+
wp_enqueue_script('tmm-admin', TMM_PLUGIN_URL . 'assets/js/admin.js', array('jquery'), TMM_VERSION, true);
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## MVC Core Components
|
|
188
|
+
|
|
189
|
+
### File: `app/core/Router.php`
|
|
190
|
+
|
|
191
|
+
```php
|
|
192
|
+
<?php
|
|
193
|
+
namespace TMM\Core;
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Router class - handles URL routing and dispatching to controllers
|
|
197
|
+
*
|
|
198
|
+
* @package Task_Manager_MVC
|
|
199
|
+
*/
|
|
200
|
+
class Router {
|
|
201
|
+
/**
|
|
202
|
+
* Routes registry
|
|
203
|
+
*
|
|
204
|
+
* @var array
|
|
205
|
+
*/
|
|
206
|
+
private $routes = array();
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Initialize router and register routes
|
|
210
|
+
*/
|
|
211
|
+
public function init() {
|
|
212
|
+
$this->register_routes();
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Register all routes
|
|
217
|
+
*/
|
|
218
|
+
private function register_routes() {
|
|
219
|
+
// Task routes
|
|
220
|
+
$this->add_route('GET', 'tasks', 'TaskController@index');
|
|
221
|
+
$this->add_route('GET', 'tasks/create', 'TaskController@create');
|
|
222
|
+
$this->add_route('POST', 'tasks/store', 'TaskController@store');
|
|
223
|
+
$this->add_route('GET', 'tasks/edit', 'TaskController@edit');
|
|
224
|
+
$this->add_route('POST', 'tasks/update', 'TaskController@update');
|
|
225
|
+
$this->add_route('POST', 'tasks/delete', 'TaskController@delete');
|
|
226
|
+
|
|
227
|
+
// Category routes
|
|
228
|
+
$this->add_route('GET', 'categories', 'CategoryController@index');
|
|
229
|
+
$this->add_route('POST', 'categories/store', 'CategoryController@store');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add a route
|
|
234
|
+
*
|
|
235
|
+
* @param string $method HTTP method
|
|
236
|
+
* @param string $path Route path
|
|
237
|
+
* @param string $action Controller@method
|
|
238
|
+
*/
|
|
239
|
+
public function add_route($method, $path, $action) {
|
|
240
|
+
$this->routes[$method][$path] = $action;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Dispatch request to appropriate controller
|
|
245
|
+
*/
|
|
246
|
+
public function dispatch() {
|
|
247
|
+
$method = $_SERVER['REQUEST_METHOD'];
|
|
248
|
+
$action = isset($_GET['action']) ? sanitize_text_field($_GET['action']) : 'tasks';
|
|
249
|
+
|
|
250
|
+
// Find matching route
|
|
251
|
+
if (isset($this->routes[$method][$action])) {
|
|
252
|
+
$route = $this->routes[$method][$action];
|
|
253
|
+
$this->call_controller($route);
|
|
254
|
+
} else {
|
|
255
|
+
// Default to task index
|
|
256
|
+
$this->call_controller('TaskController@index');
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Call controller method
|
|
262
|
+
*
|
|
263
|
+
* @param string $route Controller@method string
|
|
264
|
+
*/
|
|
265
|
+
private function call_controller($route) {
|
|
266
|
+
list($controller, $method) = explode('@', $route);
|
|
267
|
+
|
|
268
|
+
$controller_class = "TMM\\Controllers\\{$controller}";
|
|
269
|
+
|
|
270
|
+
if (class_exists($controller_class)) {
|
|
271
|
+
$controller_instance = new $controller_class();
|
|
272
|
+
|
|
273
|
+
if (method_exists($controller_instance, $method)) {
|
|
274
|
+
$controller_instance->$method();
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### File: `app/core/View.php`
|
|
282
|
+
|
|
283
|
+
```php
|
|
284
|
+
<?php
|
|
285
|
+
namespace TMM\Core;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* View class - handles view rendering
|
|
289
|
+
*
|
|
290
|
+
* @package Task_Manager_MVC
|
|
291
|
+
*/
|
|
292
|
+
class View {
|
|
293
|
+
/**
|
|
294
|
+
* Render a view
|
|
295
|
+
*
|
|
296
|
+
* @param string $view View path (e.g., 'tasks/index')
|
|
297
|
+
* @param array $data Data to pass to view
|
|
298
|
+
* @param string $layout Layout to use (default: 'admin')
|
|
299
|
+
*/
|
|
300
|
+
public static function render($view, $data = array(), $layout = 'admin') {
|
|
301
|
+
// Extract data to variables
|
|
302
|
+
extract($data);
|
|
303
|
+
|
|
304
|
+
// Start output buffering
|
|
305
|
+
ob_start();
|
|
306
|
+
|
|
307
|
+
// Include view file
|
|
308
|
+
$view_file = TMM_APP_DIR . 'views/' . $view . '.php';
|
|
309
|
+
if (file_exists($view_file)) {
|
|
310
|
+
include $view_file;
|
|
311
|
+
} else {
|
|
312
|
+
echo '<p>View not found: ' . esc_html($view) . '</p>';
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// Get view content
|
|
316
|
+
$content = ob_get_clean();
|
|
317
|
+
|
|
318
|
+
// Render with layout
|
|
319
|
+
if ($layout) {
|
|
320
|
+
self::render_layout($layout, $content, $data);
|
|
321
|
+
} else {
|
|
322
|
+
echo $content;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Render layout
|
|
328
|
+
*
|
|
329
|
+
* @param string $layout Layout name
|
|
330
|
+
* @param string $content View content
|
|
331
|
+
* @param array $data Additional data
|
|
332
|
+
*/
|
|
333
|
+
private static function render_layout($layout, $content, $data = array()) {
|
|
334
|
+
extract($data);
|
|
335
|
+
|
|
336
|
+
$layout_file = TMM_APP_DIR . 'views/layouts/' . $layout . '.php';
|
|
337
|
+
if (file_exists($layout_file)) {
|
|
338
|
+
include $layout_file;
|
|
339
|
+
} else {
|
|
340
|
+
echo $content;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Render partial view
|
|
346
|
+
*
|
|
347
|
+
* @param string $partial Partial view path
|
|
348
|
+
* @param array $data Data to pass to partial
|
|
349
|
+
*/
|
|
350
|
+
public static function partial($partial, $data = array()) {
|
|
351
|
+
extract($data);
|
|
352
|
+
|
|
353
|
+
$partial_file = TMM_APP_DIR . 'views/' . $partial . '.php';
|
|
354
|
+
if (file_exists($partial_file)) {
|
|
355
|
+
include $partial_file;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
### File: `app/core/Validator.php`
|
|
362
|
+
|
|
363
|
+
```php
|
|
364
|
+
<?php
|
|
365
|
+
namespace TMM\Core;
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Validator class - handles input validation
|
|
369
|
+
*
|
|
370
|
+
* @package Task_Manager_MVC
|
|
371
|
+
*/
|
|
372
|
+
class Validator {
|
|
373
|
+
/**
|
|
374
|
+
* Validation errors
|
|
375
|
+
*
|
|
376
|
+
* @var array
|
|
377
|
+
*/
|
|
378
|
+
private $errors = array();
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Validate data against rules
|
|
382
|
+
*
|
|
383
|
+
* @param array $data Data to validate
|
|
384
|
+
* @param array $rules Validation rules
|
|
385
|
+
* @return bool
|
|
386
|
+
*/
|
|
387
|
+
public function validate($data, $rules) {
|
|
388
|
+
$this->errors = array();
|
|
389
|
+
|
|
390
|
+
foreach ($rules as $field => $rule_string) {
|
|
391
|
+
$rules_array = explode('|', $rule_string);
|
|
392
|
+
|
|
393
|
+
foreach ($rules_array as $rule) {
|
|
394
|
+
$this->apply_rule($field, $data[$field] ?? '', $rule);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return empty($this->errors);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Apply validation rule
|
|
403
|
+
*
|
|
404
|
+
* @param string $field Field name
|
|
405
|
+
* @param mixed $value Field value
|
|
406
|
+
* @param string $rule Rule to apply
|
|
407
|
+
*/
|
|
408
|
+
private function apply_rule($field, $value, $rule) {
|
|
409
|
+
if ($rule === 'required' && empty($value)) {
|
|
410
|
+
$this->errors[$field][] = sprintf(__('%s is required', 'task-manager-mvc'), ucfirst($field));
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
if (strpos($rule, 'min:') === 0) {
|
|
414
|
+
$min = (int) substr($rule, 4);
|
|
415
|
+
if (strlen($value) < $min) {
|
|
416
|
+
$this->errors[$field][] = sprintf(__('%s must be at least %d characters', 'task-manager-mvc'), ucfirst($field), $min);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (strpos($rule, 'max:') === 0) {
|
|
421
|
+
$max = (int) substr($rule, 4);
|
|
422
|
+
if (strlen($value) > $max) {
|
|
423
|
+
$this->errors[$field][] = sprintf(__('%s must not exceed %d characters', 'task-manager-mvc'), ucfirst($field), $max);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Get validation errors
|
|
430
|
+
*
|
|
431
|
+
* @return array
|
|
432
|
+
*/
|
|
433
|
+
public function errors() {
|
|
434
|
+
return $this->errors;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Check if validation has errors
|
|
439
|
+
*
|
|
440
|
+
* @return bool
|
|
441
|
+
*/
|
|
442
|
+
public function has_errors() {
|
|
443
|
+
return !empty($this->errors);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
---
|
|
449
|
+
|
|
450
|
+
## Model Layer
|
|
451
|
+
|
|
452
|
+
### File: `app/models/BaseModel.php`
|
|
453
|
+
|
|
454
|
+
```php
|
|
455
|
+
<?php
|
|
456
|
+
namespace TMM\Models;
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Base Model class - provides common database operations
|
|
460
|
+
*
|
|
461
|
+
* @package Task_Manager_MVC
|
|
462
|
+
*/
|
|
463
|
+
abstract class BaseModel {
|
|
464
|
+
/**
|
|
465
|
+
* Database table name
|
|
466
|
+
*
|
|
467
|
+
* @var string
|
|
468
|
+
*/
|
|
469
|
+
protected $table;
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* Primary key column
|
|
473
|
+
*
|
|
474
|
+
* @var string
|
|
475
|
+
*/
|
|
476
|
+
protected $primary_key = 'id';
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Model attributes
|
|
480
|
+
*
|
|
481
|
+
* @var array
|
|
482
|
+
*/
|
|
483
|
+
protected $attributes = array();
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Get WordPress database object
|
|
487
|
+
*
|
|
488
|
+
* @return wpdb
|
|
489
|
+
*/
|
|
490
|
+
protected function db() {
|
|
491
|
+
global $wpdb;
|
|
492
|
+
return $wpdb;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Get full table name with prefix
|
|
497
|
+
*
|
|
498
|
+
* @return string
|
|
499
|
+
*/
|
|
500
|
+
protected function get_table_name() {
|
|
501
|
+
return $this->db()->prefix . $this->table;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Find record by ID
|
|
506
|
+
*
|
|
507
|
+
* @param int $id Record ID
|
|
508
|
+
* @return static|null
|
|
509
|
+
*/
|
|
510
|
+
public static function find($id) {
|
|
511
|
+
$instance = new static();
|
|
512
|
+
$table = $instance->get_table_name();
|
|
513
|
+
$pk = $instance->primary_key;
|
|
514
|
+
|
|
515
|
+
$result = $instance->db()->get_row(
|
|
516
|
+
$instance->db()->prepare("SELECT * FROM {$table} WHERE {$pk} = %d", $id),
|
|
517
|
+
ARRAY_A
|
|
518
|
+
);
|
|
519
|
+
|
|
520
|
+
if ($result) {
|
|
521
|
+
$instance->attributes = $result;
|
|
522
|
+
return $instance;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return null;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Get all records
|
|
530
|
+
*
|
|
531
|
+
* @param array $where Where conditions
|
|
532
|
+
* @param array $order_by Order by clauses
|
|
533
|
+
* @return array
|
|
534
|
+
*/
|
|
535
|
+
public static function all($where = array(), $order_by = array()) {
|
|
536
|
+
$instance = new static();
|
|
537
|
+
$table = $instance->get_table_name();
|
|
538
|
+
|
|
539
|
+
$sql = "SELECT * FROM {$table}";
|
|
540
|
+
|
|
541
|
+
if (!empty($where)) {
|
|
542
|
+
$conditions = array();
|
|
543
|
+
foreach ($where as $key => $value) {
|
|
544
|
+
$conditions[] = $instance->db()->prepare("{$key} = %s", $value);
|
|
545
|
+
}
|
|
546
|
+
$sql .= " WHERE " . implode(' AND ', $conditions);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
if (!empty($order_by)) {
|
|
550
|
+
$sql .= " ORDER BY " . implode(', ', $order_by);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
$results = $instance->db()->get_results($sql, ARRAY_A);
|
|
554
|
+
|
|
555
|
+
$models = array();
|
|
556
|
+
foreach ($results as $result) {
|
|
557
|
+
$model = new static();
|
|
558
|
+
$model->attributes = $result;
|
|
559
|
+
$models[] = $model;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
return $models;
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Save model (insert or update)
|
|
567
|
+
*
|
|
568
|
+
* @return bool
|
|
569
|
+
*/
|
|
570
|
+
public function save() {
|
|
571
|
+
$table = $this->get_table_name();
|
|
572
|
+
|
|
573
|
+
if (isset($this->attributes[$this->primary_key]) && $this->attributes[$this->primary_key] > 0) {
|
|
574
|
+
// Update
|
|
575
|
+
$id = $this->attributes[$this->primary_key];
|
|
576
|
+
unset($this->attributes[$this->primary_key]);
|
|
577
|
+
|
|
578
|
+
$result = $this->db()->update(
|
|
579
|
+
$table,
|
|
580
|
+
$this->attributes,
|
|
581
|
+
array($this->primary_key => $id)
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
$this->attributes[$this->primary_key] = $id;
|
|
585
|
+
return $result !== false;
|
|
586
|
+
} else {
|
|
587
|
+
// Insert
|
|
588
|
+
$result = $this->db()->insert($table, $this->attributes);
|
|
589
|
+
|
|
590
|
+
if ($result) {
|
|
591
|
+
$this->attributes[$this->primary_key] = $this->db()->insert_id;
|
|
592
|
+
return true;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return false;
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
/**
|
|
600
|
+
* Delete model
|
|
601
|
+
*
|
|
602
|
+
* @return bool
|
|
603
|
+
*/
|
|
604
|
+
public function delete() {
|
|
605
|
+
if (!isset($this->attributes[$this->primary_key])) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
$table = $this->get_table_name();
|
|
610
|
+
|
|
611
|
+
return $this->db()->delete(
|
|
612
|
+
$table,
|
|
613
|
+
array($this->primary_key => $this->attributes[$this->primary_key])
|
|
614
|
+
) !== false;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Get attribute value
|
|
619
|
+
*
|
|
620
|
+
* @param string $key Attribute key
|
|
621
|
+
* @return mixed
|
|
622
|
+
*/
|
|
623
|
+
public function __get($key) {
|
|
624
|
+
return $this->attributes[$key] ?? null;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Set attribute value
|
|
629
|
+
*
|
|
630
|
+
* @param string $key Attribute key
|
|
631
|
+
* @param mixed $value Attribute value
|
|
632
|
+
*/
|
|
633
|
+
public function __set($key, $value) {
|
|
634
|
+
$this->attributes[$key] = $value;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/**
|
|
638
|
+
* Get all attributes
|
|
639
|
+
*
|
|
640
|
+
* @return array
|
|
641
|
+
*/
|
|
642
|
+
public function to_array() {
|
|
643
|
+
return $this->attributes;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
```
|
|
647
|
+
|
|
648
|
+
### File: `app/models/Task.php`
|
|
649
|
+
|
|
650
|
+
```php
|
|
651
|
+
<?php
|
|
652
|
+
namespace TMM\Models;
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Task Model
|
|
656
|
+
*
|
|
657
|
+
* @package Task_Manager_MVC
|
|
658
|
+
*/
|
|
659
|
+
class Task extends BaseModel {
|
|
660
|
+
/**
|
|
661
|
+
* Table name
|
|
662
|
+
*
|
|
663
|
+
* @var string
|
|
664
|
+
*/
|
|
665
|
+
protected $table = 'tmm_tasks';
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Get tasks by status
|
|
669
|
+
*
|
|
670
|
+
* @param string $status Task status
|
|
671
|
+
* @return array
|
|
672
|
+
*/
|
|
673
|
+
public static function by_status($status) {
|
|
674
|
+
return self::all(array('status' => $status), array('created_at DESC'));
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* Get tasks by category
|
|
679
|
+
*
|
|
680
|
+
* @param int $category_id Category ID
|
|
681
|
+
* @return array
|
|
682
|
+
*/
|
|
683
|
+
public static function by_category($category_id) {
|
|
684
|
+
return self::all(array('category_id' => $category_id), array('created_at DESC'));
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* Mark task as complete
|
|
689
|
+
*
|
|
690
|
+
* @return bool
|
|
691
|
+
*/
|
|
692
|
+
public function mark_complete() {
|
|
693
|
+
$this->status = 'completed';
|
|
694
|
+
$this->completed_at = current_time('mysql');
|
|
695
|
+
return $this->save();
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Check if task is overdue
|
|
700
|
+
*
|
|
701
|
+
* @return bool
|
|
702
|
+
*/
|
|
703
|
+
public function is_overdue() {
|
|
704
|
+
if (empty($this->due_date) || $this->status === 'completed') {
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
return strtotime($this->due_date) < time();
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
/**
|
|
712
|
+
* Get category
|
|
713
|
+
*
|
|
714
|
+
* @return Category|null
|
|
715
|
+
*/
|
|
716
|
+
public function category() {
|
|
717
|
+
if (empty($this->category_id)) {
|
|
718
|
+
return null;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
return Category::find($this->category_id);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### File: `app/models/Category.php`
|
|
727
|
+
|
|
728
|
+
```php
|
|
729
|
+
<?php
|
|
730
|
+
namespace TMM\Models;
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Category Model
|
|
734
|
+
*
|
|
735
|
+
* @package Task_Manager_MVC
|
|
736
|
+
*/
|
|
737
|
+
class Category extends BaseModel {
|
|
738
|
+
/**
|
|
739
|
+
* Table name
|
|
740
|
+
*
|
|
741
|
+
* @var string
|
|
742
|
+
*/
|
|
743
|
+
protected $table = 'tmm_categories';
|
|
744
|
+
|
|
745
|
+
/**
|
|
746
|
+
* Get tasks in this category
|
|
747
|
+
*
|
|
748
|
+
* @return array
|
|
749
|
+
*/
|
|
750
|
+
public function tasks() {
|
|
751
|
+
return Task::by_category($this->id);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
/**
|
|
755
|
+
* Get task count
|
|
756
|
+
*
|
|
757
|
+
* @return int
|
|
758
|
+
*/
|
|
759
|
+
public function task_count() {
|
|
760
|
+
$table = $this->db()->prefix . 'tmm_tasks';
|
|
761
|
+
|
|
762
|
+
return (int) $this->db()->get_var(
|
|
763
|
+
$this->db()->prepare(
|
|
764
|
+
"SELECT COUNT(*) FROM {$table} WHERE category_id = %d",
|
|
765
|
+
$this->id
|
|
766
|
+
)
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
```
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
## Controller Layer
|
|
775
|
+
|
|
776
|
+
### File: `app/controllers/BaseController.php`
|
|
777
|
+
|
|
778
|
+
```php
|
|
779
|
+
<?php
|
|
780
|
+
namespace TMM\Controllers;
|
|
781
|
+
|
|
782
|
+
use TMM\Core\View;
|
|
783
|
+
use TMM\Core\Validator;
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Base Controller class
|
|
787
|
+
*
|
|
788
|
+
* @package Task_Manager_MVC
|
|
789
|
+
*/
|
|
790
|
+
abstract class BaseController {
|
|
791
|
+
/**
|
|
792
|
+
* Validator instance
|
|
793
|
+
*
|
|
794
|
+
* @var Validator
|
|
795
|
+
*/
|
|
796
|
+
protected $validator;
|
|
797
|
+
|
|
798
|
+
/**
|
|
799
|
+
* Constructor
|
|
800
|
+
*/
|
|
801
|
+
public function __construct() {
|
|
802
|
+
$this->validator = new Validator();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Render view
|
|
807
|
+
*
|
|
808
|
+
* @param string $view View path
|
|
809
|
+
* @param array $data Data to pass to view
|
|
810
|
+
* @param string $layout Layout to use
|
|
811
|
+
*/
|
|
812
|
+
protected function view($view, $data = array(), $layout = 'admin') {
|
|
813
|
+
View::render($view, $data, $layout);
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Redirect to URL
|
|
818
|
+
*
|
|
819
|
+
* @param string $url URL to redirect to
|
|
820
|
+
*/
|
|
821
|
+
protected function redirect($url) {
|
|
822
|
+
wp_safe_redirect($url);
|
|
823
|
+
exit;
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
/**
|
|
827
|
+
* Redirect back with message
|
|
828
|
+
*
|
|
829
|
+
* @param string $message Message to display
|
|
830
|
+
* @param string $type Message type (success, error, warning, info)
|
|
831
|
+
*/
|
|
832
|
+
protected function redirect_back($message, $type = 'success') {
|
|
833
|
+
set_transient('tmm_flash_message', array('message' => $message, 'type' => $type), 30);
|
|
834
|
+
$this->redirect(wp_get_referer());
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/**
|
|
838
|
+
* Verify nonce
|
|
839
|
+
*
|
|
840
|
+
* @param string $action Nonce action
|
|
841
|
+
* @param string $name Nonce name
|
|
842
|
+
* @return bool
|
|
843
|
+
*/
|
|
844
|
+
protected function verify_nonce($action, $name = '_wpnonce') {
|
|
845
|
+
return isset($_POST[$name]) && wp_verify_nonce($_POST[$name], $action);
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Check user capability
|
|
850
|
+
*
|
|
851
|
+
* @param string $capability Capability to check
|
|
852
|
+
* @return bool
|
|
853
|
+
*/
|
|
854
|
+
protected function can($capability) {
|
|
855
|
+
return current_user_can($capability);
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
### File: `app/controllers/TaskController.php`
|
|
861
|
+
|
|
862
|
+
```php
|
|
863
|
+
<?php
|
|
864
|
+
namespace TMM\Controllers;
|
|
865
|
+
|
|
866
|
+
use TMM\Models\Task;
|
|
867
|
+
use TMM\Models\Category;
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Task Controller
|
|
871
|
+
*
|
|
872
|
+
* @package Task_Manager_MVC
|
|
873
|
+
*/
|
|
874
|
+
class TaskController extends BaseController {
|
|
875
|
+
/**
|
|
876
|
+
* Display task list
|
|
877
|
+
*/
|
|
878
|
+
public function index() {
|
|
879
|
+
$status = isset($_GET['status']) ? sanitize_text_field($_GET['status']) : 'all';
|
|
880
|
+
|
|
881
|
+
if ($status === 'all') {
|
|
882
|
+
$tasks = Task::all(array(), array('created_at DESC'));
|
|
883
|
+
} else {
|
|
884
|
+
$tasks = Task::by_status($status);
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
$this->view('tasks/index', array(
|
|
888
|
+
'tasks' => $tasks,
|
|
889
|
+
'current_status' => $status,
|
|
890
|
+
));
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Show create task form
|
|
895
|
+
*/
|
|
896
|
+
public function create() {
|
|
897
|
+
$categories = Category::all();
|
|
898
|
+
|
|
899
|
+
$this->view('tasks/create', array(
|
|
900
|
+
'categories' => $categories,
|
|
901
|
+
));
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Store new task
|
|
906
|
+
*/
|
|
907
|
+
public function store() {
|
|
908
|
+
// Verify nonce
|
|
909
|
+
if (!$this->verify_nonce('tmm_create_task')) {
|
|
910
|
+
wp_die(__('Security check failed', 'task-manager-mvc'));
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
// Check capability
|
|
914
|
+
if (!$this->can('manage_options')) {
|
|
915
|
+
wp_die(__('You do not have permission to perform this action', 'task-manager-mvc'));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Validate input
|
|
919
|
+
$valid = $this->validator->validate($_POST, array(
|
|
920
|
+
'title' => 'required|min:3|max:200',
|
|
921
|
+
'description' => 'max:1000',
|
|
922
|
+
));
|
|
923
|
+
|
|
924
|
+
if (!$valid) {
|
|
925
|
+
set_transient('tmm_validation_errors', $this->validator->errors(), 30);
|
|
926
|
+
$this->redirect_back('', 'error');
|
|
927
|
+
return;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
// Create task
|
|
931
|
+
$task = new Task();
|
|
932
|
+
$task->title = sanitize_text_field($_POST['title']);
|
|
933
|
+
$task->description = sanitize_textarea_field($_POST['description']);
|
|
934
|
+
$task->category_id = isset($_POST['category_id']) ? absint($_POST['category_id']) : 0;
|
|
935
|
+
$task->due_date = isset($_POST['due_date']) ? sanitize_text_field($_POST['due_date']) : '';
|
|
936
|
+
$task->status = 'pending';
|
|
937
|
+
$task->created_at = current_time('mysql');
|
|
938
|
+
|
|
939
|
+
if ($task->save()) {
|
|
940
|
+
$this->redirect_back(__('Task created successfully', 'task-manager-mvc'), 'success');
|
|
941
|
+
} else {
|
|
942
|
+
$this->redirect_back(__('Failed to create task', 'task-manager-mvc'), 'error');
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
/**
|
|
947
|
+
* Show edit task form
|
|
948
|
+
*/
|
|
949
|
+
public function edit() {
|
|
950
|
+
$task_id = isset($_GET['id']) ? absint($_GET['id']) : 0;
|
|
951
|
+
$task = Task::find($task_id);
|
|
952
|
+
|
|
953
|
+
if (!$task) {
|
|
954
|
+
wp_die(__('Task not found', 'task-manager-mvc'));
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
$categories = Category::all();
|
|
958
|
+
|
|
959
|
+
$this->view('tasks/edit', array(
|
|
960
|
+
'task' => $task,
|
|
961
|
+
'categories' => $categories,
|
|
962
|
+
));
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* Update task
|
|
967
|
+
*/
|
|
968
|
+
public function update() {
|
|
969
|
+
// Verify nonce
|
|
970
|
+
if (!$this->verify_nonce('tmm_update_task')) {
|
|
971
|
+
wp_die(__('Security check failed', 'task-manager-mvc'));
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
// Check capability
|
|
975
|
+
if (!$this->can('manage_options')) {
|
|
976
|
+
wp_die(__('You do not have permission to perform this action', 'task-manager-mvc'));
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
$task_id = isset($_POST['task_id']) ? absint($_POST['task_id']) : 0;
|
|
980
|
+
$task = Task::find($task_id);
|
|
981
|
+
|
|
982
|
+
if (!$task) {
|
|
983
|
+
$this->redirect_back(__('Task not found', 'task-manager-mvc'), 'error');
|
|
984
|
+
return;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
// Validate input
|
|
988
|
+
$valid = $this->validator->validate($_POST, array(
|
|
989
|
+
'title' => 'required|min:3|max:200',
|
|
990
|
+
'description' => 'max:1000',
|
|
991
|
+
));
|
|
992
|
+
|
|
993
|
+
if (!$valid) {
|
|
994
|
+
set_transient('tmm_validation_errors', $this->validator->errors(), 30);
|
|
995
|
+
$this->redirect_back('', 'error');
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
|
|
999
|
+
// Update task
|
|
1000
|
+
$task->title = sanitize_text_field($_POST['title']);
|
|
1001
|
+
$task->description = sanitize_textarea_field($_POST['description']);
|
|
1002
|
+
$task->category_id = isset($_POST['category_id']) ? absint($_POST['category_id']) : 0;
|
|
1003
|
+
$task->due_date = isset($_POST['due_date']) ? sanitize_text_field($_POST['due_date']) : '';
|
|
1004
|
+
$task->status = isset($_POST['status']) ? sanitize_text_field($_POST['status']) : 'pending';
|
|
1005
|
+
$task->updated_at = current_time('mysql');
|
|
1006
|
+
|
|
1007
|
+
if ($task->save()) {
|
|
1008
|
+
$this->redirect_back(__('Task updated successfully', 'task-manager-mvc'), 'success');
|
|
1009
|
+
} else {
|
|
1010
|
+
$this->redirect_back(__('Failed to update task', 'task-manager-mvc'), 'error');
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
/**
|
|
1015
|
+
* Delete task
|
|
1016
|
+
*/
|
|
1017
|
+
public function delete() {
|
|
1018
|
+
// Verify nonce
|
|
1019
|
+
if (!$this->verify_nonce('tmm_delete_task')) {
|
|
1020
|
+
wp_die(__('Security check failed', 'task-manager-mvc'));
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
// Check capability
|
|
1024
|
+
if (!$this->can('manage_options')) {
|
|
1025
|
+
wp_die(__('You do not have permission to perform this action', 'task-manager-mvc'));
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
$task_id = isset($_POST['task_id']) ? absint($_POST['task_id']) : 0;
|
|
1029
|
+
$task = Task::find($task_id);
|
|
1030
|
+
|
|
1031
|
+
if (!$task) {
|
|
1032
|
+
$this->redirect_back(__('Task not found', 'task-manager-mvc'), 'error');
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
if ($task->delete()) {
|
|
1037
|
+
$this->redirect_back(__('Task deleted successfully', 'task-manager-mvc'), 'success');
|
|
1038
|
+
} else {
|
|
1039
|
+
$this->redirect_back(__('Failed to delete task', 'task-manager-mvc'), 'error');
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
```
|
|
1044
|
+
|
|
1045
|
+
---
|
|
1046
|
+
|
|
1047
|
+
## View Layer
|
|
1048
|
+
|
|
1049
|
+
### File: `app/views/layouts/admin.php`
|
|
1050
|
+
|
|
1051
|
+
```php
|
|
1052
|
+
<div class="wrap tmm-admin">
|
|
1053
|
+
<h1><?php echo esc_html(get_admin_page_title()); ?></h1>
|
|
1054
|
+
|
|
1055
|
+
<?php
|
|
1056
|
+
// Display flash messages
|
|
1057
|
+
$flash = get_transient('tmm_flash_message');
|
|
1058
|
+
if ($flash):
|
|
1059
|
+
delete_transient('tmm_flash_message');
|
|
1060
|
+
?>
|
|
1061
|
+
<div class="notice notice-<?php echo esc_attr($flash['type']); ?> is-dismissible">
|
|
1062
|
+
<p><?php echo esc_html($flash['message']); ?></p>
|
|
1063
|
+
</div>
|
|
1064
|
+
<?php endif; ?>
|
|
1065
|
+
|
|
1066
|
+
<?php
|
|
1067
|
+
// Display validation errors
|
|
1068
|
+
$errors = get_transient('tmm_validation_errors');
|
|
1069
|
+
if ($errors):
|
|
1070
|
+
delete_transient('tmm_validation_errors');
|
|
1071
|
+
?>
|
|
1072
|
+
<div class="notice notice-error is-dismissible">
|
|
1073
|
+
<p><strong><?php _e('Validation errors:', 'task-manager-mvc'); ?></strong></p>
|
|
1074
|
+
<ul>
|
|
1075
|
+
<?php foreach ($errors as $field => $field_errors): ?>
|
|
1076
|
+
<?php foreach ($field_errors as $error): ?>
|
|
1077
|
+
<li><?php echo esc_html($error); ?></li>
|
|
1078
|
+
<?php endforeach; ?>
|
|
1079
|
+
<?php endforeach; ?>
|
|
1080
|
+
</ul>
|
|
1081
|
+
</div>
|
|
1082
|
+
<?php endif; ?>
|
|
1083
|
+
|
|
1084
|
+
<div class="tmm-content">
|
|
1085
|
+
<?php echo $content; ?>
|
|
1086
|
+
</div>
|
|
1087
|
+
</div>
|
|
1088
|
+
```
|
|
1089
|
+
|
|
1090
|
+
### File: `app/views/tasks/index.php`
|
|
1091
|
+
|
|
1092
|
+
```php
|
|
1093
|
+
<div class="tmm-tasks-index">
|
|
1094
|
+
<div class="tmm-header">
|
|
1095
|
+
<a href="<?php echo admin_url('admin.php?page=tmm-tasks&action=tasks/create'); ?>" class="button button-primary">
|
|
1096
|
+
<?php _e('Add New Task', 'task-manager-mvc'); ?>
|
|
1097
|
+
</a>
|
|
1098
|
+
|
|
1099
|
+
<div class="tmm-filters">
|
|
1100
|
+
<a href="<?php echo admin_url('admin.php?page=tmm-tasks&status=all'); ?>"
|
|
1101
|
+
class="<?php echo $current_status === 'all' ? 'current' : ''; ?>">
|
|
1102
|
+
<?php _e('All', 'task-manager-mvc'); ?>
|
|
1103
|
+
</a>
|
|
1104
|
+
<a href="<?php echo admin_url('admin.php?page=tmm-tasks&status=pending'); ?>"
|
|
1105
|
+
class="<?php echo $current_status === 'pending' ? 'current' : ''; ?>">
|
|
1106
|
+
<?php _e('Pending', 'task-manager-mvc'); ?>
|
|
1107
|
+
</a>
|
|
1108
|
+
<a href="<?php echo admin_url('admin.php?page=tmm-tasks&status=completed'); ?>"
|
|
1109
|
+
class="<?php echo $current_status === 'completed' ? 'current' : ''; ?>">
|
|
1110
|
+
<?php _e('Completed', 'task-manager-mvc'); ?>
|
|
1111
|
+
</a>
|
|
1112
|
+
</div>
|
|
1113
|
+
</div>
|
|
1114
|
+
|
|
1115
|
+
<table class="wp-list-table widefat fixed striped">
|
|
1116
|
+
<thead>
|
|
1117
|
+
<tr>
|
|
1118
|
+
<th><?php _e('Title', 'task-manager-mvc'); ?></th>
|
|
1119
|
+
<th><?php _e('Category', 'task-manager-mvc'); ?></th>
|
|
1120
|
+
<th><?php _e('Due Date', 'task-manager-mvc'); ?></th>
|
|
1121
|
+
<th><?php _e('Status', 'task-manager-mvc'); ?></th>
|
|
1122
|
+
<th><?php _e('Actions', 'task-manager-mvc'); ?></th>
|
|
1123
|
+
</tr>
|
|
1124
|
+
</thead>
|
|
1125
|
+
<tbody>
|
|
1126
|
+
<?php if (empty($tasks)): ?>
|
|
1127
|
+
<tr>
|
|
1128
|
+
<td colspan="5"><?php _e('No tasks found', 'task-manager-mvc'); ?></td>
|
|
1129
|
+
</tr>
|
|
1130
|
+
<?php else: ?>
|
|
1131
|
+
<?php foreach ($tasks as $task): ?>
|
|
1132
|
+
<tr class="<?php echo $task->is_overdue() ? 'tmm-overdue' : ''; ?>">
|
|
1133
|
+
<td>
|
|
1134
|
+
<strong><?php echo esc_html($task->title); ?></strong>
|
|
1135
|
+
<?php if ($task->is_overdue()): ?>
|
|
1136
|
+
<span class="tmm-badge tmm-badge-danger"><?php _e('Overdue', 'task-manager-mvc'); ?></span>
|
|
1137
|
+
<?php endif; ?>
|
|
1138
|
+
</td>
|
|
1139
|
+
<td>
|
|
1140
|
+
<?php
|
|
1141
|
+
$category = $task->category();
|
|
1142
|
+
echo $category ? esc_html($category->name) : '—';
|
|
1143
|
+
?>
|
|
1144
|
+
</td>
|
|
1145
|
+
<td><?php echo $task->due_date ? esc_html(date('M j, Y', strtotime($task->due_date))) : '—'; ?></td>
|
|
1146
|
+
<td>
|
|
1147
|
+
<span class="tmm-status tmm-status-<?php echo esc_attr($task->status); ?>">
|
|
1148
|
+
<?php echo esc_html(ucfirst($task->status)); ?>
|
|
1149
|
+
</span>
|
|
1150
|
+
</td>
|
|
1151
|
+
<td>
|
|
1152
|
+
<a href="<?php echo admin_url('admin.php?page=tmm-tasks&action=tasks/edit&id=' . $task->id); ?>"
|
|
1153
|
+
class="button button-small">
|
|
1154
|
+
<?php _e('Edit', 'task-manager-mvc'); ?>
|
|
1155
|
+
</a>
|
|
1156
|
+
|
|
1157
|
+
<form method="post" action="<?php echo admin_url('admin.php?page=tmm-tasks&action=tasks/delete'); ?>"
|
|
1158
|
+
style="display:inline;"
|
|
1159
|
+
onsubmit="return confirm('<?php _e('Are you sure?', 'task-manager-mvc'); ?>');">
|
|
1160
|
+
<?php wp_nonce_field('tmm_delete_task'); ?>
|
|
1161
|
+
<input type="hidden" name="task_id" value="<?php echo esc_attr($task->id); ?>">
|
|
1162
|
+
<button type="submit" class="button button-small button-link-delete">
|
|
1163
|
+
<?php _e('Delete', 'task-manager-mvc'); ?>
|
|
1164
|
+
</button>
|
|
1165
|
+
</form>
|
|
1166
|
+
</td>
|
|
1167
|
+
</tr>
|
|
1168
|
+
<?php endforeach; ?>
|
|
1169
|
+
<?php endif; ?>
|
|
1170
|
+
</tbody>
|
|
1171
|
+
</table>
|
|
1172
|
+
</div>
|
|
1173
|
+
```
|
|
1174
|
+
|
|
1175
|
+
### File: `app/views/tasks/create.php`
|
|
1176
|
+
|
|
1177
|
+
```php
|
|
1178
|
+
<div class="tmm-task-form">
|
|
1179
|
+
<form method="post" action="<?php echo admin_url('admin.php?page=tmm-tasks&action=tasks/store'); ?>">
|
|
1180
|
+
<?php wp_nonce_field('tmm_create_task'); ?>
|
|
1181
|
+
|
|
1182
|
+
<table class="form-table">
|
|
1183
|
+
<tr>
|
|
1184
|
+
<th scope="row">
|
|
1185
|
+
<label for="title"><?php _e('Title', 'task-manager-mvc'); ?> <span class="required">*</span></label>
|
|
1186
|
+
</th>
|
|
1187
|
+
<td>
|
|
1188
|
+
<input type="text"
|
|
1189
|
+
id="title"
|
|
1190
|
+
name="title"
|
|
1191
|
+
class="regular-text"
|
|
1192
|
+
required>
|
|
1193
|
+
</td>
|
|
1194
|
+
</tr>
|
|
1195
|
+
|
|
1196
|
+
<tr>
|
|
1197
|
+
<th scope="row">
|
|
1198
|
+
<label for="description"><?php _e('Description', 'task-manager-mvc'); ?></label>
|
|
1199
|
+
</th>
|
|
1200
|
+
<td>
|
|
1201
|
+
<textarea id="description"
|
|
1202
|
+
name="description"
|
|
1203
|
+
rows="5"
|
|
1204
|
+
class="large-text"></textarea>
|
|
1205
|
+
</td>
|
|
1206
|
+
</tr>
|
|
1207
|
+
|
|
1208
|
+
<tr>
|
|
1209
|
+
<th scope="row">
|
|
1210
|
+
<label for="category_id"><?php _e('Category', 'task-manager-mvc'); ?></label>
|
|
1211
|
+
</th>
|
|
1212
|
+
<td>
|
|
1213
|
+
<select id="category_id" name="category_id">
|
|
1214
|
+
<option value="0"><?php _e('None', 'task-manager-mvc'); ?></option>
|
|
1215
|
+
<?php foreach ($categories as $category): ?>
|
|
1216
|
+
<option value="<?php echo esc_attr($category->id); ?>">
|
|
1217
|
+
<?php echo esc_html($category->name); ?>
|
|
1218
|
+
</option>
|
|
1219
|
+
<?php endforeach; ?>
|
|
1220
|
+
</select>
|
|
1221
|
+
</td>
|
|
1222
|
+
</tr>
|
|
1223
|
+
|
|
1224
|
+
<tr>
|
|
1225
|
+
<th scope="row">
|
|
1226
|
+
<label for="due_date"><?php _e('Due Date', 'task-manager-mvc'); ?></label>
|
|
1227
|
+
</th>
|
|
1228
|
+
<td>
|
|
1229
|
+
<input type="date"
|
|
1230
|
+
id="due_date"
|
|
1231
|
+
name="due_date">
|
|
1232
|
+
</td>
|
|
1233
|
+
</tr>
|
|
1234
|
+
</table>
|
|
1235
|
+
|
|
1236
|
+
<p class="submit">
|
|
1237
|
+
<input type="submit"
|
|
1238
|
+
class="button button-primary"
|
|
1239
|
+
value="<?php _e('Create Task', 'task-manager-mvc'); ?>">
|
|
1240
|
+
<a href="<?php echo admin_url('admin.php?page=tmm-tasks'); ?>"
|
|
1241
|
+
class="button">
|
|
1242
|
+
<?php _e('Cancel', 'task-manager-mvc'); ?>
|
|
1243
|
+
</a>
|
|
1244
|
+
</p>
|
|
1245
|
+
</form>
|
|
1246
|
+
</div>
|
|
1247
|
+
```
|
|
1248
|
+
|
|
1249
|
+
---
|
|
1250
|
+
|
|
1251
|
+
## MVC Principles Demonstrated
|
|
1252
|
+
|
|
1253
|
+
### 1. **Model** - Data and Business Logic
|
|
1254
|
+
|
|
1255
|
+
✅ **Encapsulates data access**: All database operations in model classes
|
|
1256
|
+
✅ **Business logic**: Methods like `is_overdue()`, `mark_complete()`
|
|
1257
|
+
✅ **Relationships**: `task->category()`, `category->tasks()`
|
|
1258
|
+
✅ **Reusable**: Models used by multiple controllers
|
|
1259
|
+
|
|
1260
|
+
### 2. **View** - Presentation Layer
|
|
1261
|
+
|
|
1262
|
+
✅ **No business logic**: Views only display data
|
|
1263
|
+
✅ **Reusable layouts**: Admin layout wraps all views
|
|
1264
|
+
✅ **Partials**: Reusable view components
|
|
1265
|
+
✅ **Proper escaping**: All output escaped for security
|
|
1266
|
+
|
|
1267
|
+
### 3. **Controller** - Request Handling
|
|
1268
|
+
|
|
1269
|
+
✅ **Handles requests**: Processes form submissions
|
|
1270
|
+
✅ **Coordinates**: Calls models, passes data to views
|
|
1271
|
+
✅ **Validation**: Validates input before processing
|
|
1272
|
+
✅ **Security**: Nonce verification, capability checks
|
|
1273
|
+
|
|
1274
|
+
### 4. **Router** - URL Mapping
|
|
1275
|
+
|
|
1276
|
+
✅ **Clean URLs**: Maps actions to controller methods
|
|
1277
|
+
✅ **RESTful**: GET for display, POST for modifications
|
|
1278
|
+
✅ **Centralized**: All routes in one place
|
|
1279
|
+
|
|
1280
|
+
---
|
|
1281
|
+
|
|
1282
|
+
## Advantages of MVC Pattern
|
|
1283
|
+
|
|
1284
|
+
### ✅ Separation of Concerns
|
|
1285
|
+
|
|
1286
|
+
- **Models**: Handle data and business logic
|
|
1287
|
+
- **Views**: Handle presentation
|
|
1288
|
+
- **Controllers**: Handle request/response flow
|
|
1289
|
+
- **Easy to modify**: Change one layer without affecting others
|
|
1290
|
+
|
|
1291
|
+
### ✅ Testability
|
|
1292
|
+
|
|
1293
|
+
- **Unit test models**: Test business logic independently
|
|
1294
|
+
- **Test controllers**: Mock models and test request handling
|
|
1295
|
+
- **Test views**: Verify output without database
|
|
1296
|
+
|
|
1297
|
+
### ✅ Reusability
|
|
1298
|
+
|
|
1299
|
+
- **Models**: Reused across admin and public
|
|
1300
|
+
- **Views**: Partials and layouts reduce duplication
|
|
1301
|
+
- **Controllers**: Base controller provides common functionality
|
|
1302
|
+
|
|
1303
|
+
### ✅ Maintainability
|
|
1304
|
+
|
|
1305
|
+
- **Clear structure**: Easy to find code
|
|
1306
|
+
- **Predictable**: Consistent patterns throughout
|
|
1307
|
+
- **Scalable**: Easy to add new features
|
|
1308
|
+
|
|
1309
|
+
### ✅ Team Collaboration
|
|
1310
|
+
|
|
1311
|
+
- **Parallel development**: Frontend and backend developers work independently
|
|
1312
|
+
- **Clear responsibilities**: Each layer has specific role
|
|
1313
|
+
- **Code review**: Smaller, focused files
|
|
1314
|
+
|
|
1315
|
+
---
|
|
1316
|
+
|
|
1317
|
+
## When to Use MVC Pattern
|
|
1318
|
+
|
|
1319
|
+
### ✅ Good Use Cases
|
|
1320
|
+
|
|
1321
|
+
- **Complex business logic**: Multiple models with relationships
|
|
1322
|
+
- **Multiple views**: Different ways to display same data
|
|
1323
|
+
- **Team development**: Frontend and backend developers
|
|
1324
|
+
- **Testability required**: Unit and integration testing
|
|
1325
|
+
- **Long-term maintenance**: Plugin will evolve over time
|
|
1326
|
+
- **API development**: Models can serve both UI and API
|
|
1327
|
+
|
|
1328
|
+
### ❌ Not Suitable For
|
|
1329
|
+
|
|
1330
|
+
- **Simple plugins**: Single feature, minimal logic (use Pattern 1)
|
|
1331
|
+
- **Quick prototypes**: Temporary or experimental code
|
|
1332
|
+
- **Beginner projects**: Learning WordPress basics
|
|
1333
|
+
- **Micro-plugins**: Very specific, focused functionality
|
|
1334
|
+
|
|
1335
|
+
---
|
|
1336
|
+
|
|
1337
|
+
## Best Practices
|
|
1338
|
+
|
|
1339
|
+
### 1. **Model Best Practices**
|
|
1340
|
+
|
|
1341
|
+
```php
|
|
1342
|
+
// Good - Business logic in model
|
|
1343
|
+
class Task extends BaseModel {
|
|
1344
|
+
public function is_overdue() {
|
|
1345
|
+
return strtotime($this->due_date) < time();
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// Bad - Business logic in controller
|
|
1350
|
+
if (strtotime($task->due_date) < time()) {
|
|
1351
|
+
// ...
|
|
1352
|
+
}
|
|
1353
|
+
```
|
|
1354
|
+
|
|
1355
|
+
### 2. **View Best Practices**
|
|
1356
|
+
|
|
1357
|
+
```php
|
|
1358
|
+
// Good - No logic in view
|
|
1359
|
+
<?php echo esc_html($task->title); ?>
|
|
1360
|
+
|
|
1361
|
+
// Bad - Business logic in view
|
|
1362
|
+
<?php
|
|
1363
|
+
$category = Category::find($task->category_id);
|
|
1364
|
+
echo $category->name;
|
|
1365
|
+
?>
|
|
1366
|
+
```
|
|
1367
|
+
|
|
1368
|
+
### 3. **Controller Best Practices**
|
|
1369
|
+
|
|
1370
|
+
```php
|
|
1371
|
+
// Good - Thin controller
|
|
1372
|
+
public function store() {
|
|
1373
|
+
$this->validate_request();
|
|
1374
|
+
$task = $this->create_task_from_request();
|
|
1375
|
+
$task->save();
|
|
1376
|
+
$this->redirect_with_message('Success');
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
// Bad - Fat controller with business logic
|
|
1380
|
+
public function store() {
|
|
1381
|
+
// 100 lines of business logic
|
|
1382
|
+
}
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
### 4. **Router Best Practices**
|
|
1386
|
+
|
|
1387
|
+
```php
|
|
1388
|
+
// Good - RESTful routes
|
|
1389
|
+
$this->add_route('GET', 'tasks', 'TaskController@index');
|
|
1390
|
+
$this->add_route('POST', 'tasks/store', 'TaskController@store');
|
|
1391
|
+
|
|
1392
|
+
// Bad - Inconsistent routes
|
|
1393
|
+
$this->add_route('GET', 'show_tasks', 'TaskController@show');
|
|
1394
|
+
$this->add_route('GET', 'save_task', 'TaskController@save');
|
|
1395
|
+
```
|
|
1396
|
+
|
|
1397
|
+
### 5. **Security Best Practices**
|
|
1398
|
+
|
|
1399
|
+
```php
|
|
1400
|
+
// Always in controllers
|
|
1401
|
+
public function store() {
|
|
1402
|
+
// 1. Verify nonce
|
|
1403
|
+
if (!$this->verify_nonce('action_name')) {
|
|
1404
|
+
wp_die('Security check failed');
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
// 2. Check capability
|
|
1408
|
+
if (!$this->can('manage_options')) {
|
|
1409
|
+
wp_die('Permission denied');
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
// 3. Validate input
|
|
1413
|
+
$this->validator->validate($_POST, $rules);
|
|
1414
|
+
|
|
1415
|
+
// 4. Sanitize input
|
|
1416
|
+
$value = sanitize_text_field($_POST['value']);
|
|
1417
|
+
}
|
|
1418
|
+
```
|
|
1419
|
+
|
|
1420
|
+
---
|
|
1421
|
+
|
|
1422
|
+
## Installation Example
|
|
1423
|
+
|
|
1424
|
+
### File: `install.php`
|
|
1425
|
+
|
|
1426
|
+
```php
|
|
1427
|
+
<?php
|
|
1428
|
+
class TMM_Install {
|
|
1429
|
+
public static function activate() {
|
|
1430
|
+
global $wpdb;
|
|
1431
|
+
|
|
1432
|
+
$charset_collate = $wpdb->get_charset_collate();
|
|
1433
|
+
|
|
1434
|
+
// Tasks table
|
|
1435
|
+
$tasks_table = $wpdb->prefix . 'tmm_tasks';
|
|
1436
|
+
$sql_tasks = "CREATE TABLE $tasks_table (
|
|
1437
|
+
id bigint(20) NOT NULL AUTO_INCREMENT,
|
|
1438
|
+
title varchar(200) NOT NULL,
|
|
1439
|
+
description text,
|
|
1440
|
+
category_id bigint(20) DEFAULT 0,
|
|
1441
|
+
due_date date,
|
|
1442
|
+
status varchar(20) DEFAULT 'pending',
|
|
1443
|
+
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
|
1444
|
+
updated_at datetime,
|
|
1445
|
+
completed_at datetime,
|
|
1446
|
+
PRIMARY KEY (id),
|
|
1447
|
+
KEY category_id (category_id),
|
|
1448
|
+
KEY status (status)
|
|
1449
|
+
) $charset_collate;";
|
|
1450
|
+
|
|
1451
|
+
// Categories table
|
|
1452
|
+
$categories_table = $wpdb->prefix . 'tmm_categories';
|
|
1453
|
+
$sql_categories = "CREATE TABLE $categories_table (
|
|
1454
|
+
id bigint(20) NOT NULL AUTO_INCREMENT,
|
|
1455
|
+
name varchar(100) NOT NULL,
|
|
1456
|
+
slug varchar(100) NOT NULL,
|
|
1457
|
+
description text,
|
|
1458
|
+
created_at datetime DEFAULT CURRENT_TIMESTAMP,
|
|
1459
|
+
PRIMARY KEY (id),
|
|
1460
|
+
UNIQUE KEY slug (slug)
|
|
1461
|
+
) $charset_collate;";
|
|
1462
|
+
|
|
1463
|
+
require_once(ABSPATH . 'wp-admin/includes/upgrade.php');
|
|
1464
|
+
dbDelta($sql_tasks);
|
|
1465
|
+
dbDelta($sql_categories);
|
|
1466
|
+
|
|
1467
|
+
add_option('tmm_db_version', '1.0.0');
|
|
1468
|
+
}
|
|
1469
|
+
|
|
1470
|
+
public static function deactivate() {
|
|
1471
|
+
// Cleanup if needed
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
```
|
|
1475
|
+
|
|
1476
|
+
---
|
|
1477
|
+
|
|
1478
|
+
## Comparison with Other Patterns
|
|
1479
|
+
|
|
1480
|
+
| Feature | Simple Procedural | OOP | MVC | WPPB |
|
|
1481
|
+
|---------|------------------|-----|-----|------|
|
|
1482
|
+
| **Complexity** | Low | Medium-High | Medium-High | High |
|
|
1483
|
+
| **File Count** | 1-3 | 10-30 | 15-40 | 20-100 |
|
|
1484
|
+
| **Learning Curve** | Easy | Moderate | Moderate-Steep | Steep |
|
|
1485
|
+
| **Separation of Concerns** | None | Good | Excellent | Excellent |
|
|
1486
|
+
| **Testability** | Low | High | Very High | Very High |
|
|
1487
|
+
| **Business Logic** | Mixed | In classes | In models | In models |
|
|
1488
|
+
| **View Reusability** | None | Low | High | High |
|
|
1489
|
+
| **Team Size** | 1 | 2-5 | 2-5 | 3-10 |
|
|
1490
|
+
| **Best For** | Simple plugins | Complex plugins | Complex business logic | Professional plugins |
|
|
1491
|
+
|
|
1492
|
+
---
|
|
1493
|
+
|
|
1494
|
+
## Migration Path
|
|
1495
|
+
|
|
1496
|
+
### From OOP to MVC
|
|
1497
|
+
|
|
1498
|
+
1. **Extract models** from existing classes
|
|
1499
|
+
2. **Create controllers** for request handling
|
|
1500
|
+
3. **Move views** to separate files
|
|
1501
|
+
4. **Add router** for URL mapping
|
|
1502
|
+
5. **Refactor** to follow MVC principles
|
|
1503
|
+
|
|
1504
|
+
### Example Migration
|
|
1505
|
+
|
|
1506
|
+
**Before (OOP):**
|
|
1507
|
+
```php
|
|
1508
|
+
class Task_Manager {
|
|
1509
|
+
public function display_tasks() {
|
|
1510
|
+
$tasks = $this->get_tasks();
|
|
1511
|
+
include 'views/tasks.php';
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1514
|
+
private function get_tasks() {
|
|
1515
|
+
global $wpdb;
|
|
1516
|
+
return $wpdb->get_results("SELECT * FROM tasks");
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
```
|
|
1520
|
+
|
|
1521
|
+
**After (MVC):**
|
|
1522
|
+
```php
|
|
1523
|
+
// Model
|
|
1524
|
+
class Task extends BaseModel {
|
|
1525
|
+
protected $table = 'tasks';
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// Controller
|
|
1529
|
+
class TaskController extends BaseController {
|
|
1530
|
+
public function index() {
|
|
1531
|
+
$tasks = Task::all();
|
|
1532
|
+
$this->view('tasks/index', compact('tasks'));
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
// View (tasks/index.php)
|
|
1537
|
+
<?php foreach ($tasks as $task): ?>
|
|
1538
|
+
<div><?php echo esc_html($task->title); ?></div>
|
|
1539
|
+
<?php endforeach; ?>
|
|
1540
|
+
```
|
|
1541
|
+
|
|
1542
|
+
---
|
|
1543
|
+
|
|
1544
|
+
## Testing Example
|
|
1545
|
+
|
|
1546
|
+
### Unit Test for Task Model
|
|
1547
|
+
|
|
1548
|
+
```php
|
|
1549
|
+
<?php
|
|
1550
|
+
class Task_Model_Test extends WP_UnitTestCase {
|
|
1551
|
+
public function test_task_creation() {
|
|
1552
|
+
$task = new TMM\Models\Task();
|
|
1553
|
+
$task->title = 'Test Task';
|
|
1554
|
+
$task->status = 'pending';
|
|
1555
|
+
$task->save();
|
|
1556
|
+
|
|
1557
|
+
$this->assertGreaterThan(0, $task->id);
|
|
1558
|
+
$this->assertEquals('Test Task', $task->title);
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
public function test_is_overdue() {
|
|
1562
|
+
$task = new TMM\Models\Task();
|
|
1563
|
+
$task->due_date = date('Y-m-d', strtotime('-1 day'));
|
|
1564
|
+
$task->status = 'pending';
|
|
1565
|
+
|
|
1566
|
+
$this->assertTrue($task->is_overdue());
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
public function test_category_relationship() {
|
|
1570
|
+
$category = new TMM\Models\Category();
|
|
1571
|
+
$category->name = 'Work';
|
|
1572
|
+
$category->save();
|
|
1573
|
+
|
|
1574
|
+
$task = new TMM\Models\Task();
|
|
1575
|
+
$task->category_id = $category->id;
|
|
1576
|
+
$task->save();
|
|
1577
|
+
|
|
1578
|
+
$this->assertEquals('Work', $task->category()->name);
|
|
1579
|
+
}
|
|
1580
|
+
}
|
|
1581
|
+
```
|
|
1582
|
+
|
|
1583
|
+
---
|
|
1584
|
+
|
|
1585
|
+
## Related Patterns
|
|
1586
|
+
|
|
1587
|
+
- **Pattern 1**: [Simple Procedural Plugin](simple-procedural-plugin.md) - For simple plugins
|
|
1588
|
+
- **Pattern 3**: [Object-Oriented Plugin](object-oriented-plugin.md) - OOP without MVC
|
|
1589
|
+
- **Pattern 4**: [WPPB Plugin](wppb-plugin.md) - Professional standard
|
|
1590
|
+
- **Pattern 6**: [Service Container Plugin](service-container-plugin.md) - Dependency injection
|
|
1591
|
+
|
|
1592
|
+
---
|
|
1593
|
+
|
|
1594
|
+
## Additional Resources
|
|
1595
|
+
|
|
1596
|
+
- [MVC Pattern Explained](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller)
|
|
1597
|
+
- [WordPress MVC Frameworks](https://wordpress.org/plugins/tags/mvc/)
|
|
1598
|
+
- [Testing WordPress Plugins](https://make.wordpress.org/core/handbook/testing/automated-testing/phpunit/)
|
|
1599
|
+
- [RESTful Routing](https://restfulapi.net/)
|
|
1600
|
+
|
|
1601
|
+
---
|
|
1602
|
+
|
|
1603
|
+
## Summary
|
|
1604
|
+
|
|
1605
|
+
This MVC plugin example demonstrates:
|
|
1606
|
+
|
|
1607
|
+
✅ **Complete MVC architecture** with models, views, controllers
|
|
1608
|
+
✅ **Router** for clean URL mapping
|
|
1609
|
+
✅ **Base classes** for code reusability
|
|
1610
|
+
✅ **Model layer** with business logic and relationships
|
|
1611
|
+
✅ **View layer** with layouts and partials
|
|
1612
|
+
✅ **Controller layer** with request handling and validation
|
|
1613
|
+
✅ **Security best practices** (nonces, capability checks, sanitization)
|
|
1614
|
+
✅ **Validation** with custom validator class
|
|
1615
|
+
✅ **Flash messages** for user feedback
|
|
1616
|
+
✅ **Database abstraction** in base model
|
|
1617
|
+
✅ **RESTful routing** for clean URLs
|
|
1618
|
+
✅ **Testable code** structure
|
|
1619
|
+
|
|
1620
|
+
**Perfect for**: Complex business logic, multiple views, team development, testable code, long-term maintenance.
|
|
1621
|
+
|
|
1622
|
+
**Next steps**: For even more structure with dependency injection, consider Pattern 6 (Service Container) or modern frameworks like Laravel-style WordPress development.
|
|
1623
|
+
|